diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 717eecc75..99dfd1068 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 a31c5e63a..972b9dacd 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -71,8 +71,14 @@ export interface AppConfigInterface { datasetJsonScientificMetadata: boolean; datasetReduceEnabled: boolean; datasetDetailsShowMissingProposalId: boolean; + datasetActionsEnabled: boolean; + datasetActions: any[]; datafilesActionsEnabled: boolean; datafilesActions: any[]; + datasetDetailsActionsEnabled: boolean; + datasetDetailsActions: any[]; + datasetSelectionActionsEnabled: boolean; + datasetSelectionActions: any[]; editDatasetEnabled: boolean; editDatasetSampleEnabled: boolean; editMetadataEnabled: boolean; diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index 948d9296c..8267d0a1d 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -1,93 +1,15 @@
-
-
- You are currently editing - {{ editingPublishedDataDoi }} - dataset list. -
- - - - - - - - - - - -
{ + if (queryParams["share"] === "true") { + this.onShare(); + } + if (queryParams["retrieve"] === "true") { + this.onRetrieve(); + } + }), + ); } ngOnDestroy() { diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts deleted file mode 100644 index e710dc544..000000000 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { - Component, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from "@angular/core"; - -import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; -import { ActionConfig, ActionDataset } from "./datafiles-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"], - standalone: false, -}) -export class DatafilesActionComponent implements OnInit, OnChanges { - @Input({ required: true }) actionConfig: ActionConfig; - @Input({ required: true }) actionDataset: ActionDataset; - @Input({ required: true }) files: DataFiles_File[]; - @Input({ required: true }) maxFileSize: number; - - jwt = ""; - visible = true; - use_mat_icon = false; - use_icon = false; - disabled_condition = "false"; - selectedTotalFileSize = 0; - numberOfFileSelected = 0; - - form: HTMLFormElement = null; - - constructor( - private usersService: UsersService, - private authService: AuthService, - private snackBar: MatSnackBar, - ) { - this.usersService.usersControllerGetUserJWTV3().subscribe((jwt) => { - this.jwt = jwt.jwt; - }); - } - - 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 prepare_disabled_condition() { - if (this.actionConfig.enabled) { - this.disabled_condition = - "!(" + - this.evaluate_disabled_condition(this.actionConfig.enabled) + - ")"; - } else if (this.actionConfig.disabled) { - this.disabled_condition = this.evaluate_disabled_condition( - this.actionConfig.disabled, - ); - } else { - this.disabled_condition = "false"; - } - } - - ngOnInit() { - this.use_mat_icon = this.actionConfig.mat_icon !== undefined; - 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") - .reduce((sum, item) => sum + item.size, 0); - this.numberOfFileSelected = this.files.filter( - (item) => item.selected, - ).length; - } - - 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, - }); - } - - add_input(name, value) { - const input = document.createElement("input"); - input.type = "hidden"; - input.name = name; - input.value = value; - return input; - } - - perform_action() { - const action_type = this.actionConfig.type || "form"; - switch (action_type) { - case "json-download": - return this.type_json_download(); - case "form": - default: - return this.type_form(); - } - } - - type_form() { - if (this.form !== null) { - document.body.removeChild(this.form); - } - - this.form = document.createElement("form"); - this.form.target = this.actionConfig.target || "_self"; - this.form.method = this.actionConfig.method || "POST"; - this.form.action = this.actionConfig.url; - this.form.style.display = "none"; - - this.form.appendChild( - this.add_input("auth_token", `Bearer ${this.authService.getToken().id}`), - ); - - 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), - ); - - let index = 0; - for (const item of this.files) { - if ( - this.actionConfig.files === "all" || - (this.actionConfig.files === "selected" && item.selected) - ) { - this.form.appendChild( - this.add_input("files[" + index + "]", item.path), - ); - index = index + 1; - } - } - - document.body.appendChild(this.form); - this.form.submit(); - - return true; - } - - type_json_download() { - let payload = ""; - if (this.actionConfig.payload) { - 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( - /{{ 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, - dataset: this.actionDataset.pid, - directory: this.actionDataset.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()); - - fetch(this.actionConfig.url, { - method: this.actionConfig.method || "POST", - headers: { - "Content-Type": "application/json", - }, - body: 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, - }, - ); - }); - - return true; - } -} diff --git a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts deleted file mode 100644 index 1218e829a..000000000 --- a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface ActionConfig { - id: string; - description?: string; - order: number; - label: string; - files: string; - mat_icon?: string; - icon?: string; - type?: string; - url: string; - target: string; - authorization: string[]; - method?: string; - enabled?: string; - disabled?: string; - payload?: string; - filename?: string; -} - -export interface ActionDataset { - pid: string; - sourceFolder: string; -} 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 cdc435d73..000000000 --- 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-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts deleted file mode 100644 index a6698cc91..000000000 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { ActionConfig, ActionDataset } from "./datafiles-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"], - standalone: false, -}) -export class DatafilesActionsComponent { - private _sortedActionsConfig: ActionConfig[]; - - @Input({ required: true }) actionsConfig: ActionConfig[]; - @Input({ required: true }) actionDataset: ActionDataset; - @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 - ); - } - - get maxFileSize(): number { - return this.appConfigService.getConfig().maxDirectDownloadSize || 0; - } - - get sortedActionsConfig(): ActionConfig[] { - this._sortedActionsConfig = this.actionsConfig; - this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => - a.order && b.order ? a.order - b.order : 0, - ); - return this._sortedActionsConfig; - } -} diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index 001ca7d38..397e37302 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

- - + [actionItems]="actionItems" + [visible]="appConfig.datafilesActionsEnabled" + >
{ { provide: AppConfigService, useValue: { getConfig } }, { provide: AuthService, useValue: MockAuthService }, { - provide: DatafilesActionsComponent, + provide: ConfigurableActionsComponent, useClass: MockDatafilesActionsComponent, }, { provide: FileSizePipe }, diff --git a/src/app/datasets/datafiles/datafiles.component.ts b/src/app/datasets/datafiles/datafiles.component.ts index e57a10bf6..761754b25 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 "datasets/datafiles-actions/datafiles-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 = ""; - actionDataset: ActionDataset; + actionItems: ActionItems; count = 0; pageSize = 25; @@ -96,7 +96,6 @@ export class DatafilesComponent icon: "save", sort: false, inList: true, - // pipe: FilePathTruncate, }, { name: "size", @@ -248,9 +247,7 @@ export class DatafilesComponent this.subscriptions.push( this.dataset$.subscribe((dataset) => { if (dataset) { - this.sourceFolder = dataset.sourceFolder; - this.datasetPid = dataset.pid; - this.actionDataset = dataset; + this.actionItems.datasets = [dataset]; } }), ); @@ -269,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 0910b29dd..4efb98147 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/datasets/dataset-detail/dataset-detail/dataset-detail.component.html b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html index 7fe0851b5..b434a7c0e 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,34 +1,18 @@ -
- -
- - Public - -
-
+
description - {{ "General Information" | translate }} + {{ + "General Information" | translate + }} @@ -169,7 +153,9 @@ person - {{ "Creator Information" | translate }} + {{ + "Creator Information" | translate + }} @@ -208,8 +194,10 @@ - folder - {{ "File Information" | translate }} + folder + {{ + "File Information" | translate + }} @@ -233,7 +221,9 @@ library_books - {{ "Related Documents" | translate }} + {{ + "Related Documents" | translate + }} @@ -347,10 +337,11 @@ - - science - {{ "Scientific Metadata" | translate }} + + science + {{ + "Scientific Metadata" | translate + }} { - 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/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts new file mode 100644 index 000000000..7f0c60118 --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -0,0 +1,497 @@ +import { + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from "@angular/core"; + +import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; +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"; +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"; + +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", + styleUrls: ["./configurable-action.component.scss"], + standalone: false, +}) +export class ConfigurableActionComponent implements OnInit, OnChanges { + @Input({ required: true }) actionConfig: ActionConfig; + @Input({ required: true }) actionItems: ActionItems; + @Input() files?: DataFiles_File[]; + + jwt = ""; + use_mat_icon = false; + use_icon = false; + disabled_condition = "false"; + #selectedTotalFileSize = 0; + #numberOfFileSelected = 0; + variables: Record = {}; + + form: HTMLFormElement = null; + + 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; + }); + } + + 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_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.prepare_action_condition(this.actionConfig.enabled) + + ")"; + } else if (this.actionConfig.disabled) { + this.disabled_condition = this.prepare_action_condition( + this.actionConfig.disabled, + ); + } else { + this.disabled_condition = "false"; + } + } + + 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; + this.use_icon = this.actionConfig.icon !== undefined; + this.prepare_disabled_condition(); + this.update_status(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes["actionItems"]) { + this.update_status(); + } + } + + update_status() { + Object.entries(this.actionConfig.variables).forEach(([key,selector]) => { + this.variables[key] = processSelector( + this.actionItems as unknown as JSONValue, + selector) + }) + } + + get disabled() { + this.update_status(); + + const expr = this.disabled_condition; + const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); + + return fn({ + variables: this.variables, + maxDownloadableSize: this.configService.getConfig().maxDirectDownloadSize, + }); + } + + 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({ + variables: this.variables, + maxDownloadableSize: this.configService.getConfig().maxDirectDownloadSize, + }); + } + } + + add_input(name: string, value: string) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = name; + input.value = value; + return input; + } + + perform_action() { + const action_type = this.actionConfig.type || "form"; + switch (action_type) { + case "json-to-download": + return this.type_json_to_download(); + case "xhr": + return this.type_xhr(); + case "link": + return this.type_link(); + case "form": + default: + return this.type_form(); + } + } + + 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); + } + + this.form = document.createElement("form"); + this.form.target = this.actionConfig.target || "_self"; + this.form.method = this.actionConfig.method || "POST"; + this.form.action = this.actionConfig.url; + this.form.style.display = "none"; + + // use the configuration under inputs to create the form + Object.entries(this.actionConfig.inputs).forEach(([input, definition]) => { + + const value = this.get_value_from_definition(definition); + + 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)); + } + }); + + document.body.appendChild(this.form); + this.form.submit(); + + return true; + } + + get_payload() { + let payload = ""; + if (this.actionConfig.payload == "#dump" ) { + payload = JSON.stringify(this.variables); + } else if (this.actionConfig.payload != "#empty" && this.actionConfig.payload) { + payload = this.actionConfig.payload + } + + 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", + }, + ...(this.actionConfig.headers || {}) + }, + 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, + }, + ); + }); + + return true; + } + + type_xhr() { + + 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: { + ...{ + "Content-Type": "application/json", + }, + ...(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; + } + + type_link() { + this.router.navigateByUrl(this.actionConfig.url); + } + +} diff --git a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts new file mode 100644 index 000000000..bdcee5b98 --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -0,0 +1,41 @@ +import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; + +export interface ActionConfig { + id: string; + description?: string; + order: number; + label: string; + files: string; + mat_icon?: string; + icon?: string; + type?: string; + url: string; + target: string; + authorization: string[]; + method?: string; + enabled?: string; + disabled?: string; + payload?: string; + filename?: string; + hidden?: string; + variables?: Record; + inputs?: Record; + headers?: Record; +} + +// export interface ActionItem { +// pid: string; +// sourceFolder?: string; +// isPublished?: boolean; +// } + +export interface ActionItemDataset { + pid: string; + sourceFolder?: string; + isPublished?: boolean; + files?: DataFiles_File[]; +} + +export interface ActionItems { + datasets: ActionItemDataset[], +} \ No newline at end of file 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 52% rename from src/app/datasets/datafiles-actions/datafiles-actions.component.html rename to src/app/shared/modules/configurable-actions/configurable-actions.component.html index b35f645ea..6c23d1ad3 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,11 @@ -
- + - +
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 000000000..5848d29ee --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss @@ -0,0 +1,4 @@ +.configurable-actions { + float: right; + margin: 1em; +} 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 88% 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 5c80ebec8..a3e19c940 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 = [ { @@ -129,10 +129,12 @@ describe("DatafilesActionsComponent", () => { }, ]; 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(); }); @@ -189,7 +191,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 +200,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/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts new file mode 100644 index 000000000..b3c3a4c5f --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -0,0 +1,33 @@ +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({ + selector: "configurable-actions", + templateUrl: "./configurable-actions.component.html", + styleUrls: ["./configurable-actions.component.scss"], + standalone: false, +}) +export class ConfigurableActionsComponent { + private _sortedActionsConfig: ActionConfig[]; + + @Input({ required: true }) actionsConfig: ActionConfig[]; + @Input({ required: true }) actionItems: ActionItems; + @Input() files?: DataFiles_File[]; + @Input() visible = true; + + constructor(public appConfigService: AppConfigService) {} + + get maxFileSize(): number { + return this.appConfigService.getConfig().maxDirectDownloadSize || 0; + } + + get sortedActionsConfig(): ActionConfig[] { + this._sortedActionsConfig = this.actionsConfig; + this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => + a.order && b.order ? a.order - b.order : 0, + ); + return this._sortedActionsConfig; + } +} 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 000000000..6241ffe26 --- /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 f2425219e..045e62fee 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -25,6 +25,7 @@ import { JsonFormsAngularMaterialModule } from "@jsonforms/angular-material"; import { JsonFormsCustomRenderersModule } from "./modules/jsonforms-custom-renderers/jsonforms-custom-renderers.module"; 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"; import { EmptyContentModule } from "./modules/generic-empty-content/empty-content.module"; import { JsonformsAccordionRendererService } from "./services/jsonforms-accordion-renderer.service"; @NgModule({ @@ -48,6 +49,7 @@ import { JsonformsAccordionRendererService } from "./services/jsonforms-accordio ScientificMetadataTreeModule, DynamicMatTableModule.forRoot({}), TranslateModule, + ConfigurableActionsModule, EmptyContentModule, JsonFormsModule, JsonFormsAngularMaterialModule, @@ -79,6 +81,7 @@ import { JsonformsAccordionRendererService } from "./services/jsonforms-accordio FiltersModule, DynamicMatTableModule, TranslateModule, + ConfigurableActionsModule, EmptyContentModule, JsonFormsModule, JsonFormsAngularMaterialModule, diff --git a/src/assets/config.json b/src/assets/config.json index 4395f9b02..a396a7a05 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -59,50 +59,118 @@ "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", "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"] }, { "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", "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" : { + "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", "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 selected files using an instance of sciwyrm", - "order": 3, + "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", @@ -110,20 +178,87 @@ "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\"}}", - "filename": "{{ uuid }}.ipynb" + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path", + "folder": ".datasets[0].sourceFolder" + }, + "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": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", - "order": 1, - "label": "Notebook Selected", - "files": "selected", + "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", - "type": "form", - "url": "https://www.scicat.info/notebook/selected", + "url": "https://www.sciwyrm.info/notebook", "target": "_blank", - "enabled": "#Selected", - "authorization": ["#datasetAccess", "#datasetPublic"] + "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": "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": "#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": {