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 {