Repository: DataBiosphere/duos-ui
Pull request #2887: [DT-1687]Progress Report Validation and other small fixes
Author: login: rjohanek
Created: 2025-05-28T13:58:17Z | Merged: 2025-05-30T18:48:19Z

### Addresses
https://broadworkbench.atlassian.net/browse/DT-1687 (Add form Validation for all components)
https://broadworkbench.atlassian.net/browse/DT-1688 (Move validation from on submit to on form update)

### Summary
- Moves validation from the submit button to on form update in the progress report application
- Disable submit button if form is not valid
- Adds validation to all components used in the Progress Report Application including
         - SummarySection, DataManagementIncident, DarCloseout, CollaboratorAddEdit, PublicationsAddEdit
- Adds missing fields (NIH ID and approverStatus) to CollaboratorAddEdit
- Makes a shared component for ApproverStatus
- Makes titles the same size, updates numbering, makes it so the titles turn red when their section is invalid
- Updates validation function to check for all required fields

## Video 
https://drive.google.com/file/d/1BlBn1FQnKVrDoKactxPIVIPWyUoalhCZ/view?usp=sharing


----
Have you read [Terra's Contributing Guide](https://github.com/DataBiosphere/terra-ui/wiki/Contributor-Guide) lately? If not, do that first.

- Label PR with a Jira ticket number and include a link to the ticket
- Label PR with a security risk modifier [no, low, medium, high]
- PR describes scope of changes
- Get a minimum of one thumbs worth of review, preferably two if enough team members are available
- Get PO sign-off for all non-trivial UI or workflow changes
- Verify all tests go green
- Test this change deployed correctly and works on dev environment after deployment



Diff:
diff --git a/cypress/component/CollaboratorList/collaborator_add_edit.spec.tsx b/cypress/component/CollaboratorList/collaborator_add_edit.spec.tsx
index 042a87a94c..cc9778ee96 100644
--- a/cypress/component/CollaboratorList/collaborator_add_edit.spec.tsx
+++ b/cypress/component/CollaboratorList/collaborator_add_edit.spec.tsx
@@ -68,8 +68,12 @@ describe('CollaboratorAddEdit - Component Tests', () => {
 
     cy.get('#name').clear();
     cy.get('#name').type('Jane Doe');
+    cy.get('#eraCommonsId').clear();
+    cy.get('#eraCommonsId').type('janedoe456');
     cy.get('#title').clear();
     cy.get('#title').type('Senior Researcher');
+    cy.get('#email').clear();
+    cy.get('#email').type('janedoe@example.com')
 
     cy.contains('Save').click();
 
@@ -87,6 +91,7 @@ describe('CollaboratorAddEdit - Component Tests', () => {
     />);
 
     cy.get('#name').type('Test Name');
+    cy.get('#eraCommonsId').type('example123');
     cy.get('#title').type('Test Title');
     cy.get('#email').type('test@example.com');
 
diff --git a/cypress/component/CollaboratorList/collaborator_list.spec.tsx b/cypress/component/CollaboratorList/collaborator_list.spec.tsx
index b64d3b583a..46818ef2c6 100644
--- a/cypress/component/CollaboratorList/collaborator_list.spec.tsx
+++ b/cypress/component/CollaboratorList/collaborator_list.spec.tsx
@@ -101,6 +101,8 @@ describe('CollaboratorList - Component Tests', () => {
 
         cy.get('#name').type('New Person');
         cy.get('#name').blur();
+        cy.get('#eraCommonsId').type('newperson123');
+        cy.get('#eraCommonsId').blur();
         cy.get('#title').type('New Title');
         cy.get('#title').blur();
         cy.get('#email').type('new.person@example.com');
diff --git a/cypress/component/ProgressReports/SubmitProgressReport.spec.tsx b/cypress/component/ProgressReports/SubmitProgressReport.spec.tsx
index b657ca58d8..21dc3769a0 100644
--- a/cypress/component/ProgressReports/SubmitProgressReport.spec.tsx
+++ b/cypress/component/ProgressReports/SubmitProgressReport.spec.tsx
@@ -22,9 +22,6 @@ describe('SubmitProgressReport tests', () => {
             }}
             onCancel={() => {
             }}
-            validateForm={() => {
-              return {}
-            }}
         />
     );
     cy.get('[data-cy=pr-submit-button]').should('exist');
@@ -43,9 +40,6 @@ describe('SubmitProgressReport tests', () => {
             }}
             onCancel={() => {
             }}
-            validateForm={() => {
-              return {}
-            }}
             formState={{}}/>
     );
     cy.get('[data-cy=pr-submit-button]').click();
@@ -65,12 +59,6 @@ describe('SubmitProgressReport tests', () => {
       statusCode: 200,
       body: {},
     });
-    const validationSpy = {
-        validateForm: () => {
-            return {}
-        }
-    }
-    cy.spy(validationSpy, 'validateForm').as('validateForm');
 
     mount(
         <SubmitProgressReport
@@ -79,43 +67,13 @@ describe('SubmitProgressReport tests', () => {
             onSuccess={functionSpy.successHandler}
             onCancel={() => {
             }}
-            validateForm={validationSpy.validateForm}
         />
     );
     cy.get('[data-cy=pr-submit-button]').should('exist');
     cy.get('[data-cy=pr-submit-button]').click();
-    cy.get('@validateForm').should('have.been.calledOnce');
     cy.get('@successHandler').should('have.been.calledOnce');
   });
 
-  it('API and On Submit handler should not be called if validation fails', () => {
-      const functionSpy = {
-          successHandler: () => {
-              console.log('successHandler')
-          }
-      }
-      cy.spy(functionSpy, 'successHandler').as('successHandler');
-      const submitSpy = cy.spy().as('submitSpy');
-      cy.intercept('POST', '/api/dar/v2/progress_report/1', () => submitSpy());
-
-      mount(
-          <SubmitProgressReport
-              formState={{}}
-              parentReferenceId="1"
-              onSuccess={functionSpy.successHandler}
-              onCancel={() => {
-              }}
-              validateForm={() => {
-                  return {darErrors: {datasetIds: {valid: false, failed: ['required']}}}
-              }}
-            />
-      );
-      cy.get('[data-cy=pr-submit-button]').should('exist');
-      cy.get('[data-cy=pr-submit-button]').click();
-      cy.get('@submitSpy').should('not.always.have.been.called')
-      cy.get('@successHandler').should('not.have.been.called');
-  });
-
   it('On Cancel handler should be called after cancel button clicked', () => {
     const functionSpy = {
       cancelHandler: () => {
@@ -130,9 +88,6 @@ describe('SubmitProgressReport tests', () => {
             onSuccess={() => {
             }}
             onCancel={functionSpy.cancelHandler}
-            validateForm={() => {
-              return {}
-            }}
         />
     );
     cy.get('[data-cy=pr-cancel-button]').should('exist');
@@ -154,9 +109,6 @@ describe('SubmitProgressReport tests', () => {
             }}
             onCancel={() => {
             }}
-            validateForm={() => {
-              return {}
-            }}
         />
     );
     cy.get('[data-cy=pr-submit-button]').click();
diff --git a/cypress/component/ProgressReports/collaborator_changes.spec.tsx b/cypress/component/ProgressReports/collaborator_changes.spec.tsx
index 1ca5028e3c..624cf58be4 100644
--- a/cypress/component/ProgressReports/collaborator_changes.spec.tsx
+++ b/cypress/component/ProgressReports/collaborator_changes.spec.tsx
@@ -5,18 +5,18 @@ import { Collaborator } from 'src/components/collaborator_list/Collaborator';
 
 describe('Collaborator Changes - Component Tests', () => {
   let onFormChangeSpy: () => void;
-  
+
   const initialCollaborators: Collaborator[] = [
-    { 
-      name: 'Test User 1', 
+    {
+      name: 'Test User 1',
       title: 'Researcher',
       uuid: '1',
       eraCommonsId: 'user1',
       email: 'user1@example.com',
       institution: 'Test Institution'
     },
-    { 
-      name: 'Test User 2', 
+    {
+      name: 'Test User 2',
       title: 'Assistant',
       uuid: '2',
       eraCommonsId: 'user2',
@@ -24,19 +24,19 @@ describe('Collaborator Changes - Component Tests', () => {
       institution: 'Test Institution'
     }
   ];
-  
+
   const mountComponent = (customState = {}) => {
     const formState = { ...customState };
-    
+
     const props = {
       readOnly: false,
       formState,
       onFormChange: onFormChangeSpy
     };
-    
+
     return mount(<CollaboratorChanges {...props} />);
   };
-  
+
   beforeEach(() => {
     onFormChangeSpy = cy.stub().as('formChangeStub');
     mountComponent();
@@ -58,7 +58,7 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('displays preloaded internal lab staff', () => {
     mountComponent({ labCollaborators: initialCollaborators });
-    
+
     cy.contains('Test User 1').should('be.visible');
     cy.contains('Test User 2').should('be.visible');
     cy.contains('Researcher').should('be.visible');
@@ -67,14 +67,14 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('displays preloaded internal collaborators', () => {
     mountComponent({ internalCollaborators: initialCollaborators });
-    
+
     cy.contains('Test User 1').should('be.visible');
     cy.contains('Test User 2').should('be.visible');
   });
 
   it('displays preloaded external collaborators', () => {
     mountComponent({ externalCollaborators: initialCollaborators });
-    
+
     cy.contains('Test User 1').should('be.visible');
     cy.contains('Test User 2').should('be.visible');
   });
@@ -85,7 +85,7 @@ describe('Collaborator Changes - Component Tests', () => {
       formState: { labCollaborators: initialCollaborators },
       onFormChange: onFormChangeSpy
     };
-    
+
     mount(<CollaboratorChanges {...props} />);
 
     cy.contains('Test User 1').should('be.visible');
@@ -98,7 +98,7 @@ describe('Collaborator Changes - Component Tests', () => {
       internalCollaborators: [],
       externalCollaborators: []
     });
-    
+
     cy.contains('Internal Lab Staff').should('exist');
     cy.contains('Internal Collaborators').should('exist');
     cy.contains('External Collaborators').should('exist');
@@ -106,13 +106,13 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('displays correct description text for each collaborator type', () => {
     mountComponent();
-    
+
     cy.contains('Please add Internal Lab Staff here').should('be.visible');
     cy.contains('Please add Internal Collaborators here').should('be.visible');
     cy.contains('Please list External collaborators here').should('be.visible');
 
     cy.contains('Internal Lab Staff are defined as users of data from this Data Access Request').should('be.visible');
     cy.contains('Internal Collaborators are defined as individuals who are not under the direct supervision of the PI').should('be.visible');
-    cy.contains('External Collaboratos are not employees of the Requesting PI').should('be.visible');
+    cy.contains('External Collaborators are not employees of the Requesting PI').should('be.visible');
   });
 });
\ No newline at end of file
diff --git a/cypress/component/ProgressReports/dar_closeout.spec.tsx b/cypress/component/ProgressReports/dar_closeout.spec.tsx
index ebade620e4..184f75d373 100644
--- a/cypress/component/ProgressReports/dar_closeout.spec.tsx
+++ b/cypress/component/ProgressReports/dar_closeout.spec.tsx
@@ -35,7 +35,7 @@ describe('DAR Closeout - Component Tests', () => {
     cy.get('@formChangeStub').should('have.been.calledWith', { closeoutYesNo: true });
 
     cy.get('#closeoutYesNo').parent().contains('label', 'No').find('input[type="radio"]').click({force: true});
-    cy.get('@formChangeStub').should('have.been.calledWith', { closeoutYesNo: false });
+    cy.get('@formChangeStub').should('have.been.calledWith', { closeoutYesNo: false, closeoutSupplement: undefined});
   });
 
   it('shows closeout options when "Yes" is selected', () => {
diff --git a/cypress/component/ProgressReports/summary_section.spec.tsx b/cypress/component/ProgressReports/summary_section.spec.tsx
index 352142692d..14bc9352fc 100644
--- a/cypress/component/ProgressReports/summary_section.spec.tsx
+++ b/cypress/component/ProgressReports/summary_section.spec.tsx
@@ -71,6 +71,10 @@ describe('Summary Section - Component Tests', () => {
       onFormChange: onFormChangeSpy,
       location,
       researcher,
+      onValidationChange: cy.stub(),
+      validation: {},
+      nihValid: true,
+      onNihStatusUpdate: cy.stub(),
     };
 
     return mount(<SummarySection {...props} />);
@@ -84,10 +88,11 @@ describe('Summary Section - Component Tests', () => {
   it('renders the component correctly', () => {
     cy.get('[data-cy=summary-section]').should('exist');
     cy.contains('Step 1: Submit a Progress Report').should('be.visible');
-    cy.contains('1.1 Summary of Progress').should('be.visible');
-    cy.contains('1.2 Intellectual Property').should('be.visible');
-    cy.contains('1.3 Publications').should('be.visible');
-    cy.contains('1.4 Presentations').should('be.visible');
+    cy.contains('1.1 Researcher Identification').should('be.visible');
+    cy.contains('1.2 Summary of Progress').should('be.visible');
+    cy.contains('1.3 Intellectual Property').should('be.visible');
+    cy.contains('1.4 Publications').should('be.visible');
+    cy.contains('1.5 Presentations').should('be.visible');
   });
 
   it('renders in read-only mode correctly', () => {
diff --git a/src/components/collaborator_list/CollaboratorAddEdit.tsx b/src/components/collaborator_list/CollaboratorAddEdit.tsx
index 89af7b43b6..cf34b8f977 100644
--- a/src/components/collaborator_list/CollaboratorAddEdit.tsx
+++ b/src/components/collaborator_list/CollaboratorAddEdit.tsx
@@ -1,6 +1,10 @@
-import React, { useState } from 'react';
+import React, {useState} from 'react';
 import { FormField, FormValidators } from '../forms/forms';
 import {Collaborator} from "src/types/model";
+import {ValidationError} from "src/pages/dar_application/FormValidationState";
+import {computeCollaboratorErrors, validationFailed} from "src/utils/darFormUtils";
+import {nihAccountLabel} from "src/utils/ERACommonsUtils";
+import ApproverStatus from "src/pages/dar_application/collaborator/ApproverStatus";
 
 interface FormFieldChange {
     key: string;
@@ -14,12 +18,31 @@ interface CollaboratorAddEditProps {
     readonly collaborators: Collaborator[];
     readonly closeAction: () => void;
     readonly onCollaboratorChange: (collaborators: Collaborator[]) => void;
+    readonly showApproverStatus?: boolean;
 }
 
-export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): React.JSX.Element {
-    const { id, collaborator, collaboratorText, collaborators, closeAction, onCollaboratorChange } = props;
+interface Validation {
+    name?: ValidationError;
+    eraCommonsId?: ValidationError;
+    title?: ValidationError;
+    email?: ValidationError;
+    approverStatus?: ValidationError;
+}
 
+export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): React.JSX.Element {
+    const { id, collaborator, collaboratorText, collaborators, closeAction, onCollaboratorChange, showApproverStatus = false } = props;
     const [newCollaborator, setNewCollaborator] = useState(collaborator);
+    const [validation, setValidation] = useState<Validation>({});
+    const accountLabel = nihAccountLabel();
+
+    const onChange = ({ key, value }: FormFieldChange) => {
+        const setCollaborator = {
+            ...newCollaborator,
+            [key]: value
+        } as Collaborator;
+        setNewCollaborator(setCollaborator);
+        setValidation(computeCollaboratorErrors({collaborator: setCollaborator, needsApproverStatus: showApproverStatus}));
+    };
 
     return (
         <div className='form-group row no-margin'>
@@ -32,13 +55,17 @@ export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): Re
                         defaultValue={collaborator?.name}
                         placeholder='Full Name'
                         validators={[FormValidators.REQUIRED]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setCollaborator = {
-                                ...newCollaborator,
-                                [key]: value
-                            } as Collaborator;
-                            setNewCollaborator(setCollaborator);
-                        }}
+                        onChange={onChange}
+                        validation={validation.name}
+                    />
+                    <FormField
+                        id='eraCommonsId'
+                        title={`${collaboratorText} ${accountLabel} Account`}
+                        defaultValue={collaborator?.eraCommonsId}
+                        placeholder={`${accountLabel} Account`}
+                        validators={[FormValidators.REQUIRED]}
+                        onChange={onChange}
+                        validation={validation.eraCommonsId}
                     />
                     <FormField
                         id='title'
@@ -46,13 +73,8 @@ export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): Re
                         defaultValue={collaborator?.title}
                         placeholder='Title'
                         validators={[FormValidators.REQUIRED]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setCollaborator = {
-                                ...newCollaborator,
-                                [key]: value
-                            } as Collaborator;
-                            setNewCollaborator(setCollaborator);
-                        }}
+                        onChange={onChange}
+                        validation={validation.title}
                     />
                     <FormField
                         id='email'
@@ -60,20 +82,22 @@ export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): Re
                         defaultValue={collaborator?.email}
                         placeholder='Email'
                         validators={[FormValidators.REQUIRED, FormValidators.EMAIL]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setCollaborator = {
-                                ...newCollaborator,
-                                [key]: value
-                            } as Collaborator;
-                            setNewCollaborator(setCollaborator);
-                        }}
+                        onChange={onChange}
+                        validation={validation.email}
                     />
+                    {showApproverStatus && (
+                    <ApproverStatus
+                        index={id}
+                        approverStatus={newCollaborator?.approverStatus}
+                        validation={validation.approverStatus}
+                        onChange={onChange}/>
+                    )}
                 </div>
                 <div className='row' style={{ marginTop: 20 }}>
                     {/* add/save button */}
-                    <div
+                    <button
                         className='collaborator-form-add-save-button f-left btn'
-                        role='button'
+                        type='button'
                         onClick={() => {
                             if (id < 0) {
                                 onCollaboratorChange([...collaborators, newCollaborator]);
@@ -86,9 +110,10 @@ export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): Re
                             }
                             closeAction();
                         }}
+                        disabled={validationFailed(computeCollaboratorErrors({collaborator: newCollaborator, needsApproverStatus: showApproverStatus}))}
                     >
                         {collaborator === undefined ? 'Add' : 'Save'}
-                    </div>
+                    </button>
                     {/* cancel button */}
                     <div
                         className='collaborator-form-cancel-button f-left btn'
diff --git a/src/components/collaborator_list/CollaboratorList.tsx b/src/components/collaborator_list/CollaboratorList.tsx
index d336d6951a..e721c35c18 100644
--- a/src/components/collaborator_list/CollaboratorList.tsx
+++ b/src/components/collaborator_list/CollaboratorList.tsx
@@ -9,10 +9,11 @@ interface CollaboratorListProps {
     readonly columnsToShow?: string[];
     readonly onCollaboratorChange: (collaborators: Collaborator[]) => void;
     readonly disabled?: boolean;
+    readonly showApproverStatus?: boolean;
 }
 
 export default function CollaboratorList(props: CollaboratorListProps): React.JSX.Element {
-    const { collaborators, collaboratorText, columnsToShow = [], onCollaboratorChange, disabled = false } = props;
+    const { collaborators, collaboratorText, columnsToShow = [], onCollaboratorChange, disabled = false, showApproverStatus = false } = props;
 
     const [showAddEdit, setShowAddEdit] = useState(false);
     const [editState, setEditState] = useState(collaborators.map(() => false));
@@ -52,6 +53,7 @@ export default function CollaboratorList(props: CollaboratorListProps): React.JS
                         collaborators={collaborators}
                         closeAction={() => setShowAddEdit(false)}
                         onCollaboratorChange={onCollaboratorChange}
+                        showApproverStatus={showApproverStatus}
                     />
                 )}
             </div>
diff --git a/src/components/publications_list/PublicationAddEdit.tsx b/src/components/publications_list/PublicationAddEdit.tsx
index 50bd1eb9dd..ae3f6c421a 100644
--- a/src/components/publications_list/PublicationAddEdit.tsx
+++ b/src/components/publications_list/PublicationAddEdit.tsx
@@ -1,6 +1,8 @@
-import React, { useState } from 'react';
+import React, {useState} from 'react';
 import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms';
 import { PublicationOrPresentation } from 'src/components/publications_list/PublicationOrPresentation';
+import { ValidationError } from "src/pages/dar_application/FormValidationState";
+import {validationFailed, calcPublicationOrPresentationErrors, isPublication} from "src/utils/darFormUtils";
 
 interface FormFieldChange {
     key: string;
@@ -16,10 +18,29 @@ interface PublicationAddEditProps {
     readonly onPublicationChange: (publications: PublicationOrPresentation[]) => void;
 }
 
+interface Validation {
+    title?: ValidationError;
+    date?: ValidationError;
+    authors?: ValidationError;
+    pubmed_id?: ValidationError;
+    bibliographic_citation?: ValidationError;
+    link?: ValidationError;
+}
 export default function PublicationAddEdit(props: PublicationAddEditProps): React.JSX.Element {
     const { id, publication, publicationText, publications, closeAction, onPublicationChange } = props;
 
     const [newPublication, setNewPublication] = useState(publication);
+    const [validation, setValidation] = useState<Validation>({});
+
+    const onChange = ({ key, value }: FormFieldChange) => {
+        const setPublication = {
+            ...newPublication,
+            [key]: value
+        } as PublicationOrPresentation;
+        setNewPublication(setPublication);
+        setValidation(calcPublicationOrPresentationErrors(setPublication, publicationText));
+    };
+
 
     return (
         <div className='form-group row no-margin'>
@@ -32,13 +53,8 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                         defaultValue={publication?.title}
                         placeholder='Title'
                         validators={[FormValidators.REQUIRED]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setPublication = {
-                                ...newPublication,
-                                [key]: value
-                            } as PublicationOrPresentation;
-                            setNewPublication(setPublication);
-                        }}
+                        onChange={onChange}
+                        validation={validation.title}
                     />
                     <FormField
                         id='date'
@@ -46,13 +62,8 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                         defaultValue={publication?.date}
                         placeholder='Date'
                         validators={[FormValidators.REQUIRED, FormValidators.DATE]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setPublication = {
-                                ...newPublication,
-                                [key]: value
-                            } as PublicationOrPresentation;
-                            setNewPublication(setPublication);
-                        }}
+                        onChange={onChange}
+                        validation={validation.date}
                     />
                     <FormField
                         id='authors'
@@ -60,15 +71,10 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                         defaultValue={publication?.authors}
                         placeholder='Authors'
                         validators={[FormValidators.REQUIRED]}
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setPublication = {
-                                ...newPublication,
-                                [key]: value
-                            } as PublicationOrPresentation;
-                            setNewPublication(setPublication);
-                        }}
+                        onChange={onChange}
+                        validation={validation.authors}
                     />
-                    {publicationText === 'Publication' ?
+                    {isPublication(publicationText) ?
                         <>
                             <FormField
                                 id='pubmed_id'
@@ -76,27 +82,17 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                                 defaultValue={publication?.pubmed_id}
                                 placeholder='PubMed ID'
                                 validators={[FormValidators.REQUIRED]}
-                                onChange={({ key, value }: FormFieldChange) => {
-                                    const setPublication = {
-                                        ...newPublication,
-                                        [key]: value
-                                    } as PublicationOrPresentation;
-                                    setNewPublication(setPublication);
-                                }}
+                                onChange={onChange}
+                                validation={validation.pubmed_id}
                             />
                             <FormField
                                 id='bibliographic_citation'
                                 title={`${publicationText} Bibliographic Citation`}
                                 defaultValue={publication?.bibliographic_citation}
-                                placeholder='Date'
+                                placeholder='Bibliographic Citation'
                                 validators={[FormValidators.REQUIRED]}
-                                onChange={({ key, value }: FormFieldChange) => {
-                                    const setPublication = {
-                                        ...newPublication,
-                                        [key]: value
-                                    } as PublicationOrPresentation;
-                                    setNewPublication(setPublication);
-                                }}
+                                onChange={onChange}
+                                validation={validation.bibliographic_citation}
                             />
                         </> : // otherwise it's a presentation
                         <FormField
@@ -105,13 +101,8 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                             defaultValue={publication?.link}
                             placeholder='Link'
                             validators={[FormValidators.REQUIRED]}
-                            onChange={({ key, value }: FormFieldChange) => {
-                                const setPublication = {
-                                    ...newPublication,
-                                    [key]: value
-                                } as PublicationOrPresentation;
-                                setNewPublication(setPublication);
-                            }}
+                            onChange={onChange}
+                            validation={validation.link}
                         />
                     }
                     <FormField
@@ -119,33 +110,21 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                         title={`Dataset citation used in this publication`}
                         defaultValue={publication?.dataset_citation}
                         placeholder='Dataset Citation'
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setPublication = {
-                                ...newPublication,
-                                [key]: value
-                            } as PublicationOrPresentation;
-                            setNewPublication(setPublication);
-                        }}
+                        onChange={onChange}
                     />
                     <FormField
                         id='did_cite'
                         type={FormFieldTypes.YESNORADIOGROUP}
                         title='Did you cite the dataset(s) used in this publication?'
                         orientation='horizontal'
-                        onChange={({ key, value }: FormFieldChange) => {
-                            const setPublication = {
-                                ...newPublication,
-                                [key]: value
-                            } as PublicationOrPresentation;
-                            setNewPublication(setPublication);
-                        }}
+                        onChange={onChange}
                     />
                 </div>
                 <div className='row' style={{ marginTop: 20 }}>
                     {/* add/save button */}
-                    <div
+                    <button
                         className='collaborator-form-add-save-button f-left btn'
-                        role='button'
+                        type='button'
                         onClick={() => {
                             if (id < 0) {
                                 onPublicationChange([...publications, newPublication]);
@@ -158,9 +137,10 @@ export default function PublicationAddEdit(props: PublicationAddEditProps): Reac
                             }
                             closeAction();
                         }}
+                        disabled={validationFailed(calcPublicationOrPresentationErrors(newPublication, publicationText))}
                     >
                         {publication === undefined ? 'Add' : 'Save'}
-                    </div>
+                    </button>
                     {/* cancel button */}
                     <div
                         className='collaborator-form-cancel-button f-left btn'
diff --git a/src/components/publications_list/PublicationList.tsx b/src/components/publications_list/PublicationList.tsx
index 2886d2ccae..8c2d21e3d3 100644
--- a/src/components/publications_list/PublicationList.tsx
+++ b/src/components/publications_list/PublicationList.tsx
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
 import PublicationAddEdit from './PublicationAddEdit';
 import PublicationRow from './PublicationRow';
 import { PublicationOrPresentation } from './PublicationOrPresentation';
+import {DarErrors} from "src/pages/dar_application/FormValidationState";
+import {isPublication} from "src/utils/darFormUtils";
 
 interface PublicationListProps {
     publications: PublicationOrPresentation[];
@@ -9,10 +11,18 @@ interface PublicationListProps {
     readonly columnsToShow?: string[];
     readonly onPublicationChange: (publications: PublicationOrPresentation[]) => void;
     readonly disabled?: boolean;
+    readonly validation?: DarErrors;
 }
 
 export default function PublicationList(props: PublicationListProps): React.JSX.Element {
-    const { publications, publicationText, columnsToShow = [], onPublicationChange, disabled = false } = props;
+    const {
+        publications,
+        publicationText,
+        columnsToShow = [],
+        onPublicationChange,
+        disabled = false,
+        validation
+    } = props;
 
     const [showAddEdit, setShowAddEdit] = useState(false);
     const [editState, setEditState] = useState(publications.map(() => false));
@@ -28,6 +38,14 @@ export default function PublicationList(props: PublicationListProps): React.JSX.
         onPublicationChange(updatedPublications);
     }
 
+    const getValidationState = () => {
+        if (isPublication(publicationText)) {
+            return validation?.publications;
+        } else {
+            return validation?.presentations;
+        }
+    }
+
     return (
         <div className="publication-list-component">
             <div className="row no-margin">
@@ -38,6 +56,8 @@ export default function PublicationList(props: PublicationListProps): React.JSX.
                     style={{
                         marginTop: 25,
                         marginBottom: 5,
+                        border: getValidationState() ? '1px solid red' : '1px solid #0948B7',
+                        boxShadow: getValidationState() ? '0 0 5px red' : 'none',
                         ...(disabled ? { cursor: 'not-allowed' } : {}),
                     }}
                     onClick={() => !disabled && setShowAddEdit(true) }
diff --git a/src/pages/dar_application/DataUseAcknowlegements.tsx b/src/pages/dar_application/DataUseAcknowlegements.tsx
index c6f2f33c11..779e18088f 100644
--- a/src/pages/dar_application/DataUseAcknowlegements.tsx
+++ b/src/pages/dar_application/DataUseAcknowlegements.tsx
@@ -12,7 +12,7 @@ type DataUseAcknowledgementsProps = {
     readOnlyMode: boolean,
     includeInstructions?: boolean,
     onChange: (obj: object) => void,
-    onValidationChange: (validation: {key: string, validation: ValidationError}) => void,
+    onValidationChange?: (validation: {key: string, validation: ValidationError}) => void,
     validation?: DarErrors
 }
 
@@ -37,6 +37,7 @@ return (
               key={'dataUseAcknowledgements'}
               title={title}
               description={includeInstructions ? 'Please confirm listed acknowledgements and/or document requirements below:' : ''}
+              validation={validation?.gsoAcknowledgement || validation?.pubAcknowledgement || validation?.dsAcknowledgement}
           />
         }
         {
diff --git a/src/pages/dar_application/FormValidationState.tsx b/src/pages/dar_application/FormValidationState.tsx
index 2ea7ccdb7c..386befc62d 100644
--- a/src/pages/dar_application/FormValidationState.tsx
+++ b/src/pages/dar_application/FormValidationState.tsx
@@ -22,6 +22,7 @@ export interface ResearcherErrors {
 }
 
 export interface DarErrors {
+    nihEraId?: ValidationError,
     datasetIds?: ValidationError,
     projectTitle?: ValidationError,
     rus?: ValidationError,
@@ -33,6 +34,28 @@ export interface DarErrors {
     gsoAcknowledgement?: ValidationError,
     pubAcknowledgement?: ValidationError,
     dsAcknowledgement?: ValidationError,
+    progressReportSummary?: ValidationError,
+    intellectualPropertyYesNo?: ValidationError,
+    intellectualPropertySummary?: ValidationError,
+    publicationsYesNo?: ValidationError,
+    publications?: ValidationError,
+    presentationsYesNo?: ValidationError,
+    presentations?: ValidationError,
+    labCollaborators?: ValidationError,
+    internalCollaborators?: ValidationError,
+    externalCollaborators?: ValidationError,
+    dmiYesNo?: ValidationError,
+    dmiCombination?: ValidationError,
+    dmiIdentification?: ValidationError,
+    dmiSharing?: ValidationError,
+    dmiSecurity?: ValidationError,
+    dmiAcknowledgement?: ValidationError,
+    dmiPublication?: ValidationError,
+    dmiFalsification?: ValidationError,
+    dmiOther?: ValidationError,
+    dmiDescription?: ValidationError,
+    closeoutYesNo?: ValidationError,
+    closeoutSupplement?: ValidationError,
 }
 
 export interface RusErrors {
diff --git a/src/pages/dar_application/ProgressReportApplication.tsx b/src/pages/dar_application/ProgressReportApplication.tsx
index b7ead33bdc..bb957ce439 100644
--- a/src/pages/dar_application/ProgressReportApplication.tsx
+++ b/src/pages/dar_application/ProgressReportApplication.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useEffect, useCallback} from 'react';
+import React, {useState, useEffect} from 'react';
 import {DataAccessRequest, Dataset, DuosUser} from 'src/types/model';
 import {Location} from 'history';
 import {DMI_INCIDENT_KEYS, FormState} from 'src/pages/progress_reports/ProgressReportFormState';
@@ -13,8 +13,8 @@ import {Storage} from "src/libs/storage";
 import {History} from 'history';
 import {DataUseAcknowledgements} from 'src/pages/dar_application/DataUseAcknowlegements';
 import {translateDataUseRestrictionsFromDataUseArray} from 'src/libs/dataUseTranslation';
-import {validatePRFormData} from 'src/utils/darFormUtils';
-import {FormValidationState, ValidationError} from 'src/pages/dar_application/FormValidationState';
+import {validatePRFormData, validationFailed} from 'src/utils/darFormUtils';
+import {FormValidationState} from 'src/pages/dar_application/FormValidationState';
 
 type ProgressReportApplicationProps = {
   readonly dar: DataAccessRequest; // corresponds either to the parent DAR for a new application or an existing readonly progress report
@@ -55,18 +55,30 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
     };
 
     const [formState, setFormState] = useState<FormState>(initialState);
+    const [nihValid, setNihValid] = useState<boolean>(true);
     const [dataUseTranslations, setDataUseTranslations] = useState<string[]>([]);
     const [selectedDatasets, setSelectedDatasets] = useState<Dataset[]>(datasets);
-    const [formValidation, setFormValidation] = useState<FormValidationState>(
-      {darErrors:
-            {gsoAcknowledgement: {}, pubAcknowledgement: {}, dsAcknowledgement: {}}});
+    const getValidation = (newState) => {
+        if (!readOnlyMode) {
+            return validatePRFormData(
+                nihValid,
+                newState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+        }
+        return {darErrors: {}}
+    }
+    const [formValidation, setFormValidation] = useState<FormValidationState>({darErrors:{}});
     const eRACommonsDestination = 'progress_report_application/' + dar.collectionId;
 
     const onFormChange = (newState: Partial<FormState>) => {
+        const setState = {...formState, ...newState};
         setFormState(prevState => ({
             ...prevState,
             ...newState
         }));
+        setFormValidation(getValidation(setState))
     };
 
     // required because the datasets state changes during component mount
@@ -86,28 +98,6 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
-        setFormValidation((formValidation) => {
-            return {
-                ...formValidation,
-                darErrors: {
-                    ...formValidation.darErrors,
-                    [key]: validation
-                }
-            };
-        });
-    }, []);
-
     return (
         <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
             <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
@@ -118,6 +108,9 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
                     eRACommonsDestination={eRACommonsDestination}
                     researcher={researcher}
                     location={location}
+                    validation={formValidation.darErrors}
+                    nihValid={nihValid}
+                    onNihStatusUpdate={setNihValid}
                 />
             </div>
             <div data-cy='remove-datasets'>
@@ -133,7 +126,7 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
             </div>
             <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
                 <DataUseAcknowledgements
-                    title={'3.1 Data Use Acknowledgements'}
+                    title={'2.1 Data Use Acknowledgements'}
                     datasets={selectedDatasets}
                     dataUseTranslations={dataUseTranslations}
                     formData={formState}
@@ -141,7 +134,6 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
                     onChange={(dua) => {
                         onFormChange({[dua.key]: dua.value})
                     }}
-                    onValidationChange={formValidationChange}
                     validation={formValidation.darErrors}
                 />
             </div>
@@ -149,10 +141,20 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
                 <CollaboratorChanges readOnly={readOnlyMode} formState={formState} onFormChange={onFormChange}/>
             </div>
             <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
-                <DataManagementIncident readOnly={readOnlyMode} formState={formState} onFormChange={onFormChange}/>
+                <DataManagementIncident
+                    readOnly={readOnlyMode}
+                    formState={formState}
+                    onFormChange={onFormChange}
+                    validation={formValidation.darErrors}
+                />
             </div>
             <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
-                <DarCloseout readOnly={readOnlyMode} formState={formState} onFormChange={onFormChange}/>
+                <DarCloseout
+                    readOnly={readOnlyMode}
+                    formState={formState}
+                    onFormChange={onFormChange}
+                    validation={formValidation.darErrors}
+                />
             </div>
             <br/><br/>
             {!readOnlyMode && <div>
@@ -165,7 +167,7 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
                     onCancel={() => {
                         Navigation.console(Storage.getCurrentUser(), history);
                     }}
-                    validateForm={validateForm}
+                    disabled={validationFailed(formValidation)}
                 />
             </div>}
         </div>
diff --git a/src/pages/dar_application/collaborator/ApproverStatus.tsx b/src/pages/dar_application/collaborator/ApproverStatus.tsx
new file mode 100644
index 0000000000..cd9f172aef
--- /dev/null
+++ b/src/pages/dar_application/collaborator/ApproverStatus.tsx
@@ -0,0 +1,54 @@
+import {FormField, FormFieldTypes, FormValidators} from "src/components/forms/forms";
+import React from "react";
+import {ValidationError} from "src/pages/dar_application/FormValidationState";
+
+type ApproverStatusType = boolean | 'yes' | 'no' | undefined;
+
+interface ApproverStatusProps {
+    readonly index: number;
+    readonly approverStatus: ApproverStatusType;
+    readonly validation?: ValidationError;
+    readonly onValidationChange?: ({key: string, validator: ValidationError}) => void;
+    readonly onChange: ({key: string, value: ApproverStatusType}) => void;
+}
+export default function ApproverStatus(props: ApproverStatusProps): React.JSX.Element {
+    const {index, approverStatus, validation, onValidationChange, onChange} = props;
+    const calculateDefaultValue = () => {
+        if (approverStatus === true || approverStatus === 'yes') {
+            return 'yes';
+        }
+        if (approverStatus === false || approverStatus === 'no') {
+            return 'no';
+        }
+        return undefined;
+    }
+
+    return (
+        <div className='row' style={{marginTop: 25}}>
+            <FormField
+                id={`${index}_collaboratorApproval`}
+                type={FormFieldTypes.RADIOGROUP}
+                name='approverStatus'
+                description={`Are you requesting permission for this member of the Internal Lab Staff to be given
+                Designated Download/Approval' status? This indication should be limited to individuals who
+                the PI designates to download data and/or share the requested data with other Internal Lab Staff
+                (ie., staff members and trainees under the direct supervision of the PI).`}
+                options={[
+                    {name: 'yes', text: 'Yes'},
+                    {name: 'no', text: 'No'}
+                ]}
+                validators={[FormValidators.REQUIRED]}
+                orientation='horizontal'
+                defaultValue={calculateDefaultValue}
+                validation={validation}
+                onValidationChange={onValidationChange}
+                onChange={onChange}
+            />
+            <p className='control-label rp-choice-questions' style={{fontSize: 14, marginTop: 5, marginBottom: 5}}>
+                Please note: the terms of the Library Card Agreement are applicable to the Library Card Holder as well
+                as
+                their Internal Lab Staff.
+            </p>
+        </div>
+    )
+};
\ No newline at end of file
diff --git a/src/pages/dar_application/collaborator/CollaboratorForm.jsx b/src/pages/dar_application/collaborator/CollaboratorForm.jsx
index 34530041ec..3f422ae7d7 100644
--- a/src/pages/dar_application/collaborator/CollaboratorForm.jsx
+++ b/src/pages/dar_application/collaborator/CollaboratorForm.jsx
@@ -1,10 +1,11 @@
-import {FormFieldTypes, FormField, FormValidators} from '../../../components/forms/forms';
+import {FormField, FormValidators} from 'src/components/forms/forms.jsx';
 import React, {useEffect, useState} from 'react';
 import {isEmpty, isNil} from 'lodash/fp';
 import {v4 as uuidV4} from 'uuid';
-import {computeCollaboratorErrors} from '../../../utils/darFormUtils';
+import {computeCollaboratorErrors} from 'src/utils/darFormUtils.js';
 import DeleteCollaboratorModal from './DeleteCollaboratorModal';
-import {nihAccountLabel} from '../../../utils/ERACommonsUtils.js';
+import {nihAccountLabel} from 'src/utils/ERACommonsUtils.js';
+import ApproverStatus from 'src/pages/dar_application/collaborator/ApproverStatus.js';
 
 export default function CollaboratorForm(props) {
   const {
@@ -104,35 +105,13 @@ export default function CollaboratorForm(props) {
           />
         </div>
         {props.showApproval && (
-          <div className='row' style={{marginTop: 25}}>
-            <FormField
-              id={`${index}_collaboratorApproval`}
-              type={FormFieldTypes.RADIOGROUP}
-              name='approverStatus'
-              description={`Are you requesting permission for this member of the Internal Lab Staff to be given
-                Designated Download/Approval' status? This indication should be limited to individuals who
-                the PI designates to download data and/or share the requested data with other Internal Lab Staff
-                (ie., staff members and trainees under the direct supervision of the PI).`}
-              options={[
-                {name: 'yes', text: 'Yes'},
-                {name: 'no', text: 'No'}
-              ]}
-              validators={[FormValidators.REQUIRED]}
-              orientation='horizontal'
-              defaultValue={
-                (approverStatus === true || approverStatus === 'yes') ? 'yes'
-                  : (approverStatus === false || approverStatus === 'no') ? 'no'
-                    : undefined
-              }
-              validation={validation.approverStatus}
-              onValidationChange={onValidationChange}
-              onChange={({value}) => setApproverStatus(value)}
-            />
-            <p className='control-label rp-choice-questions' style={{fontSize: 14, marginTop: 5, marginBottom: 5}}>
-              Please note: the terms of the Library Card Agreement are applicable to the Library Card Holder as well as
-              their Internal Lab Staff.
-            </p>
-          </div>
+          <ApproverStatus
+            index={index}
+            approverStatus={approverStatus}
+            validation={validation.approverStatus}
+            onValidationChange={onValidationChange}
+            onChange={({_key, value}) => setApproverStatus(value)}
+          />
         )}
         <div className='row' style={{marginTop: 20}}>
           {/* Toggle Delete Buttons Cancel/Delete */}
diff --git a/src/pages/progress_reports/CollaboratorChanges.tsx b/src/pages/progress_reports/CollaboratorChanges.tsx
index 6f946f2bf1..46cbbe61c2 100644
--- a/src/pages/progress_reports/CollaboratorChanges.tsx
+++ b/src/pages/progress_reports/CollaboratorChanges.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
 import {FormState, FormStateKey} from 'src/pages/progress_reports/ProgressReportFormState';
 import CollaboratorList from 'src/components/collaborator_list/CollaboratorList';
 import {Collaborator} from "src/types/model";
+import {FormFieldTitle} from "src/components/forms/forms";
 
 interface CollaboratorProps {
     readonly readOnly: boolean;
@@ -39,27 +40,26 @@ export default function CollaboratorChanges(props: CollaboratorProps): React.JSX
                 <h2>Step 3: Add or Remove Collaborators</h2>
 
                 <div className='progress-report-row'>
-                    <div>
-                        <h3>3.1 Internal Lab Staff</h3>
-                        <p>
-                            Please add Internal Lab Staff here. Internal Lab Staff are defined as users of data from this Data Access Request, including any data that are downloaded or utilized in the cloud. Please do not list External Collaborators or Internal Collaborators at a PI or equivalent level here.
-                        </p>
-                    </div>
+                    <FormFieldTitle
+                        id='internalLabStaffTitle'
+                        title='3.1 Internal Lab Staff'
+                        description='Please add Internal Lab Staff here. Internal Lab Staff are defined as users of data from this Data Access Request, including any data that are downloaded or utilized in the cloud. Please do not list External Collaborators or Internal Collaborators at a PI or equivalent level here.'
+                    />
                     <CollaboratorList
                         collaborators={labCollaborators}
                         collaboratorText='Internal Lab Staff'
                         columnsToShow={['name', 'title']}
                         onCollaboratorChange={onInternalLabStaffChange}
                         disabled={readOnly}
+                        showApproverStatus={true}
                     />
                 </div>
                 <div className='progress-report-row'>
-                    <div>
-                        <h3>3.2 Internal Collaborators</h3>
-                        <p>
-                            Please add Internal Collaborators here. Internal Collaborators are defined as individuals who are not under the direct supervision of the PI (e.g., not a member of the PI&apos;s laboratory) who assists with the PI&apos;s research project involving controlled-access data subject to the NIH GDS Policy. Internal collaborators are employees of the Requesting PI&apos;s institution and work at the same location/campus as the PI. Internal Collaborators must be at the PI or equivalent level and are required to have a Library Card in order to access data through this request. Internal Collaborators will have Data Downloader/Approver status so that they may add their own relevant Internal Lab Staff. Internal Collaborators will not be required to submit an independent DAR to collaborate on this project.
-                        </p>
-                    </div>
+                    <FormFieldTitle
+                        id='internalCollaboratorsTitle'
+                        title='3.2 Internal Collaborators'
+                        description='Please add Internal Collaborators here. Internal Collaborators are defined as individuals who are not under the direct supervision of the PI (e.g., not a member of the PI&apos;s laboratory) who assists with the PI&apos;s research project involving controlled-access data subject to the NIH GDS Policy. Internal collaborators are employees of the Requesting PI&apos;s institution and work at the same location/campus as the PI. Internal Collaborators must be at the PI or equivalent level and are required to have a Library Card in order to access data through this request. Internal Collaborators will have Data Downloader/Approver status so that they may add their own relevant Internal Lab Staff. Internal Collaborators will not be required to submit an independent DAR to collaborate on this project.'
+                    />
                     <CollaboratorList
                         collaborators={internalCollaborators}
                         collaboratorText='Internal Collaborators'
@@ -69,12 +69,11 @@ export default function CollaboratorChanges(props: CollaboratorProps): React.JSX
                     />
                 </div>
                 <div className='progress-report-row'>
-                    <div>
-                        <h3>3.3 External Collaborators</h3>
-                        <p>
-                            Please list External collaborators here. External Collaboratos are not employees of the Requesting PI&apos;s institution and/or do not work at the same location as the PI, and consequently must be independently approved to access controlled-access data subject to the GDS Policy. External Collaborators must be at the PI or equivalent level and are not required to have a Library Card in order to access data, although it is encouraged. Note: External Collaborators must submit an independent DAR approved by their signing Official to collaborate on this project. External Collaborators will be able to add their Lab Staff, as needed, via their independent DAR. Approval of this DAR does not indicate approval of the External Collaborators listed.
-                        </p>
-                    </div>
+                    <FormFieldTitle
+                        id='externalCollaboratorsTitle'
+                        title='3.3 External Collaborators'
+                        description='Please list External collaborators here. External Collaborators are not employees of the Requesting PI&apos;s institution and/or do not work at the same location as the PI, and consequently must be independently approved to access controlled-access data subject to the GDS Policy. External Collaborators must be at the PI or equivalent level and are not required to have a Library Card in order to access data, although it is encouraged. Note: External Collaborators must submit an independent DAR approved by their signing Official to collaborate on this project. External Collaborators will be able to add their Lab Staff, as needed, via their independent DAR. Approval of this DAR does not indicate approval of the External Collaborators listed.'
+                    />
                     <CollaboratorList
                         collaborators={externalCollaborators}
                         collaboratorText='External Collaborators'
diff --git a/src/pages/progress_reports/DarCloseout.tsx b/src/pages/progress_reports/DarCloseout.tsx
index c94da83c7c..ec41337591 100644
--- a/src/pages/progress_reports/DarCloseout.tsx
+++ b/src/pages/progress_reports/DarCloseout.tsx
@@ -2,15 +2,18 @@ import React from 'react';
 import { FormField, FormFieldTypes } from 'src/components/forms/forms';
 import {ValidFormState, FormState, FormStateKey} from 'src/pages/progress_reports/ProgressReportFormState';
 import {CloseOutSupplement} from "src/types/model";
+import {DarErrors, ValidationError} from "src/pages/dar_application/FormValidationState";
 
 interface DarCloseoutProps {
     readonly readOnly: boolean;
     formState: FormState;
     onFormChange: (newState: Partial<FormState>) => void;
+    onValidationChange?: (validationState: { key: string, validation: ValidationError }) => void;
+    validation?: DarErrors;
 }
 
 export default function DarCloseout(props: DarCloseoutProps): React.JSX.Element {
-    const { readOnly, formState, onFormChange } = props;
+    const { readOnly, formState, onFormChange, onValidationChange, validation } = props;
 
     return (
         <div data-cy='dar-closeout'>
@@ -25,14 +28,17 @@ export default function DarCloseout(props: DarCloseoutProps): React.JSX.Element
                         description={<span>Are you ready to finish work on this project?</span>}
                         orientation='horizontal'
                         onChange={({ key, value }: ValidFormState) => {
-                            onFormChange({ [key]: value }  as Partial<FormState>);
-                            // clear out selected options if the user selects "No"
+                            const newState = {[key]: value} as Partial<FormState>;
                             if (value === false) {
-                                onFormChange({ [FormStateKey.CLOSEOUT_SUPPLEMENT]: undefined } as Partial<FormState>);
+                                // If the user selects "No", clear out the closeout supplement field
+                                newState[FormStateKey.CLOSEOUT_SUPPLEMENT] = undefined;
                             }
+                            onFormChange(newState);
                         }}
                         defaultValue={formState.closeoutYesNo}
                         disabled={readOnly}
+                        validation={validation?.closeoutYesNo}
+                        onValidationChange={onValidationChange}
                     />
                     {formState.closeoutYesNo && (
                         <div>
@@ -57,6 +63,8 @@ export default function DarCloseout(props: DarCloseoutProps): React.JSX.Element
                                 onChange={({ key, value }: ValidFormState) => {
                                     onFormChange({ [key]: value } as Partial<FormState>);
                                 }}
+                                onValidationChange={onValidationChange}
+                                validation={validation?.closeoutSupplement}
                             />
                         </div>
                     </div>
diff --git a/src/pages/progress_reports/DataManagementIncident.tsx b/src/pages/progress_reports/DataManagementIncident.tsx
index dc44fd53cd..f399802a3a 100644
--- a/src/pages/progress_reports/DataManagementIncident.tsx
+++ b/src/pages/progress_reports/DataManagementIncident.tsx
@@ -2,17 +2,18 @@ import React from 'react';
 import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 import { FormField, FormFieldTypes } from 'src/components/forms/forms';
 import {ValidFormState, FormState, FormStateKey} from 'src/pages/progress_reports/ProgressReportFormState';
-
-const titleStyle = { fontSize: '24px', fontWeight: 500, color: '#333333' };
+import {DarErrors, ValidationError} from "src/pages/dar_application/FormValidationState";
 
 interface DataManagementIncidentProps {
     readonly readOnly: boolean;
     formState: FormState;
     onFormChange: (newState: Partial<FormState>) => void;
+    onValidationChange?: (validationState: { key: string; validation: ValidationError }) => void;
+    validation?: DarErrors;
 }
 
 export default function DataManagementIncident(props: DataManagementIncidentProps): React.JSX.Element {
-    const { readOnly, formState, onFormChange } = props;
+    const { readOnly, formState, onFormChange, onValidationChange, validation } = props;
 
     return (
         <div data-cy='data-management-incident'>
@@ -25,26 +26,26 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                             id={FormStateKey.DMI_YES_NO}
                             type={FormFieldTypes.YESNORADIOGROUP}
                             title='4.1 Data Management Incident'
-                            titleStyle={titleStyle}
                             description='Have there been any incidents related to mismanagement or misuse of data?'
                             orientation='horizontal'
                             defaultValue={formState.dmiYesNo}
                             onChange={({ key, value }: ValidFormState) => {
+                                const newState = { [key]: value } as Partial<FormState>;
                                 if (value === false) {
-                                    onFormChange({
-                                        dmiCombination: false,
-                                        dmiIdentification: false,
-                                        dmiSharing: false,
-                                        dmiSecurity: false,
-                                        dmiAcknowledgement: false,
-                                        dmiPublication: false,
-                                        dmiFalsification: false,
-                                        dmiOther: false
-                                    });
+                                    newState[FormStateKey.DMI_COMBINATION] = false;
+                                    newState[FormStateKey.DMI_IDENTIFICATION] = false;
+                                    newState[FormStateKey.DMI_SHARING] = false;
+                                    newState[FormStateKey.DMI_SECURITY] = false;
+                                    newState[FormStateKey.DMI_ACKNOWLEDGEMENT] = false;
+                                    newState[FormStateKey.DMI_PUBLICATION] = false;
+                                    newState[FormStateKey.DMI_FALSIFICATION] = false;
+                                    newState[FormStateKey.DMI_OTHER] = false;
                                 }
-                                onFormChange({ [key]: value } as Partial<FormState>);
+                                onFormChange(newState);
                             }}
                             disabled={readOnly}
+                            validation={validation?.dmiYesNo}
+                            onValidationChange={onValidationChange}
                         />
                     </div>
                     {formState.dmiYesNo &&
@@ -60,6 +61,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value }  as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiCombination}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_IDENTIFICATION}
@@ -70,6 +73,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiIdentification}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_SHARING}
@@ -80,6 +85,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value }  as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiSharing}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_SECURITY}
@@ -90,6 +97,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiSecurity}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_ACKNOWLEDGEMENT}
@@ -100,6 +109,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiAcknowledgement}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_PUBLICATION}
@@ -110,6 +121,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiPublication}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_FALSIFICATION}
@@ -120,6 +133,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiFalsification}
+                                    onValidationChange={onValidationChange}
                                 />
                                 <FormField
                                     id={FormStateKey.DMI_OTHER}
@@ -130,13 +145,14 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiOther}
+                                    onValidationChange={onValidationChange}
                                 />
                             </div>
                             <div style={{ marginTop: '20px' }}>
                                 <FormField
                                     id={FormStateKey.DMI_DESCRIPTION}
                                     type={FormFieldTypes.TEXTAREA}
-                                    titleStyle={titleStyle}
                                     description='Please describe the incidents related to mismanagement or misuse of data below.'
                                     placeholder={`Please limit your Data Management Incident to ${FORM_TEXT_AREA_MAX_LENGTH} characters.`}
                                     rows={6}
@@ -146,6 +162,8 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                         onFormChange({ [key]: value } as Partial<FormState>);
                                     }}
                                     disabled={readOnly}
+                                    validation={validation?.dmiDescription}
+                                    onValidationChange={onValidationChange}
                                 />
                             </div>
                         </>
diff --git a/src/pages/progress_reports/SubmitProgressReport.tsx b/src/pages/progress_reports/SubmitProgressReport.tsx
index 2506063c89..c11691f7a6 100644
--- a/src/pages/progress_reports/SubmitProgressReport.tsx
+++ b/src/pages/progress_reports/SubmitProgressReport.tsx
@@ -3,8 +3,6 @@ import {AxiosError} from 'axios';
 import {ProgressReport} from 'src/libs/ajax/ProgressReport';
 import {Notifications} from 'src/libs/utils';
 import {ConsentError} from 'src/types/responseTypes';
-import {validationFailed} from 'src/utils/darFormUtils';
-import {FormValidationState} from 'src/pages/dar_application/FormValidationState';
 import {DMI_INCIDENT_KEYS, FormState} from "src/pages/progress_reports/ProgressReportFormState";
 import {PublicationOrPresentation} from "src/components/publications_list/PublicationOrPresentation";
 import {
@@ -13,17 +11,18 @@ import {
   Presentation,
   Publication
 } from "src/types/model";
+import {Theme} from "src/libs/theme";
 
 interface SubmitProgressReportProps {
   readonly formState: FormState;
   readonly parentReferenceId: string;
   readonly onSuccess: (result: unknown) => void;
   readonly onCancel: () => void;
-  readonly validateForm: () => FormValidationState;
+  readonly disabled?: boolean;
 }
 
 export default function SubmitProgressReport(props: SubmitProgressReportProps) {
-  const { formState, parentReferenceId, onSuccess, onCancel, validateForm} = props;
+  const { formState, parentReferenceId, onSuccess, onCancel, disabled} = props;
 
   const getPublicationList = (formState: FormState): Publication[] => {
     const publications: PublicationOrPresentation[] = formState.publications ?? [];
@@ -94,16 +93,12 @@ export default function SubmitProgressReport(props: SubmitProgressReportProps) {
   }
 
   const submit = async () => {
-    if (validationFailed(validateForm())) {
-      Notifications.showError({text: "Form validation failed. Please check the form for errors."});
-    } else {
-      try {
-        const multiPartFormData = createMultiPartFormData(convertFormStateToDAR(formState));
-        const submittedPR = await ProgressReport.submitProgressReport(multiPartFormData, parentReferenceId);
-        onSuccess(submittedPR);
-      } catch (error: unknown) {
-        handleError('Error: Unable to submit progress report: ', error);
-      }
+    try {
+      const multiPartFormData = createMultiPartFormData(convertFormStateToDAR(formState));
+      const submittedPR = await ProgressReport.submitProgressReport(multiPartFormData, parentReferenceId);
+      onSuccess(submittedPR);
+    } catch (error: unknown) {
+      handleError('Error: Unable to submit progress report: ', error);
     }
   }
 
@@ -135,22 +130,30 @@ export default function SubmitProgressReport(props: SubmitProgressReportProps) {
     const serverError = consentError.message ?? 'Unknown error';
     Notifications.showError({text: message + serverError});
   }
+  const disabledStyle = {
+    backgroundColor: Theme.palette.disabled,
+    borderColor: Theme.palette.disabled
+  }
 
   return (
       <div className='flex flex-row' style={{justifyContent: 'flex-start'}}>
-        <button
-            type={'button'}
-            className='button button-blue'
-            style={{marginRight: '2rem', cursor: 'pointer'}}
-            data-cy='pr-submit-button'
-            onClick={submit}>Submit
-        </button>
-        <button
-            type={'button'}
+        <span>
+            <button type={'button'}
+                className='button button-blue'
+                style={{marginRight: '2rem', cursor: 'pointer', ...(disabled ? disabledStyle : {})}}
+                data-cy='pr-submit-button'
+                disabled={disabled}
+                title='Complete required form fields to enable submission.'
+                onClick={submit}>
+              Submit
+            </button>
+        </span>
+        <button type={'button'}
             className='button button-white'
             style={{cursor: 'pointer'}}
             data-cy='pr-cancel-button'
-            onClick={cancel}>Cancel this Update
+            onClick={cancel}>
+          Cancel this Update
         </button>
       </div>
   )
diff --git a/src/pages/progress_reports/SummarySection.tsx b/src/pages/progress_reports/SummarySection.tsx
index 5c87e608cb..c4d0229db4 100644
--- a/src/pages/progress_reports/SummarySection.tsx
+++ b/src/pages/progress_reports/SummarySection.tsx
@@ -1,12 +1,13 @@
 import React, { useState } from 'react';
 import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
-import { FormField, FormFieldTypes } from 'src/components/forms/forms';
+import {FormField, FormFieldTitle, FormFieldTypes} from 'src/components/forms/forms';
 import { PublicationOrPresentation } from 'src/components/publications_list/PublicationOrPresentation';
 import PublicationList from 'src/components/publications_list/PublicationList';
 import {ValidFormState, FormState, FormStateKey} from "src/pages/progress_reports/ProgressReportFormState";
 import ERACommons from 'src/components/ERACommons';
 import {DuosUser} from 'src/types/model';
 import {Location} from 'history';
+import {DarErrors, ValidationError} from "src/pages/dar_application/FormValidationState";
 
 interface SummarySectionProps {
     readonly readOnly: boolean;
@@ -15,10 +16,14 @@ interface SummarySectionProps {
     readonly eRACommonsDestination?: string;
     readonly location?: Location;
     readonly researcher: DuosUser;
+    onValidationChange?: (validationState: { key: string, validation: ValidationError }) => void;
+    validation?: DarErrors;
+    nihValid?: boolean;
+    onNihStatusUpdate?: (valid: boolean) => void;
 }
 
 export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
-    const { readOnly, formState, onFormChange, eRACommonsDestination, location, researcher } = props;
+    const { readOnly, formState, onFormChange, eRACommonsDestination, location, researcher, onValidationChange, validation, nihValid, onNihStatusUpdate } = props;
 
     const [publications, setPublications] = useState<PublicationOrPresentation[]>(formState.publications || []);
     const [presentations, setPresentations] = useState<PublicationOrPresentation[]>(formState.presentations || []);
@@ -41,15 +46,18 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                 {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
 
                 <div className='progress-report-row' data-cy='researcher-identification'>
-                    <span className={'control-label'}>{'Researcher Identification'}</span>
+                    <FormFieldTitle
+                        id='researcherIdentificationTitle'
+                        title='1.1 Researcher Identification'
+                        validation={validation?.nihEraId}>
+                    </FormFieldTitle>
                     <ERACommons
                         destination={eRACommonsDestination}
                         researcherProfile={researcher}
-                        onNihStatusUpdate={() => {
-                        }}
+                        nihValid={nihValid}
+                        onNihStatusUpdate={onNihStatusUpdate}
                         location={location}
-                        validationError={() => {
-                        }}
+                        validationError={validation?.nihEraId}
                         readOnly={readOnly}
                         header={true}
                         required={!readOnly} // In read-only mode, this is not required
@@ -60,7 +68,7 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                     <FormField
                         id={FormStateKey.PROGRESS_REPORT_SUMMARY}
                         type={FormFieldTypes.TEXTAREA}
-                        title='1.1 Summary of Progress'
+                        title='1.2 Summary of Progress'
                         description='Please summarize your research on this project since your initial request or most recent renewal in the space below. Please describe whether and how the dataset(s) was used, including referencing the dataset(s) by name in your summary.'
                         placeholder='Please provide an update here.'
                         rows={6}
@@ -70,13 +78,15 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                             onFormChange({ [key]: value } as Partial<FormState>);
                         }}
                         disabled={readOnly}
+                        validation={validation?.progressReportSummary}
+                        onValidationChange={onValidationChange}
                     />
                 </div>
                 <div className='progress-report-row'>
                     <FormField
                         id={FormStateKey.INTELLECTUAL_PROPERTY_YES_NO}
                         type={FormFieldTypes.YESNORADIOGROUP}
-                        title='1.2 Intellectual Property'
+                        title='1.3 Intellectual Property'
                         description={<span>Have you generated any <strong>intellectual property</strong> since your last renewal as a result of using the data?</span>}
                         orientation='horizontal'
                         defaultValue={formState.intellectualPropertyYesNo}
@@ -84,6 +94,8 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                             onFormChange({ [key]: value } as Partial<FormState>);
                         }}
                         disabled={readOnly}
+                        validation={validation?.intellectualPropertyYesNo}
+                        onValidationChange={onValidationChange}
                     />
                     {formState.intellectualPropertyYesNo && <FormField
                         id={FormStateKey.INTELLECTUAL_PROPERTY_SUMMARY}
@@ -97,13 +109,15 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                             onFormChange({ [key]: value } as Partial<FormState>);
                         }}
                         disabled={readOnly}
+                        validation={validation?.intellectualPropertySummary}
+                        onValidationChange={onValidationChange}
                     />}
                 </div>
                 <div className='progress-report-row'>
                     <FormField
                         id={FormStateKey.PUBLICATIONS_YES_NO}
                         type={FormFieldTypes.YESNORADIOGROUP}
-                        title='1.3 Publications'
+                        title='1.4 Publications'
                         description={<span>Have you published in any <strong>publications</strong> since your last renewal as a result of using the data?</span>}
                         orientation='horizontal'
                         defaultValue={formState.publicationsYesNo}
@@ -111,6 +125,8 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                             onFormChange({ [key]: value } as Partial<FormState>);
                         }}
                         disabled={readOnly}
+                        validation={validation?.publicationsYesNo}
+                        onValidationChange={onValidationChange}
                     />
                     {formState.publicationsYesNo && <PublicationList
                         publications={publications}
@@ -118,13 +134,14 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                         columnsToShow={['title', 'date']}
                         onPublicationChange={onPublicationChange}
                         disabled={readOnly}
+                        validation={validation}
                     />}
                 </div>
                 <div className='progress-report-row'>
                     <FormField
                         id={FormStateKey.PRESENTATIONS_YES_NO}
                         type={FormFieldTypes.YESNORADIOGROUP}
-                        title='1.4 Presentations'
+                        title='1.5 Presentations'
                         description={<span>Have you published in any <strong>presentations</strong> since your last renewal as a result of using the data?</span>}
                         orientation='horizontal'
                         defaultValue={formState.presentationsYesNo}
@@ -132,6 +149,8 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                             onFormChange({ [key]: value } as Partial<FormState>);
                         }}
                         disabled={readOnly}
+                        validation={validation?.presentationsYesNo}
+                        onValidationChange={onValidationChange}
                     />
                     {formState.presentationsYesNo && <PublicationList
                         publications={presentations}
@@ -139,6 +158,7 @@ export default function SummarySection(props: SummarySectionProps): React.JSX.El
                         columnsToShow={['title', 'date']}
                         onPublicationChange={onPresentationChange}
                         disabled={readOnly}
+                        validation={validation}
                     />}
                 </div>
             </div>
diff --git a/src/utils/darFormUtils.js b/src/utils/darFormUtils.js
index 47fae963da..0450bb4024 100644
--- a/src/utils/darFormUtils.js
+++ b/src/utils/darFormUtils.js
@@ -68,27 +68,27 @@ const isStringEmpty = (str) => {
 
 export const computeCollaboratorErrors = ({collaborator, needsApproverStatus=true}) => {
   const errors = {};
-
-  if (isStringEmpty(collaborator.name)) {
+  // if collaborator is undefined or a required field is empty, include an error
+  if (isStringEmpty(collaborator?.name)) {
     errors.name = requiredError;
   }
 
-  if (isStringEmpty(collaborator.eraCommonsId)) {
+  if (isStringEmpty(collaborator?.eraCommonsId)) {
     errors.eraCommonsId = requiredError;
   }
 
-  if (isStringEmpty(collaborator.title)) {
+  if (isStringEmpty(collaborator?.title)) {
     errors.title = requiredError;
   }
 
-  if (isStringEmpty(collaborator.email)) {
+  if (isStringEmpty(collaborator?.email)) {
     errors.email = requiredError;
   } else if (!FormValidators.EMAIL.isValid(collaborator.email)) {
     errors.email = validationError('email');
   }
 
   if (needsApproverStatus) {
-    if (isEmpty(collaborator.approverStatus)) {
+    if (isEmpty(collaborator?.approverStatus)) {
       errors.approverStatus = requiredError;
     }
   }
@@ -201,9 +201,69 @@ const calcDarErrors = (formData, datasets, dataUseTranslations, irbDocument, col
   return errors;
 };
 
-const calcPRErrors = (formData, datasets, dataUseTranslations) => {
+const calcSummaryErrors = (nihValid, errors, formData) => {
+  if (nihValid === false) {
+    errors.nihEraId = requiredError;
+  }
+  if (isEmpty(formData.progressReportSummary)) {
+    errors.progressReportSummary = requiredError;
+  }
+  if (isNil(formData.intellectualPropertyYesNo)) {
+    errors.intellectualPropertyYesNo = requiredError;
+  }
+  if (formData.intellectualPropertyYesNo && isEmpty(formData.intellectualPropertySummary)) {
+    errors.intellectualPropertySummary = requiredError;
+  }
+  if (isNil(formData.publicationsYesNo)) {
+    errors.publicationsYesNo = requiredError;
+  }
+  if (formData.publicationsYesNo && isEmpty(formData.publications)) {
+    errors.publications = requiredError;
+  }
+  if (isNil(formData.presentationsYesNo)) {
+    errors.presentationsYesNo = requiredError;
+  }
+  if (formData.presentationsYesNo && isEmpty(formData.presentations)) {
+    errors.presentations = requiredError;
+  }
+}
+
+const calcDmiErrors = (formData, errors) =>{
+  if (isNil(formData.dmiYesNo)) {
+    errors.dmiYesNo = requiredError;
+  }
+  const dmiFields = [formData.dmiAcknowledgement, formData.dmiCombination, formData.dmiFalsification,
+    formData.dmiIdentification, formData.dmiOther, formData.dmiPublication, formData.dmiSecurity, formData.dmiSharing]
+  if (formData.dmiYesNo && dmiFields.every((field) => !field)) {
+    errors.dmiAcknowledgement = requiredError;
+    errors.dmiCombination = requiredError;
+    errors.dmiFalsification = requiredError;
+    errors.dmiIdentification = requiredError;
+    errors.dmiOther = requiredError;
+    errors.dmiPublication = requiredError;
+    errors.dmiSecurity = requiredError;
+    errors.dmiSharing = requiredError;
+  }
+  if (formData.dmiYesNo && isEmpty(formData.dmiDescription)) {
+    errors.dmiDescription = requiredError;
+  }
+}
+
+const calcCloseoutErrors= (formData, errors) => {
+  if (isNil(formData.closeoutYesNo)) {
+    errors.closeoutYesNo = requiredError;
+  }
+  if (formData.closeoutYesNo && isEmpty(formData.closeoutSupplement)) {
+    errors.closeoutSupplement = requiredError;
+  }
+}
+
+const calcPRErrors = (nihValid, formData, datasets, dataUseTranslations) => {
   const errors = {};
+  calcSummaryErrors(nihValid, errors, formData);
   calcDUAErrors(formData, datasets, dataUseTranslations, errors);
+  calcDmiErrors(formData, errors);
+  calcCloseoutErrors(formData, errors);
   return errors;
 };
 
@@ -221,6 +281,36 @@ const calcDUAErrors = (formData, datasets, dataUseTranslations, errors) => {
   }
 }
 
+export const calcPublicationOrPresentationErrors = (newPublication, publicationText) => {
+  const validation = {};
+  if (isEmpty(newPublication?.title)) {
+    validation.title = requiredError;
+  }
+  if (isEmpty(newPublication?.date)) {
+    validation.date = requiredError;
+  } else if (!FormValidators.DATE.isValid(newPublication?.date)) {
+    validation.date = validationError('date');
+  }
+  if (isEmpty(newPublication?.authors)) {
+    validation.authors = requiredError;
+  }
+  if (isPublication(publicationText)) {
+    if (isEmpty(newPublication?.pubmed_id)) {
+      validation.pubmed_id = requiredError;
+    }
+    if (isEmpty(newPublication?.bibliographic_citation)) {
+      validation.bibliographic_citation = requiredError;
+    }
+  } else if (isEmpty(newPublication?.link)) {
+      validation.link = requiredError;
+  }
+  return validation;
+}
+
+export const isPublication = (publicationText) => {
+    return publicationText === 'Publication';
+}
+
 const requiredRusFields = [
   'controls',
   'population',
@@ -283,11 +373,12 @@ export const validateDARFormData = ({
 };
 
 export const validatePRFormData = (
+    nihValid,
     formData,
     datasets,
     dataUseTranslations,
     ) => {
     return {
-        darErrors: calcPRErrors(formData, datasets, dataUseTranslations)
+        darErrors: calcPRErrors(nihValid, formData, datasets, dataUseTranslations)
     };
 }
\ No newline at end of file


Reviews:
user: login: fboulnois
body: A few suggestions:
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:22:57Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:26:05Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:27:15Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:31:35Z
user: login: rushtong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T13:31:11Z
user: login: snf2ye
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T13:40:46Z
user: login: snf2ye
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T13:46:24Z
user: login: snf2ye
body: Overall looks reasonable to me - I would like to dig into the useEffects a bit more. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T13:47:33Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T14:08:52Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T14:47:33Z
user: login: otchet-broad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T15:00:39Z
user: login: snf2ye
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:27:15Z
user: login: fboulnois
body: Looks great! I think the validation is really easy to follow now. Thank you so much.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:43:41Z

Review comments:
diff_hunk: @@ -203,7 +203,55 @@ const calcDarErrors = (formData, datasets, dataUseTranslations, irbDocument, col
 
 const calcPRErrors = (formData, datasets, dataUseTranslations) => {
   const errors = {};
+  if (formData.nihValid === false) {
+    errors.nihEraId = requiredError;
+  }
+  if (isEmpty(formData.progressReportSummary)) {
+      errors.progressReportSummary = requiredError;
+  }
+  if (isNil(formData.intellectualPropertyYesNo)) {
+      errors.intellectualPropertyYesNo = requiredError;
+  }
+  if (formData.intellectualPropertyYesNo && isEmpty(formData.intellectualPropertySummary)) {
+      errors.intellectualPropertySummary = requiredError;
+  }
+  if (isNil(formData.publicationsYesNo)) {
+      errors.publicationsYesNo = requiredError;
+  }
+  if (formData.publicationsYesNo && isEmpty(formData.publications)) {
+      errors.publications = requiredError;
+  }
+  if (isNil(formData.presentationsYesNo)) {
+        errors.presentationsYesNo = requiredError;
+  }
+  if (formData.presentationsYesNo && isEmpty(formData.presentations)) {
+      errors.presentations = requiredError;
+  }
path: src/utils/darFormUtils.js
user: login: fboulnois
body: Could we pull this into `CalcSummaryErrors` and `CalcDMIErrors`?
created_at: 2025-05-28T20:09:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,18 +130,24 @@ export default function SubmitProgressReport(props: SubmitProgressReportProps) {
     const serverError = consentError.message ?? 'Unknown error';
     Notifications.showError({text: message + serverError});
   }
+  const disabledStyle = {
+    backgroundColor: Theme.palette.disabled,
+    borderColor: Theme.palette.disabled
+  }
 
   return (
       <div className='flex flex-row' style={{justifyContent: 'flex-start'}}>
-        <button
-            type={'button'}
-            className='button button-blue'
-            style={{marginRight: '2rem', cursor: 'pointer'}}
-            data-cy='pr-submit-button'
-            onClick={submit}>Submit
-        </button>
-        <button
-            type={'button'}
+        <span>
+            <button type={'button'}
+                className='button button-blue'
+                style={{marginRight: '2rem', cursor: 'pointer', ...(disabled ? disabledStyle : {})}}
+                data-cy='pr-submit-button'
+                disabled={disabled}
+                title='Complete required form fields to enable submission.'
+                onClick={submit}>Submit
+            </button>
path: src/pages/progress_reports/SubmitProgressReport.tsx
user: login: fboulnois
body: ```suggestion
                onClick={submit}>
                Submit
            </button>
```

Improve readability.
created_at: 2025-05-28T20:12:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,26 +69,26 @@ const isStringEmpty = (str) => {
 export const computeCollaboratorErrors = ({collaborator, needsApproverStatus=true}) => {
   const errors = {};
 
-  if (isStringEmpty(collaborator.name)) {
+  if (isStringEmpty(collaborator?.name)) {
path: src/utils/darFormUtils.js
user: login: fboulnois
body: I think if you add a `if(collaborator == undefined) { return }` all these `?` wouldn't be necessary.
created_at: 2025-05-28T20:14:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,17 +84,17 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
+    useEffect(() => {
+        if (!readOnlyMode) {
+            const validation = validatePRFormData(
+                formState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+            setFormValidation(validation);
+        }
+    }, [formState]);
path: src/pages/dar_application/ProgressReportApplication.tsx
user: login: fboulnois
body: I would avoid using `useEffect`, as this gets us into infinite loops.
created_at: 2025-05-28T20:16:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import {FormField, FormFieldTypes, FormValidators} from "src/components/forms/forms";
+import React from "react";
+import {ValidationError} from "src/pages/dar_application/FormValidationState";
+
+interface ApproverStatusProps {
+    readonly index: number;
+    readonly approverStatus: boolean | string | undefined;
+    readonly validation?: ValidationError;
+    readonly onValidationChange: ({key: string, validator: ValidationError}) => void;
+    readonly onChange: (value: string) => void;
+}
+export default function ApproverStatus(props: ApproverStatusProps): React.JSX.Element {
+    const {index, approverStatus, validation, onValidationChange, onChange} = props;
+
+    return (
+        <div className='row' style={{marginTop: 25}}>
+            <FormField
+                id={`${index}_collaboratorApproval`}
+                type={FormFieldTypes.RADIOGROUP}
+                name='approverStatus'
+                description={`Are you requesting permission for this member of the Internal Lab Staff to be given
+                Designated Download/Approval' status? This indication should be limited to individuals who
+                the PI designates to download data and/or share the requested data with other Internal Lab Staff
+                (ie., staff members and trainees under the direct supervision of the PI).`}
+                options={[
+                    {name: 'yes', text: 'Yes'},
+                    {name: 'no', text: 'No'}
+                ]}
+                validators={[FormValidators.REQUIRED]}
+                orientation='horizontal'
+                defaultValue={
+                    (approverStatus === true || approverStatus === 'yes') ? 'yes'
+                        : (approverStatus === false || approverStatus === 'no') ? 'no'
+                            : undefined
path: src/pages/dar_application/collaborator/ApproverStatus.tsx
user: login: fboulnois
body: Instead of using multiple embedded `? :`, I would separate the conditions out clearly.
created_at: 2025-05-28T20:17:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +33,28 @@ export interface DarErrors {
     gsoAcknowledgement?: ValidationError,
     pubAcknowledgement?: ValidationError,
     dsAcknowledgement?: ValidationError,
+    progressReportSummary?: ValidationError,
path: src/pages/dar_application/FormValidationState.tsx
user: login: fboulnois
body: (perhaps in a follow-up PR): I don't think any of the validation pieces should be optional. I think instead we should consider a model where things default validate to true, unless there's validation method which ends up validating to false.

This would avoid having to make all of these fields optional, and allow us to better type the validation.
created_at: 2025-05-28T20:20:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +18,32 @@ interface PublicationAddEditProps {
     readonly onPublicationChange: (publications: PublicationOrPresentation[]) => void;
 }
 
+interface Validation {
+    title?: ValidationError;
+    date?: ValidationError;
+    authors?: ValidationError;
+    pubmed_id?: ValidationError;
+    bibliographic_citation?: ValidationError;
+    link?: ValidationError;
+}
 export default function PublicationAddEdit(props: PublicationAddEditProps): React.JSX.Element {
     const { id, publication, publicationText, publications, closeAction, onPublicationChange } = props;
 
     const [newPublication, setNewPublication] = useState(publication);
+    const [validation, setValidation] = useState<Validation>({});
+
+    useEffect(() => {
+        setValidation(calcPublicationOrPresentationErrors(newPublication, publicationText));
+    }, [newPublication]);
path: src/components/publications_list/PublicationAddEdit.tsx
user: login: fboulnois
body: I think we should avoid using a useEffect here as well. When `newPublication` is created, do the validation at that stage.
created_at: 2025-05-28T20:22:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,12 +18,35 @@ interface CollaboratorAddEditProps {
     readonly collaborators: Collaborator[];
     readonly closeAction: () => void;
     readonly onCollaboratorChange: (collaborators: Collaborator[]) => void;
+    readonly showApproverStatus?: boolean;
 }
 
+interface Validation {
+    name?: ValidationError;
+    eraCommonsId?: ValidationError;
+    title?: ValidationError;
+    email?: ValidationError;
+    approverStatus?: ValidationError;
+}
 export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): React.JSX.Element {
-    const { id, collaborator, collaboratorText, collaborators, closeAction, onCollaboratorChange } = props;
+    const { id, collaborator, collaboratorText, collaborators, closeAction, onCollaboratorChange, showApproverStatus } = props;
 
     const [newCollaborator, setNewCollaborator] = useState(collaborator);
+    const [validation, setValidation] = useState<Validation>({});
+    const accountLabel = nihAccountLabel();
+
+    useEffect(() => {
+        setValidation(computeCollaboratorErrors({collaborator: newCollaborator, needsApproverStatus: showApproverStatus}));
+    }, [setNewCollaborator]);
path: src/components/collaborator_list/CollaboratorAddEdit.tsx
user: login: fboulnois
body: ditto here for collaborators.
created_at: 2025-05-28T20:22:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,17 +84,17 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
+    useEffect(() => {
+        if (!readOnlyMode) {
+            const validation = validatePRFormData(
+                formState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+            setFormValidation(validation);
+        }
+    }, [formState]);
path: src/pages/dar_application/ProgressReportApplication.tsx
user: login: rjohanek
body: if I don't use useEffect, but want to update anytime the form is updated, what would I use?
created_at: 2025-05-28T20:26:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +33,28 @@ export interface DarErrors {
     gsoAcknowledgement?: ValidationError,
     pubAcknowledgement?: ValidationError,
     dsAcknowledgement?: ValidationError,
+    progressReportSummary?: ValidationError,
path: src/pages/dar_application/FormValidationState.tsx
user: login: rjohanek
body: that makes sense, I was just following the example of the DAR Application. If we change it for PR, we should change it for DARs too
created_at: 2025-05-28T20:27:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +18,32 @@ interface PublicationAddEditProps {
     readonly onPublicationChange: (publications: PublicationOrPresentation[]) => void;
 }
 
+interface Validation {
+    title?: ValidationError;
+    date?: ValidationError;
+    authors?: ValidationError;
+    pubmed_id?: ValidationError;
+    bibliographic_citation?: ValidationError;
+    link?: ValidationError;
+}
 export default function PublicationAddEdit(props: PublicationAddEditProps): React.JSX.Element {
     const { id, publication, publicationText, publications, closeAction, onPublicationChange } = props;
 
     const [newPublication, setNewPublication] = useState(publication);
+    const [validation, setValidation] = useState<Validation>({});
+
+    useEffect(() => {
+        setValidation(calcPublicationOrPresentationErrors(newPublication, publicationText));
+    }, [newPublication]);
path: src/components/publications_list/PublicationAddEdit.tsx
user: login: rjohanek
body: what do you mean when newPublication is created? are you suggesting just doing the validation on attempt to submit/add publication instead of on form update? if so, I think we wanted to take a more proactive approach with validation and not even allow an attempt to submit if it is not valid
created_at: 2025-05-28T20:31:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,17 +84,17 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
+    useEffect(() => {
+        if (!readOnlyMode) {
+            const validation = validatePRFormData(
+                formState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+            setFormValidation(validation);
+        }
+    }, [formState]);
path: src/pages/dar_application/ProgressReportApplication.tsx
user: login: rushtong
body: Have we considered [`useFormStatus`](https://react.dev/reference/react-dom/hooks/useFormStatus) as a potential solution?
created_at: 2025-05-29T13:31:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,17 +85,18 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
+    useEffect(() => {
+        if (!readOnlyMode) {
+            const validation = validatePRFormData(
+                nihValid,
+                formState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+            setFormValidation(validation);
+        }
+    }, [formState, nihValid]);
path: src/pages/dar_application/ProgressReportApplication.tsx
user: login: snf2ye
body: What happens if you add the missing dependencies as suggested by the linter? 
created_at: 2025-05-29T13:40:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +284,32 @@ const calcDUAErrors = (formData, datasets, dataUseTranslations, errors) => {
   }
 }
 
+export const calcPublicationOrPresentationErrors = (newPublication, publicationText) => {
+  const validation = {};
+  if (isEmpty(newPublication?.title)) {
+    validation.title = requiredError;
+  }
+  if (isEmpty(newPublication?.date)) {
+    validation.date = requiredError;
+  } else if (!FormValidators.DATE.isValid(newPublication?.date)) {
+    validation.date = validationError('date');
+  }
+  if (isEmpty(newPublication?.authors)) {
+    validation.authors = requiredError;
+  }
+  if (publicationText === 'Publication') {
path: src/utils/darFormUtils.js
user: login: snf2ye
body: Since it is comparing a hard-coded string, it would be nice to pull this check into a shared utility method. This will help us identify all of the locations where we're doing this sort of check when we separate out the publication/presentation components
created_at: 2025-05-29T13:46:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +18,32 @@ interface PublicationAddEditProps {
     readonly onPublicationChange: (publications: PublicationOrPresentation[]) => void;
 }
 
+interface Validation {
+    title?: ValidationError;
+    date?: ValidationError;
+    authors?: ValidationError;
+    pubmed_id?: ValidationError;
+    bibliographic_citation?: ValidationError;
+    link?: ValidationError;
+}
 export default function PublicationAddEdit(props: PublicationAddEditProps): React.JSX.Element {
     const { id, publication, publicationText, publications, closeAction, onPublicationChange } = props;
 
     const [newPublication, setNewPublication] = useState(publication);
+    const [validation, setValidation] = useState<Validation>({});
+
+    useEffect(() => {
+        setValidation(calcPublicationOrPresentationErrors(newPublication, publicationText));
+    }, [newPublication]);
path: src/components/publications_list/PublicationAddEdit.tsx
user: login: fboulnois
body: We could do it `onChange` instead of on form update / continuously using `useEffect`.
created_at: 2025-05-29T14:08:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,17 +85,18 @@ export const ProgressReportApplication = ({ dar, datasets, readOnlyMode = true,
         onFormChange({ datasetIds: selectedIds });
     }, [selectedDatasets]);
 
-    const validateForm = () => {
-        const validation = validatePRFormData(
-            formState,
-            selectedDatasets,
-            dataUseTranslations
-        );
-        setFormValidation(validation);
-        return validation;
-    }
-
-    const formValidationChange = useCallback(({key, validation}: { key: string; validation: ValidationError }) => {
+    useEffect(() => {
+        if (!readOnlyMode) {
+            const validation = validatePRFormData(
+                nihValid,
+                formState,
+                selectedDatasets,
+                dataUseTranslations
+            );
+            setFormValidation(validation);
+        }
+    }, [formState, nihValid]);
path: src/pages/dar_application/ProgressReportApplication.tsx
user: login: rjohanek
body: I added them and it doesn't seem anything has changed
created_at: 2025-05-29T14:47:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +33,28 @@ export interface DarErrors {
     gsoAcknowledgement?: ValidationError,
     pubAcknowledgement?: ValidationError,
     dsAcknowledgement?: ValidationError,
+    progressReportSummary?: ValidationError,
path: src/pages/dar_application/FormValidationState.tsx
user: login: otchet-broad
body: Agree with both of you on this.  It's probably also time to upgrade `forms.jsx` to typescript as well.  It's underlying all of this.
created_at: 2025-05-29T15:00:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,12 +18,30 @@ interface CollaboratorAddEditProps {
     readonly collaborators: Collaborator[];
     readonly closeAction: () => void;
     readonly onCollaboratorChange: (collaborators: Collaborator[]) => void;
+    readonly showApproverStatus?: boolean;
 }
 
+interface Validation {
+    name?: ValidationError;
+    eraCommonsId?: ValidationError;
+    title?: ValidationError;
+    email?: ValidationError;
+    approverStatus?: ValidationError;
+}
 export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): React.JSX.Element {
path: src/components/collaborator_list/CollaboratorAddEdit.tsx
user: login: fboulnois
body: ```suggestion
}

export default function CollaboratorAddEdit(props: CollaboratorAddEditProps): React.JSX.Element {
```

tiny nit
created_at: 2025-05-30T17:40:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-30T18:01:37Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DataBiosphere_duos-ui&pullRequest=2887) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DataBiosphere_duos-ui&pullRequest=2887&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DataBiosphere_duos-ui&pullRequest=2887&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DataBiosphere_duos-ui&pullRequest=2887&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_duos-ui&pullRequest=2887&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_duos-ui&pullRequest=2887&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DataBiosphere_duos-ui&pullRequest=2887)


author_association: NONE