diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index ac4552cef1..2ecf9daebd 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -295,6 +295,67 @@ "target": "_blank" } ], + "publishedDataActionsEnabled": true, + "publishedDataActions": [ + { + "id": "publish-published-data", + "description": "Publish published data", + "order": 1, + "label": "Publish", + "type": "xhr", + "mat_icon": "publish", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/publish", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData0Status" + }, + "enabled": "@status === 'private'", + "payload": "{\"isPublished\":\"true\"}", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "register-published-data", + "description": "Register published data", + "order": 2, + "label": "Register", + "type": "xhr", + "mat_icon": "registration", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/register", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData0Status" + }, + "enabled": "@status === 'public'", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "amend-published-data", + "description": "Amend published data", + "order": 3, + "label": "Amend", + "type": "xhr", + "mat_icon": "edit", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/amend", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData0Status" + }, + "enabled": "@status === 'registered'", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + } + ], "datasetDetailsActionsEnabled": false, "datasetDetailsActions": [], "selectionActionsEnabled": true, diff --git a/cypress/e2e/published-data/published-data.cy.js b/cypress/e2e/published-data/published-data.cy.js index 97fc09f388..67d5b1b7d2 100644 --- a/cypress/e2e/published-data/published-data.cy.js +++ b/cypress/e2e/published-data/published-data.cy.js @@ -203,9 +203,9 @@ describe("Datasets general", () => { cy.get('[data-cy="status"]').contains("private"); - cy.get('[data-cy="publishButton"]').click(); + cy.get("configurable-action button").contains("Publish").click(); - cy.get("simple-snack-bar").should("contain", "Publishing Failed."); + cy.get("simple-snack-bar").should("contain", "Action failed"); }); it("admins should be able to edit their private published data", () => { @@ -360,7 +360,7 @@ describe("Datasets general", () => { cy.get('[data-cy="status"]').contains("private"); - cy.get('[data-cy="publishButton"]').click(); + cy.get("configurable-action button").contains("Publish").click(); cy.get('[data-cy="status"]').contains("public"); }); @@ -428,7 +428,7 @@ describe("Datasets general", () => { cy.get('[data-cy="status"]').contains("public"); - cy.get('[data-cy="registerButton"]').click(); + cy.get("configurable-action button").contains("Register").click(); cy.get('[data-cy="status"]').contains("registered"); }); @@ -517,7 +517,7 @@ describe("Datasets general", () => { cy.get('[data-cy="status"]').contains("private"); - cy.get('[data-cy="publishButton"]').click(); + cy.get("configurable-action button").contains("Publish").click(); cy.get('[data-cy="status"]').contains("public"); diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index 7a4cde39b1..daf68ebe70 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -167,6 +167,62 @@ } } }, + "publishedDataActionsEnabled": { "type": "boolean" }, + "publishedDataActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { "type": "string" }, + "id": { "type": "string" }, + "description": { "type": "string" }, + "order": { "type": "number" }, + "mat_icon": { "type": "string" }, + "url": { "type": "string" }, + "target": { "type": "string" }, + "enabled": { "type": "string" }, + "authorization": { "type": "array", "items": { "type": "string" } }, + "inputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + }, + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + }, + "headers": { + "type": "object", + "properties": { + "content-type": { "type": "string" }, + "Authorization": { "type": "string" } + } + }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] + }, + "type": { + "type": "string", + "enum": ["form", "xhr", "link", "json-download"] + }, + "icon": { "type": "string" }, + "payload": { "type": "string" }, + "filename": { "type": "string" } + } + } + }, "defaultDatasetsListSettings": { "type": "object", @@ -696,6 +752,97 @@ } ] }, + { + "type": "Group", + "label": "Published Data Actions", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/publishedDataActionsEnabled" + }, + { + "type": "ListWithDetail", + "scope": "#/properties/publishedDataActions", + "options": { + "detail": { + "type": "VerticalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/label" }, + { "type": "Control", "scope": "#/properties/id" }, + { + "type": "Control", + "scope": "#/properties/description" + }, + { "type": "Control", "scope": "#/properties/order" }, + { "type": "Control", "scope": "#/properties/mat_icon" }, + { "type": "Control", "scope": "#/properties/url" }, + { "type": "Control", "scope": "#/properties/target" }, + { "type": "Control", "scope": "#/properties/enabled" }, + { + "type": "Control", + "scope": "#/properties/method" + }, + { "type": "Control", "scope": "#/properties/type" }, + { "type": "Control", "scope": "#/properties/icon" }, + { + "type": "Control", + "scope": "#/properties/payload", + "options": { "multi": true } + }, + { "type": "Control", "scope": "#/properties/filename" }, + { + "type": "Control", + "scope": "#/properties/authorization" + }, + { + "type": "Control", + "scope": "#/properties/variables", + "options": { + "detail": { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/key" }, + { "type": "Control", "scope": "#/properties/value" } + ] + } + } + }, + { + "type": "Control", + "scope": "#/properties/inputs", + "options": { + "detail": { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/key" }, + { "type": "Control", "scope": "#/properties/value" } + ] + } + } + }, + { + "type": "Group", + "label": "Headers", + "elements": [ + { + "type": "Control", + "scope": "#/properties/headers/properties/content-type" + }, + { + "type": "Control", + "scope": "#/properties/headers/properties/Authorization" + } + ] + } + ] + } + } + } + ] + }, { "type": "Group", "label": "Localization", diff --git a/src/app/app-config.service.spec.ts b/src/app/app-config.service.spec.ts index 2670134565..14ba045148 100644 --- a/src/app/app-config.service.spec.ts +++ b/src/app/app-config.service.spec.ts @@ -5,7 +5,6 @@ import { AppConfigService, HelpMessages, } from "app-config.service"; -import { time } from "node:console"; import { Observable, of } from "rxjs"; import { MockHttp } from "shared/MockStubs"; @@ -94,6 +93,8 @@ const appConfig: AppConfigInterface = { datafilesActions: [], datasetSelectionActionsEnabled: false, datasetSelectionActions: [], + publishedDataActionsEnabled: false, + publishedDataActions: [], defaultDatasetsListSettings: { columns: [ { diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 6b42dc9177..871c5527b7 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -97,6 +97,8 @@ export interface AppConfigInterface { datasetDetailsActions: ActionConfig[]; datasetSelectionActionsEnabled: boolean; datasetSelectionActions: ActionConfig[]; + publishedDataActions: ActionConfig[]; + publishedDataActionsEnabled: boolean; editDatasetEnabled: boolean; editDatasetSampleEnabled: boolean; editMetadataEnabled: boolean; diff --git a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html index 0100f54db5..7449f43f64 100644 --- a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html +++ b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html @@ -26,33 +26,13 @@ - - - - + + + diff --git a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.scss b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.scss index 6167f01611..e2af89a3ea 100644 --- a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.scss +++ b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.scss @@ -30,3 +30,9 @@ mat-card { margin: 0 1em; } } + +configurable-actions { + display: flex; + flex-direction: row-reverse; + max-width: 80%; +} diff --git a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.spec.ts b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.spec.ts index e14a0557e0..6c322094f1 100644 --- a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.spec.ts +++ b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.spec.ts @@ -2,8 +2,10 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { PublisheddataDetailsComponent } from "./publisheddata-details.component"; import { MockActivatedRoute, + MockAuthService, MockPublishedDataApi, MockRouter, + MockUserApi, } from "shared/MockStubs"; import { provideMockStore, MockStore } from "@ngrx/store/testing"; import { NgxJsonViewerModule } from "ngx-json-viewer"; @@ -17,17 +19,29 @@ import { AppConfigService } from "app-config.service"; import { PublishedData, PublishedDataService, + UsersService, } from "@scicatproject/scicat-sdk-ts-angular"; -import { selectCurrentPublishedData } from "state-management/selectors/published-data.selectors"; +import { AuthService } from "shared/services/auth/auth.service"; import { - selectIsAdmin, - selectIsLoggedIn, -} from "state-management/selectors/user.selectors"; + provideHttpClient, + withInterceptorsFromDi, +} from "@angular/common/http"; +import { selectCurrentPublishedData } from "state-management/selectors/published-data.selectors"; +import { selectIsAdmin } from "state-management/selectors/user.selectors"; const getConfig = () => ({ editMetadataEnabled: true, editPublishedData: true, jsonMetadataEnabled: true, + publishedDataActionsEnabled: true, + publishedDataActions: [ + { + id: "test-action", + label: "Test Action", + type: "link", + url: "https://example.com", + }, + ], }); const createPublishedData = (status: string) => @@ -55,10 +69,13 @@ describe("PublisheddataDetailsComponent", () => { SharedScicatFrontendModule, ], providers: [ + provideHttpClient(withInterceptorsFromDi()), provideMockStore(), { provide: Router, useClass: MockRouter }, { provide: ActivatedRoute, useClass: MockActivatedRoute }, { provide: PublishedDataService, useClass: MockPublishedDataApi }, + { provide: UsersService, useClass: MockUserApi }, + { provide: AuthService, useClass: MockAuthService }, { provide: AppConfigService, useValue: { getConfig } }, ], }).compileComponents(); @@ -69,7 +86,6 @@ describe("PublisheddataDetailsComponent", () => { createPublishedData("public"), ); store.overrideSelector(selectIsAdmin, false); - store.overrideSelector(selectIsLoggedIn, false); })); beforeEach(() => { @@ -82,21 +98,10 @@ describe("PublisheddataDetailsComponent", () => { expect(component).toBeTruthy(); }); - describe("login state", () => { - it("should hide status actions when the user is not logged in", () => { - const compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('[data-cy="registerButton"]')).toBeNull(); - }); - - it("should show status actions when the user is logged in", () => { - store.overrideSelector(selectIsLoggedIn, true); - store.refreshState(); - fixture.detectChanges(); - + describe("configurable actions", () => { + it("should render configurable-actions element when publishedDataActions are configured", () => { const compiled = fixture.debugElement.nativeElement; - expect( - compiled.querySelector('[data-cy="registerButton"]'), - ).not.toBeNull(); + expect(compiled.querySelector("configurable-actions")).not.toBeNull(); }); }); diff --git a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.ts b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.ts index b33db46c6a..311a96069f 100644 --- a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.ts +++ b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.ts @@ -3,21 +3,16 @@ import { PublishedData } from "@scicatproject/scicat-sdk-ts-angular"; import { Store } from "@ngrx/store"; import { ActivatedRoute, Router } from "@angular/router"; import { - amendPublishedDataAction, deletePublishedDataAction, fetchPublishedDataAction, fetchRelatedDatasetsAndAddToBatchAction, - publishPublishedDataAction, - registerPublishedDataAction, } from "state-management/actions/published-data.actions"; import { Subscription } from "rxjs"; import { pluck } from "rxjs/operators"; import { selectCurrentPublishedData } from "state-management/selectors/published-data.selectors"; import { AppConfigService } from "app-config.service"; -import { - selectIsAdmin, - selectIsLoggedIn, -} from "state-management/selectors/user.selectors"; +import { selectIsAdmin } from "state-management/selectors/user.selectors"; +import { ActionItems } from "shared/modules/configurable-actions/configurable-action.interfaces"; @Component({ selector: "publisheddata-details", @@ -30,12 +25,16 @@ export class PublisheddataDetailsComponent implements OnInit, OnDestroy { isAdmin$ = this.store.select(selectIsAdmin); publishedData: PublishedData & { metadata?: any }; subscriptions: Subscription[] = []; - isLoggedIn$ = this.store.select(selectIsLoggedIn); appConfig = this.appConfigService.getConfig(); show = false; landingPageUrl = ""; doi = ""; + actionItems: ActionItems = { + datasets: [], + publisheddata: [], + }; + constructor( private appConfigService: AppConfigService, private route: ActivatedRoute, @@ -55,6 +54,7 @@ export class PublisheddataDetailsComponent implements OnInit, OnDestroy { this.currentData$.subscribe((data) => { if (data) { this.publishedData = data; + this.actionItems.publisheddata[0] = data; if (this.appConfig.landingPage) { this.landingPageUrl = @@ -69,30 +69,18 @@ export class PublisheddataDetailsComponent implements OnInit, OnDestroy { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } - onRegisterClick(doi: string) { - if ( - confirm( - "Are you sure you want to register this published data? Keep in mind that no further changes can be made after this action.", - ) - ) { - this.store.dispatch(registerPublishedDataAction({ doi })); + onActionFinished(event: { success: boolean }) { + if (event.success) { + this.store.dispatch(fetchPublishedDataAction({ id: this.doi })); } } - onAmendClick(doi: string) { - this.store.dispatch(amendPublishedDataAction({ doi })); - } - onDeleteClick(doi: string) { if (confirm("Are you sure you want to delete this published data?")) { this.store.dispatch(deletePublishedDataAction({ doi })); } } - onPublishClick(doi: string) { - this.store.dispatch(publishPublishedDataAction({ doi })); - } - onEditClick() { const id = encodeURIComponent(this.doi); this.router.navigateByUrl("/publishedDatasets/" + id + "/edit"); 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 0dfb36efae..3c2c71cec5 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -170,6 +170,15 @@ export class ConfigurableActionComponent return _.get(this.actionItems, `instruments[${index}].${field}`); } + const publishedDataFieldMatch = selector.match( + /^#PublishedData\[(\d+)\]Field\[(\w+)\]$/, + ); + if (publishedDataFieldMatch) { + const index = Number(publishedDataFieldMatch[1]); + const field = publishedDataFieldMatch[2]; + return _.get(this.actionItems, `publisheddata[${index}].${field}`); + } + return undefined; } @@ -212,6 +221,10 @@ export class ConfigurableActionComponent .flatMap("files") .filter("selected") .sumBy((f) => Number(f.size || 0)), + "#PublishedData0Doi": () => + _.get(this.actionItems, "publisheddata[0].doi"), + "#PublishedData0Status": () => + _.get(this.actionItems, "publisheddata[0].status"), }; return staticMap; } 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 167486474a..d15a9bd51d 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -1,7 +1,10 @@ import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; -import { Instrument } from "@scicatproject/scicat-sdk-ts-angular"; +import { + DatasetClass, + Instrument, + PublishedData, +} from "@scicatproject/scicat-sdk-ts-angular"; import { DynamicField } from "../dialog/dialog.component"; -import { DatasetClass } from "@scicatproject/scicat-sdk-ts-angular"; export type DialogField = { key: string } & DynamicField; @@ -45,6 +48,7 @@ export type ActionItemDataset = DatasetClass & { export interface ActionItems { datasets: ActionItemDataset[]; + publisheddata?: PublishedData[]; instruments?: Instrument[]; [key: string]: unknown; } diff --git a/src/assets/config.json b/src/assets/config.json index 887725a3c1..337b715bf2 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -300,6 +300,87 @@ "datasetDetailsActions": [], "selectionActionsEnabled": true, "selectionActions": [], + "publishedDataActionsEnabled": true, + "publishedDataActions": [ + { + "id": "3a954df5-6a4b-4159-ad95-4d9717891932", + "description": "Make a published data record private", + "order": 1, + "label": "Private", + "type": "xhr", + "mat_icon": "public_off", + "method": "PATCH", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}", + "payload": "{\"status\": \"private\"}", + "enabled": "@status == 'public'", + "authorization": "#datasetOwner || #userIsAdmin", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData[0]Field[status]" + }, + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "9a67e562-2f08-4637-a8e4-a36cd6cfe6a1", + "description": "Publish published data", + "order": 2, + "label": "Publish", + "type": "xhr", + "mat_icon": "publish", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/publish", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData[0]Field[status]" + }, + "enabled": "@status === 'private'", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "5d6f59b4-11c4-4a52-b21c-92fc5834c2ed", + "description": "Register published data", + "order": 3, + "label": "Register", + "type": "xhr", + "mat_icon": "registration", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/register", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData[0]Field[status]" + }, + "enabled": "@status === 'public'", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "b3a11f37-aed5-466a-87b4-4e66dd488621", + "description": "Amend published data", + "order": 4, + "label": "Amend", + "type": "xhr", + "mat_icon": "edit", + "method": "POST", + "url": "http://localhost:3000/api/v4/publisheddata/{{ @doi }}/amend", + "variables": { + "doi": "#PublishedData0Doi", + "status": "#PublishedData[0]Field[status]" + }, + "enabled": "@status === 'registered'", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + } + ], "labelMaps": { "filters": { "LocationFilter": "Location",