diff --git a/oar-dmp/src/app/dmp-form/dmp-form.component.ts b/oar-dmp/src/app/dmp-form/dmp-form.component.ts index c3630a5..9966dce 100644 --- a/oar-dmp/src/app/dmp-form/dmp-form.component.ts +++ b/oar-dmp/src/app/dmp-form/dmp-form.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ViewChild, afterNextRender } from '@angular/core'; -import { ObservedValueOf, Subscription } from "rxjs"; +import { ObservedValueOf, Subscription, forkJoin, switchMap, of, EMPTY } from "rxjs"; import { UntypedFormBuilder } from '@angular/forms'; import { BasicInfoComponent } from '../form-components/basic-info/basic-info.component'; import { PersonelComponent } from '../form-components/personel/personel.component'; @@ -127,6 +127,10 @@ export class DmpFormComponent implements OnInit{ OUsUpdate: UpdateIndicator = {numUpdates:0, isUpdated:false}; OUsTotalUpdates:number = 0; + canWrite:boolean = false; + isAdmin:boolean = false; + canDelete:boolean = false; + constructor( private fb: UntypedFormBuilder, private dmp_Service: DmpService, @@ -138,7 +142,7 @@ export class DmpFormComponent implements OnInit{ private updateOUs: UpdateNistContributorService ) { - // console.log("constructor"); + console.log("constructor"); afterNextRender(() => { // used for one-time initialisation: // subscribe to track if the form has been changed by performing @@ -185,7 +189,7 @@ export class DmpFormComponent implements OnInit{ getFromDB:boolean = false; ngOnInit(): void { - // console.log("dmp-form.component ngOnInit") + console.log("dmp-form.component ngOnInit") // const elementToObserve = document.getElementById("footer"); @@ -204,7 +208,8 @@ export class DmpFormComponent implements OnInit{ this.formButtonSubscribe(); this.formExportFormatSubscribe(); - this.id = this.route.snapshot.paramMap.get('id') + this.id = this.route.snapshot.paramMap.get('id'); + this.route.data.subscribe(data => { this.action = data["action"] ; if (this.action === "edit"){ @@ -217,24 +222,47 @@ export class DmpFormComponent implements OnInit{ } }); - // Fetch initial data from the backend - this.dmp_Service.fetchDMP(this.action, this.id).subscribe( + // 1. Start with the permission check + this.dmp_Service.aclsPermission(this.id, 'read').pipe( + switchMap(hasReadAccess => { + if (hasReadAccess) { + // 2. If true, move to the next "link" in the chain: fetch everything else + return forkJoin({ + dmpData: this.dmp_Service.fetchDMP(this.action, this.id), // Fetch initial data from the backend + writePerm: this.dmp_Service.aclsPermission(this.id, 'write'), + adminPerm: this.dmp_Service.aclsPermission(this.id, 'admin'), + deletePerm: this.dmp_Service.aclsPermission(this.id, 'delete'), + }); + } + else { + // 3. If false, stop the chain and handle the lack of access + this.router.navigate(['error', { dmpError: "You do not have read privileges for this record." }]); + return EMPTY; // Effectively kills the stream so 'next' isn't called + } + }) + ).subscribe( { - next: data => { + next: (result) => { + const { dmpData, writePerm, adminPerm, deletePerm } = result; if (this.id !==null){ // fetch DMP data from the backend - this.initialDMP = data.data; - this.dmp = data.data; - this.name.setValue(data.name); + this.initialDMP = dmpData.data; + this.dmp = dmpData.data; + this.name.setValue(dmpData.name); this.getFromDB = true; } else{ // empty new form for creating new DMP - this.initialDMP = data; - this.dmp = data; + this.initialDMP = dmpData; + this.dmp = dmpData; // disable save button by default until user has made a change on the form this.disableSaveButton(); } + + // Set permission flags + this.canWrite = writePerm; + this.isAdmin = adminPerm; + this.canDelete = deletePerm }, error: error => { console.log(error.message); @@ -260,7 +288,13 @@ export class DmpFormComponent implements OnInit{ this.resetDmp(); } else if (this.formButtonMessage === "Save"){ - this.saveDraft(); + if (this.canWrite){ + this.saveDraft(); + } + else{ + alert("Can not save changes to DMP because you don't have write privileges on this record."); + } + } else if (this.formButtonMessage === "Download"){ if (this.dmpExportFormatType === ""){ @@ -290,14 +324,14 @@ export class DmpFormComponent implements OnInit{ if (!this.formContributorsSubscription) { //subscribe if not already subscribed this.formContributorsSubscription = this.updateContributor.updateNISTContrib$.subscribe({ - next: (hasChanged:UpdateIndicator) => { + next: (hasChanged:UpdateIndicator) => { if (hasChanged.isUpdated){ // change this flag to indicate that we need to display alert about auto update of NIST contributors metadata from people service this.contributorsUpdate.isUpdated = hasChanged.isUpdated; // update this counter to indicate how many contributors have been updated this.contribTotalUpdates = hasChanged.numUpdates; this.saveDraft(); - } + } } }); } @@ -309,7 +343,7 @@ export class DmpFormComponent implements OnInit{ if (!this.formOUsSubscription) { //subscribe if not already subscribed this.formOUsSubscription = this.updateOUs.updateOUs$.subscribe({ - next: (hasChanged:UpdateIndicator) => { + next: (hasChanged:UpdateIndicator) => { if (hasChanged.isUpdated){ // change this flag to indicate that we need to display alert about auto update of NIST contributors metadata from people service this.OUsUpdate.isUpdated = hasChanged.isUpdated; @@ -406,8 +440,10 @@ export class DmpFormComponent implements OnInit{ // arr1 = [...arr1, ...arr2]; // arr1 is now [0, 1, 2, 3, 4, 5] this.dmp = { ...this.dmp, ...patch }; - this.contributorsSubscribe(); - this.OUsSubscribe(); + if (this.canWrite){ + this.contributorsSubscribe(); + this.OUsSubscribe(); + } } enableSaveButton(){ diff --git a/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.html b/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.html index 1741e88..e552799 100644 --- a/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.html +++ b/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.html @@ -37,9 +37,12 @@ } diff --git a/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.ts b/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.ts index 37870e9..e50ef25 100644 --- a/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.ts +++ b/oar-dmp/src/app/form-components/data-preservation/data-preservation.component.ts @@ -1,8 +1,8 @@ -import { Component, Input, Output, ChangeDetectionStrategy, inject, signal } from '@angular/core'; +import { Component, Input, Output, ChangeDetectionStrategy, signal, ViewChild, ElementRef } from '@angular/core'; import { UntypedFormBuilder } from '@angular/forms'; import { defer, map, of, startWith } from 'rxjs'; -import { MatChipInputEvent } from '@angular/material/chips'; +import { MatChipInputEvent, MatChipInput } from '@angular/material/chips'; import { ChipsSplitterService } from 'src/app/shared/chips-splitter.service'; @@ -18,6 +18,12 @@ export class DataPreservationComponent { separatorExp: RegExp = /,|;/; reactivePathsURLs = signal(['']); + pathsInputVal = ''; + // Reference the HTML input element that uses chips matching the #pathInput in the HTML + @ViewChild('pathInput') chipInputEl!: ElementRef; + + // This finds the MatChipInput directive inside that same element + @ViewChild(MatChipInput) chipInputDirective!: MatChipInput; preservationForm = this.fb.group( { @@ -145,11 +151,18 @@ export class DataPreservationComponent { } addReactivePathsURLs(event: MatChipInputEvent): void { - const chips = (this.spChips.splitChips(event.value.trim()) || '') + // To clean up chips array and ensure no empty strings or "just whitespace" items make it through, + // we should make fall back to an empty array [] and use the JavaScript .filter() method. + const chips = (this.spChips.splitChips(event.value.trim()) || []) + .filter(chip => chip.trim().length > 0); - // Add our keyword + // Add our path if (chips) { - this.reactivePathsURLs.update(pathsURLs => [...pathsURLs, ...chips]); + this.reactivePathsURLs.update(pathsURLs => { + // Combine both arrays into a Set to force uniqueness, + // then spread it back into a standard array. + return [...new Set([...pathsURLs, ...chips])]; + }); chips.forEach((chip)=>{ this.preservationForm.patchValue({ pathsURLs: chip @@ -162,4 +175,34 @@ export class DataPreservationComponent { event.chipInput!.clear(); } + onBlur(event: FocusEvent) { + // this is called if user did not hit enter on keyboard to add chips but has rather pressed + // elsewhere with a mouse + + // Trigger event if input is not empty + if (this.pathsInputVal !== ''){ + this.triggerAddChip(); + } + } + + onInputChange(value: string){ + // console.log('onInputChange', value); + this.pathsInputVal = value; + + } + + triggerAddChip() { + //Construct the mock event + const mockEvent: MatChipInputEvent = { + input: this.chipInputEl.nativeElement, + value: this.pathsInputVal.trim(), + chipInput: this.chipInputDirective + }; + + // Manually call your existing add function + this.addReactivePathsURLs(mockEvent); + // Clear input value + this.pathsInputVal = ''; + } + } diff --git a/oar-dmp/src/app/form-components/keywords/keywords.component.html b/oar-dmp/src/app/form-components/keywords/keywords.component.html index 06d747c..30e4d04 100644 --- a/oar-dmp/src/app/form-components/keywords/keywords.component.html +++ b/oar-dmp/src/app/form-components/keywords/keywords.component.html @@ -18,9 +18,13 @@ } diff --git a/oar-dmp/src/app/form-components/keywords/keywords.component.ts b/oar-dmp/src/app/form-components/keywords/keywords.component.ts index f2f2d10..f92a359 100644 --- a/oar-dmp/src/app/form-components/keywords/keywords.component.ts +++ b/oar-dmp/src/app/form-components/keywords/keywords.component.ts @@ -1,6 +1,6 @@ -import { Component, Input, Output } from '@angular/core'; +import { Component, Input, Output, ViewChild, ElementRef } from '@angular/core'; import { UntypedFormBuilder } from '@angular/forms'; -import { MatChipInputEvent } from '@angular/material/chips'; +import { MatChipInputEvent, MatChipInput} from '@angular/material/chips'; import { defer, map, of, startWith } from 'rxjs'; import { DMP_Meta } from '../../types/DMP.types'; import { ChipsSplitterService } from 'src/app/shared/chips-splitter.service'; @@ -21,7 +21,13 @@ export class KeywordsComponent { } ); - reactiveKeywords = signal(['']); + reactiveKeywords = signal(['']); + keywordsInputVal = ''; + // Reference the HTML input element that uses chips matching the #chipInput in the HTML + @ViewChild('chipInput') chipInputEl!: ElementRef; + + // This finds the MatChipInput directive inside that same element + @ViewChild(MatChipInput) chipInputDirective!: MatChipInput; constructor(private fb: UntypedFormBuilder, private spChips: ChipsSplitterService) { // console.log("Keywords Component"); @@ -114,11 +120,18 @@ export class KeywordsComponent { } addReactiveKeyword(event: MatChipInputEvent): void { - const chips = (this.spChips.splitChips(event.value.trim()) || '') + // To clean up chips array and ensure no empty strings or "just whitespace" items make it through, + // we should make fall back to an empty array [] and use the JavaScript .filter() method. + const chips = (this.spChips.splitChips(event.value.trim()) || []) + .filter(chip => chip.trim().length > 0); // Add our keyword if (chips) { - this.reactiveKeywords.update(keywords => [...keywords, ...chips]); + this.reactiveKeywords.update(keywords => { + // Combine both arrays into a Set to force uniqueness, + // then spread it back into a standard array. + return [...new Set([...keywords, ...chips])]; + }); chips.forEach((chip)=>{ this.keyWordsForm.patchValue({ keywords: chip @@ -130,6 +143,36 @@ export class KeywordsComponent { event.chipInput!.clear(); } + onBlur(event: FocusEvent) { + // this is called if user did not hit enter on keyboard to add chips but has rather pressed + // elsewhere with a mouse + + // Trigger event if input is not empty + if (this.keywordsInputVal !== ''){ + this.triggerAddChip(); + } + } + + onInputChange(value: string){ + // console.log('onInputChange', value); + this.keywordsInputVal = value; + + } + + triggerAddChip() { + //Construct the mock event + const mockEvent: MatChipInputEvent = { + input: this.chipInputEl.nativeElement, + value: this.keywordsInputVal.trim(), + chipInput: this.chipInputDirective + }; + + // Manually call your existing add function + this.addReactiveKeyword(mockEvent); + // Clear input value + this.keywordsInputVal = ''; + } + } diff --git a/oar-dmp/src/app/form-components/personel/personel.component.ts b/oar-dmp/src/app/form-components/personel/personel.component.ts index 998e13a..10a37f8 100644 --- a/oar-dmp/src/app/form-components/personel/personel.component.ts +++ b/oar-dmp/src/app/form-components/personel/personel.component.ts @@ -346,7 +346,7 @@ export class PersonelComponent implements OnInit { this.contribOrcidWarn = ''; personel.contributors.forEach( (dmpContributor, index) => { - if (dmpContributor.orcid.length === 0){ + if (!dmpContributor.orcid){ this.contribOrcidWarn = PersonelComponent.ORCID_WARNING; } this.dmpContributors.push({ @@ -492,9 +492,10 @@ export class PersonelComponent implements OnInit { }, complete: () => { if (this.NISTPersonMetaChanged){ - console.info(`Metadata for ${dmpContributor.firstName} ${dmpContributor.lastName} has been been updated to reflect most recent info found in the NIST people service database.`) + console.info(`Metadata for ${dmpContributor.firstName} ${dmpContributor.lastName} does not match most recent info found in the NIST people service database.`) // add changes to the form values if any changes were made to NIST contributors metadata - this.personelForm.value['contributors'] = this.dmpContributors; + this.RePopulateTable(); + // this.personelForm.value['contributors'] = this.dmpContributors; // patch value to indicate that the form has changed this.personelForm.patchValue({ @@ -834,49 +835,55 @@ export class PersonelComponent implements OnInit { this.resetContributorFields(); this.resetWarningAndErrorMessages(); } + + RePopulateTable(){ + this.dmpContributors = this.dmpContributors.filter((u: any) => !u.isSelected); + this.resetTable(); + this.contribOrcidWarn = ""; + this.dmpContributors.forEach((element)=>{ + if (element.orcid.length == 0){ + this.contribOrcidWarn = PersonelComponent.ORCID_WARNING; + } + // re populate contributors array + this.personelForm.value['contributors'].push({ + + firstName:element.firstName, + lastName:element.lastName, + orcid: element.orcid, + emailAddress: element.emailAddress, + + groupOrgID:element.groupOrgID, + groupNumber:element.groupNumber, + groupName:element.groupName, + + divisionOrgID:element.divisionOrgID, + divisionNumber:element.divisionNumber, + divisionName:element.divisionName, + + ouOrgID:element.ouOrgID, + ouNumber:element.ouNumber, + ouName:element.ouName, + + primary_contact: element.primary_contact, + institution: element.institution, + role: element.role + }); + }); + if (this.dmpContributors.length === 0){ + // If the table is empty disable clear and remove buttons + this.disableClear=true; + this.disableRemove=true; + } + + } removeSelectedRows() { const result = confirmDialog("Are you sure you want to delete selected contributor(s) for this DMP?"); if (result) { - this.dmpContributors = this.dmpContributors.filter((u: any) => !u.isSelected); - this.resetTable(); - this.contribOrcidWarn = ""; - this.dmpContributors.forEach((element)=>{ - if (element.orcid.length == 0){ - this.contribOrcidWarn = PersonelComponent.ORCID_WARNING; - } - // re populate contributors array - this.personelForm.value['contributors'].push({ - - firstName:element.firstName, - lastName:element.lastName, - orcid: element.orcid, - emailAddress: element.emailAddress, - - groupOrgID:element.groupOrgID, - groupNumber:element.groupNumber, - groupName:element.groupName, - - divisionOrgID:element.divisionOrgID, - divisionNumber:element.divisionNumber, - divisionName:element.divisionName, - - ouOrgID:element.ouOrgID, - ouNumber:element.ouNumber, - ouName:element.ouName, - - primary_contact: element.primary_contact, - institution: element.institution, - role: element.role - }); - }); - if (this.dmpContributors.length === 0){ - // If the table is empty disable clear and remove buttons - this.disableClear=true; - this.disableRemove=true; - } + this.RePopulateTable(); + } } diff --git a/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.html b/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.html index b082ee4..7084fd6 100644 --- a/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.html +++ b/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.html @@ -118,9 +118,12 @@ } diff --git a/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.ts b/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.ts index eb4c199..a27ddb8 100644 --- a/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.ts +++ b/oar-dmp/src/app/form-components/technical-requirements/technical-requirements.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, Output, ChangeDetectionStrategy, inject, signal } from '@angular/core'; +import { Component, Input, Output, ChangeDetectionStrategy, signal, ViewChild, ElementRef } from '@angular/core'; import { confirmDialog } from 'src/app/shared/dmp.service'; import { DropDownSelectService } from '../../shared/drop-down-select.service'; //resources service to talk between two components @@ -12,7 +12,7 @@ import { DMP_Meta } from '../../types/DMP.types'; import { SoftwareDevelopment } from '../../types/software-development.type'; import { Subscription } from 'rxjs'; -import { MatChipInputEvent } from '@angular/material/chips'; +import { MatChipInputEvent, MatChipInput } from '@angular/material/chips'; import { ChipsSplitterService } from 'src/app/shared/chips-splitter.service'; interface InstrTblRow { @@ -81,6 +81,12 @@ export class StorageNeedsComponent { separatorExp: RegExp = /,|;/; reactiveInstruments = signal(['']); + instrumentsInputVal = ''; + // Reference the HTML input element that uses chips matching the #equipmentChips in the HTML + @ViewChild('equipmentChips') chipInputEl!: ElementRef; + + // This finds the MatChipInput directive inside that same element + @ViewChild(MatChipInput) chipInputDirective!: MatChipInput; // This mimics the technical-requirements type interface from // types/technical-requirements.type.ts @@ -654,11 +660,18 @@ export class StorageNeedsComponent { } addReactiveInstruments(event: MatChipInputEvent): void { - const chips = (this.spChips.splitChips(event.value.trim()) || '') + // To clean up chips array and ensure no empty strings or "just whitespace" items make it through, + // we should make fall back to an empty array [] and use the JavaScript .filter() method. + const chips = (this.spChips.splitChips(event.value.trim()) || []) + .filter(chip => chip.trim().length > 0); - // Add our keyword + // Add our instrument if (chips) { - this.reactiveInstruments.update(technicalResources => [...technicalResources, ...chips]); + this.reactiveInstruments.update(technicalResources => { + // Combine both arrays into a Set to force uniqueness, + // then spread it back into a standard array. + return [...new Set([...technicalResources, ...chips])]; + }); chips.forEach((chip)=>{ this.technicalRequirementsForm.patchValue({ technicalResources: chip @@ -671,6 +684,36 @@ export class StorageNeedsComponent { event.chipInput!.clear(); } + onBlur(event: FocusEvent) { + // this is called if user did not hit enter on keyboard to add chips but has rather pressed + // elsewhere with a mouse + + // Trigger event if input is not empty + if (this.instrumentsInputVal !== ''){ + this.triggerAddChip(); + } + } + + onInputChange(value: string){ + // console.log('onInputChange', value); + this.instrumentsInputVal = value; + + } + + triggerAddChip() { + //Construct the mock event + const mockEvent: MatChipInputEvent = { + input: this.chipInputEl.nativeElement, + value: this.instrumentsInputVal.trim(), + chipInput: this.chipInputDirective + }; + + // Manually call your existing add function + this.addReactiveInstruments(mockEvent); + // Clear input value + this.instrumentsInputVal = ''; + } + } diff --git a/oar-dmp/src/app/shared/dmp.service.ts b/oar-dmp/src/app/shared/dmp.service.ts index 83a71d9..b20be41 100644 --- a/oar-dmp/src/app/shared/dmp.service.ts +++ b/oar-dmp/src/app/shared/dmp.service.ts @@ -162,6 +162,23 @@ export class DmpService { } + aclsPermission(recordID:string|null, permissionType:string) { + /** + * get DMP write permissions from API + */ + let apiAddress:string = this.configService.getConfig().PDRDMP; //this.PDR_API; + if (recordID !==null){ + apiAddress += "/" + recordID +"/acls/" + permissionType + "/:user"; + } + return this.authService.getCredentials().pipe( + switchMap(creds => { + if (! creds) + return throwError(() => new Error('Authentication Failed')); + return this.http.get(apiAddress, this.getHttpOptions(creds)) + }) + ); + } + } export function confirmDialog(message: string): boolean {