diff --git a/cypress/e2e/proposals/proposals-general.cy.js b/cypress/e2e/proposals/proposals-general.cy.js index f08c167d7..734a0a12c 100644 --- a/cypress/e2e/proposals/proposals-general.cy.js +++ b/cypress/e2e/proposals/proposals-general.cy.js @@ -107,5 +107,65 @@ describe("Proposals general", () => { cy.get('[data-cy="proposal-type"]').contains(newProposalType); }); + + it("proposal should have metadata and if not it should be able to add", () => { + const metadataName = "Proposal Metadata Name"; + const metadataValue = "proposal metadata value"; + const newProposal = { + ...testData.proposal, + proposalId: Math.floor(100000 + Math.random() * 900000).toString(), + }; + cy.createProposal(newProposal); + + cy.visit(`/proposals/${newProposal.proposalId}`); + + cy.finishedLoading(); + + cy.contains(newProposal.title); + + cy.finishedLoading(); + + cy.get('[data-cy="proposal-metadata-card"]').should("exist"); + + cy.get('[data-cy="proposal-metadata-card"] [role="tab"]') + .contains("Edit") + .click(); + + cy.get('[data-cy="add-new-row"]').click(); + + // simulate click event on the drop down + cy.get("mat-select[data-cy=field-type-input]").last().click(); // opens the drop down + + // simulate click event on the drop down item (mat-option) + cy.get("mat-option") + .contains("string") + .then((option) => { + option[0].click(); + }); + + cy.get("[data-cy=metadata-name-input]") + .last() + .type(`${metadataName}{enter}`); + cy.get("[data-cy=metadata-value-input]") + .last() + .type(`${metadataValue}{enter}`); + + cy.get("button[data-cy=save-changes-button]").click(); + + cy.finishedLoading(); + + cy.reload(); + + cy.finishedLoading(); + + cy.contains(newProposal.title); + + cy.get('[data-cy="proposal-metadata-card"]').contains(metadataName, { + matchCase: true, + }); + cy.get('[data-cy="proposal-metadata-card"]').contains(metadataValue, { + matchCase: true, + }); + }); }); }); diff --git a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts index ab782cf6b..de553c72d 100644 --- a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts +++ b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts @@ -97,8 +97,6 @@ describe("DatasetTableActionsComponent", () => { describe("#onModeChange()", () => { it("should dispatch a SetViewModeAction", () => { dispatchSpy = spyOn(store, "dispatch"); - - const event = "test"; const modeToggle = ArchViewMode.all; component.onModeChange(modeToggle); diff --git a/src/app/datasets/dataset-table/dataset-table.component.spec.ts b/src/app/datasets/dataset-table/dataset-table.component.spec.ts index 61baec645..fbf8d9acc 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.spec.ts +++ b/src/app/datasets/dataset-table/dataset-table.component.spec.ts @@ -20,16 +20,10 @@ import { deselectDatasetAction, selectAllDatasetsAction, clearSelectionAction, - changePageAction, sortByColumnAction, } from "state-management/actions/datasets.actions"; -import { PageChangeEvent } from "shared/modules/table/table.component"; import { provideMockStore } from "@ngrx/store/testing"; import { selectDatasets } from "state-management/selectors/datasets.selectors"; -import { - selectColumnAction, - deselectColumnAction, -} from "state-management/actions/user.actions"; import { MatTableModule } from "@angular/material/table"; import { MatCheckboxChange, diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts index f01b8bd66..8aec69bdf 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts @@ -65,7 +65,7 @@ const labelMaps = { PidFilterStartsWith: "PID filter (Starts With)- Not implemented", }; export class MockStoreWithFilters extends MockStore { - public select(selector: any) { + public select(selector) { if (selector === selectFilters) { return of(filterConfigs); } diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts index 3171707b3..d47b81be8 100644 --- a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts @@ -12,10 +12,7 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { removeScientificConditionAction } from "state-management/actions/datasets.actions"; import { of } from "rxjs"; -import { - deselectColumnAction, - deselectAllCustomColumnsAction, -} from "state-management/actions/user.actions"; +import { deselectColumnAction } from "state-management/actions/user.actions"; import { ScientificCondition } from "state-management/models"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; diff --git a/src/app/proposals/proposal-detail/proposal-detail.component.html b/src/app/proposals/proposal-detail/proposal-detail.component.html index 84745dad9..6c07a0962 100644 --- a/src/app/proposals/proposal-detail/proposal-detail.component.html +++ b/src/app/proposals/proposal-detail/proposal-detail.component.html @@ -114,15 +114,71 @@ - - - -
-
- + + +
+ science
+ Metadata +
+ + + +
+ + + +
+
+ + + + list View + + + + + + edit Edit + +
+ + + + +
+
+
+
+ + + +
diff --git a/src/app/proposals/proposal-detail/proposal-detail.component.ts b/src/app/proposals/proposal-detail/proposal-detail.component.ts index 06538ff7f..97e4befb9 100644 --- a/src/app/proposals/proposal-detail/proposal-detail.component.ts +++ b/src/app/proposals/proposal-detail/proposal-detail.component.ts @@ -1,9 +1,25 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; import { Proposal } from "state-management/models"; import { AppConfigService } from "app-config.service"; import { Store } from "@ngrx/store"; -import { selectParentProposal } from "state-management/selectors/proposals.selectors"; +import { + selectCurrentProposal, + selectParentProposal, +} from "state-management/selectors/proposals.selectors"; import { Router } from "@angular/router"; +import { updateProposalPropertyAction } from "state-management/actions/proposals.actions"; +import { + Observable, + Subscription, + combineLatest, + fromEvent, + map, + switchMap, +} from "rxjs"; +import { + selectIsAdmin, + selectProfile, +} from "state-management/selectors/user.selectors"; import { clearProposalsStateAction } from "state-management/actions/proposals.actions"; @Component({ @@ -11,10 +27,18 @@ import { clearProposalsStateAction } from "state-management/actions/proposals.ac templateUrl: "proposal-detail.component.html", styleUrls: ["proposal-detail.component.scss"], }) -export class ProposalDetailComponent { +export class ProposalDetailComponent implements OnInit, OnDestroy { + private subscriptions: Subscription[] = []; + private _hasUnsavedChanges = false; @Input() proposal: Proposal; parentProposal: Proposal | undefined; parentProposal$ = this.store.select(selectParentProposal); + editingAllowed = false; + userProfile$ = this.store.select(selectProfile); + isAdmin$ = this.store.select(selectIsAdmin); + accessGroups$: Observable = this.userProfile$.pipe( + map((profile) => (profile ? profile.accessGroups : [])), + ); appConfig = this.appConfigService.getConfig(); @@ -26,9 +50,65 @@ export class ProposalDetailComponent { private router: Router, ) {} + ngOnInit(): void { + // Prevent user from reloading page if there are unsave changes + this.subscriptions.push( + fromEvent(window, "beforeunload").subscribe((event) => { + if (this.hasUnsavedChanges()) { + event.preventDefault(); + } + }), + ); + + this.subscriptions.push( + this.store + .select(selectCurrentProposal) + .pipe( + switchMap((proposal) => { + this.proposal = proposal; + return combineLatest([this.accessGroups$, this.isAdmin$]).pipe( + map(([groups, isAdmin]) => ({ + proposal, + groups, + isAdmin, + })), + ); + }), + map(({ proposal, groups, isAdmin }) => { + this.editingAllowed = + groups.indexOf(proposal?.ownerGroup) !== -1 || isAdmin; + }), + ) + .subscribe(), + ); + } + + hasUnsavedChanges() { + return this._hasUnsavedChanges; + } + onClickProposal(proposalId: string): void { this.store.dispatch(clearProposalsStateAction()); const id = encodeURIComponent(proposalId); this.router.navigateByUrl("/proposals/" + id); } + + onSaveMetadata(metadata: Record) { + if (this.proposal) { + const { proposalId } = this.proposal; + const property = { metadata }; + + this.store.dispatch( + updateProposalPropertyAction({ proposalId, property }), + ); + } + } + + onHasUnsavedChanges($event: boolean) { + this._hasUnsavedChanges = $event; + } + + ngOnDestroy() { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + } } diff --git a/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.html b/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.html index 675583d9c..983cd16ac 100644 --- a/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.html +++ b/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.html @@ -4,18 +4,16 @@ - Name - - {{ metadata.name | replaceUnderscore | titlecase }} - + Name + {{ + metadata.name | replaceUnderscore | titlecase + }} - Value - - {{ metadata.value }} - + Value + {{ metadata.value }} diff --git a/src/app/state-management/actions/proposals.actions.ts b/src/app/state-management/actions/proposals.actions.ts index acfff1ddb..f03ae0f07 100644 --- a/src/app/state-management/actions/proposals.actions.ts +++ b/src/app/state-management/actions/proposals.actions.ts @@ -101,6 +101,18 @@ export const updateAttachmentCaptionFailedAction = createAction( "[Proposal] Update Attachment Caption Failed", ); +export const updateProposalPropertyAction = createAction( + "[Proposal] Update Proposal Property", + // TODO: Most probably with the new sdk the property should be of type UpdateProposalDto or something similar + props<{ proposalId: string; property: Record }>(), +); +export const updateProposalPropertyCompleteAction = createAction( + "[Proposal] Update Proposal Property Complete", +); +export const updateProposalPropertyFailedAction = createAction( + "[Proposal] Update Proposal Property Failed", +); + export const removeAttachmentAction = createAction( "[Proposal] Remove Attachment", props<{ proposalId: string; attachmentId: string }>(), diff --git a/src/app/state-management/effects/proposals.effects.ts b/src/app/state-management/effects/proposals.effects.ts index f134b8410..55fa5aeb9 100644 --- a/src/app/state-management/effects/proposals.effects.ts +++ b/src/app/state-management/effects/proposals.effects.ts @@ -157,6 +157,25 @@ export class ProposalEffects { ); }); + updateProposalProperty$ = createEffect(() => { + return this.actions$.pipe( + ofType(fromActions.updateProposalPropertyAction), + switchMap(({ proposalId, property }) => + this.proposalApi + .patchAttributes(encodeURIComponent(proposalId), property) + .pipe( + switchMap(() => [ + fromActions.updateProposalPropertyCompleteAction(), + fromActions.fetchProposalAction({ proposalId }), + ]), + catchError(() => + of(fromActions.updateProposalPropertyFailedAction()), + ), + ), + ), + ); + }); + removeAttachment$ = createEffect(() => { return this.actions$.pipe( ofType(fromActions.removeAttachmentAction), @@ -180,12 +199,14 @@ export class ProposalEffects { return this.actions$.pipe( ofType( fromActions.fetchProposalsAction, + fromActions.fetchParentProposalAction, fromActions.fetchCountAction, fromActions.fetchProposalAction, fromActions.fetchProposalDatasetsAction, fromActions.fetchProposalDatasetsCountAction, fromActions.addAttachmentAction, fromActions.updateAttachmentCaptionAction, + fromActions.updateProposalPropertyAction, fromActions.removeAttachmentAction, ), switchMap(() => of(loadingAction())), @@ -197,6 +218,8 @@ export class ProposalEffects { ofType( fromActions.fetchProposalsCompleteAction, fromActions.fetchProposalsFailedAction, + fromActions.fetchParentProposalCompleteAction, + fromActions.fetchParentProposalFailedAction, fromActions.fetchCountCompleteAction, fromActions.fetchCountFailedAction, fromActions.fetchProposalCompleteAction, @@ -209,6 +232,8 @@ export class ProposalEffects { fromActions.addAttachmentFailedAction, fromActions.updateAttachmentCaptionCompleteAction, fromActions.updateAttachmentCaptionFailedAction, + fromActions.updateProposalPropertyCompleteAction, + fromActions.updateProposalPropertyFailedAction, fromActions.removeAttachmentCompleteAction, fromActions.removeAttachmentFailedAction, ),