Repository: DataBiosphere/duos-ui
Pull request #2870: [DT-1629] Progress Report summary section
Author: login: fboulnois
Created: 2025-05-15T19:27:07Z | Merged: 2025-05-16T15:43:04Z

### Addresses

https://broadworkbench.atlassian.net/browse/DT-1629

### Summary

- Adapts the collaborator component for publications to move quickly (I checked with Victoria and Jonathan beforehand and I added https://broadworkbench.atlassian.net/browse/DT-1671 as a follow-up)
- Add the Progress Report summary section to the form
- Renumbers the section headers

![Screenshot 2025-05-15 at 15-42-17 Broad Data Use Oversight System](https://github.com/user-attachments/assets/5d65d8cb-ea65-4a98-8ad6-87bf291593a0)

----
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/ProgressReports/collaborator_changes.spec.tsx b/cypress/component/ProgressReports/collaborator_changes.spec.tsx
index b4d64a9633..5ab937c459 100644
--- a/cypress/component/ProgressReports/collaborator_changes.spec.tsx
+++ b/cypress/component/ProgressReports/collaborator_changes.spec.tsx
@@ -44,10 +44,10 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('renders the component correctly', () => {
     cy.get('[data-cy=dar-closeout]').should('exist');
-    cy.contains('Step 2: Add or Remove Collaborators').should('be.visible');
-    cy.contains('2.1 Internal Lab Staff').should('be.visible');
-    cy.contains('2.2 Internal Collaborators').should('be.visible');
-    cy.contains('2.3 External Collaborators').should('be.visible');
+    cy.contains('Step 3: Add or Remove Collaborators').should('be.visible');
+    cy.contains('3.1 Internal Lab Staff').should('be.visible');
+    cy.contains('3.2 Internal Collaborators').should('be.visible');
+    cy.contains('3.3 External Collaborators').should('be.visible');
   });
 
   it('renders all three collaborator sections', () => {
diff --git a/cypress/component/ProgressReports/dar_closeout.spec.tsx b/cypress/component/ProgressReports/dar_closeout.spec.tsx
index 40c8faec4f..80a90e7fcc 100644
--- a/cypress/component/ProgressReports/dar_closeout.spec.tsx
+++ b/cypress/component/ProgressReports/dar_closeout.spec.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import { mount } from 'cypress/react';
 import DarCloseout from 'src/pages/progress_reports/DarCloseout';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 
 describe('DAR Closeout - Component Tests', () => {
   let onFormChangeSpy: () => void;
@@ -24,8 +25,8 @@ describe('DAR Closeout - Component Tests', () => {
 
   it('renders the component correctly', () => {
     cy.get('[data-cy=dar-closeout]').should('exist');
-    cy.contains('Step 4: DAR Closeout').should('be.visible');
-    cy.contains('4.1 Closeouts').should('be.visible');
+    cy.contains('Step 5: DAR Closeout').should('be.visible');
+    cy.contains('5.1 Closeouts').should('be.visible');
     cy.contains('If you are ready to finish work on this project').should('be.visible');
   });
 
@@ -76,9 +77,9 @@ describe('DAR Closeout - Component Tests', () => {
   it('enforces character limit on other closeout reason', () => {
     mountComponent({ closeoutOther: true });
     
-    // Generate a string longer than the 2200 character limit
-    const longText = 'A'.repeat(2300);
-    const expectedText = longText.substring(0, 2200);
+    // Generate a string longer than the max length character limit
+    const longText = 'A'.repeat(FORM_TEXT_AREA_MAX_LENGTH + 100);
+    const expectedText = longText.substring(0, FORM_TEXT_AREA_MAX_LENGTH);
     
     cy.get('#closeoutOtherContext').type(longText, { delay: 0 });
     cy.get('#closeoutOtherContext').should('have.value', expectedText);
diff --git a/cypress/component/ProgressReports/data_management_incident.spec.tsx b/cypress/component/ProgressReports/data_management_incident.spec.tsx
index bea801189d..a475bc3967 100644
--- a/cypress/component/ProgressReports/data_management_incident.spec.tsx
+++ b/cypress/component/ProgressReports/data_management_incident.spec.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import { mount } from 'cypress/react';
 import DataManagementIncident from 'src/pages/progress_reports/DataManagementIncident';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 
 describe('Data Management Incident - Component Tests', () => {
   let onFormChangeSpy: () => void;
@@ -24,8 +25,8 @@ describe('Data Management Incident - Component Tests', () => {
 
   it('renders the component correctly', () => {
     cy.get('[data-cy=data-management-incident]').should('exist');
-    cy.contains('Step 3: Data Management Incident').should('be.visible');
-    cy.contains('3.1 Data Management Incident').should('be.visible');
+    cy.contains('Step 4: Data Management Incident').should('be.visible');
+    cy.contains('4.1 Data Management Incident').should('be.visible');
     cy.contains('Have there been any incidents related to mismanagement or misuse of data?').should('be.visible');
   });
 
@@ -85,9 +86,9 @@ describe('Data Management Incident - Component Tests', () => {
   it('enforces character limit on incident description', () => {
     mountComponent({ dmiYesNo: true });
     
-    // Generate a string longer than the 2200 character limit
-    const longText = 'A'.repeat(2300);
-    const expectedText = longText.substring(0, 2200);
+    // Generate a string longer than the max length character limit
+    const longText = 'A'.repeat(FORM_TEXT_AREA_MAX_LENGTH + 100);
+    const expectedText = longText.substring(0, FORM_TEXT_AREA_MAX_LENGTH);
     
     cy.get('#dmiDescription').type(longText, { delay: 0 });
     cy.get('#dmiDescription').should('have.value', expectedText);
diff --git a/cypress/component/ProgressReports/summary_section.spec.tsx b/cypress/component/ProgressReports/summary_section.spec.tsx
new file mode 100644
index 0000000000..79e2d44c78
--- /dev/null
+++ b/cypress/component/ProgressReports/summary_section.spec.tsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import { mount } from 'cypress/react';
+import SummarySection from 'src/pages/progress_reports/SummarySection';
+import { Publication } from 'src/components/publications_list/Publication';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
+
+describe('Summary Section - Component Tests', () => {
+    let onFormChangeSpy: () => void;
+
+    const initialPublications: Publication[] = [
+        {
+            title: 'Test Publication 1',
+            date: '2022-01-01',
+            bibliographic_citation: 'Citation 1',
+            did_cite: true,
+            pubmed_id: '12345',
+            authors: 'Author 1, Author 2',
+            dataset_citation: 'Dataset Citation 1',
+            link: 'http://example.com/1'
+        },
+        {
+            title: 'Test Publication 2',
+            date: '2022-02-01',
+            bibliographic_citation: 'Citation 2',
+            did_cite: true,
+            pubmed_id: '67890',
+            authors: 'Author 3, Author 4',
+            dataset_citation: 'Dataset Citation 2',
+            link: 'http://example.com/2'
+        }
+    ];
+
+    const mountComponent = (customState = {}) => {
+        const formState = { ...customState };
+
+        const props = {
+            readOnly: false,
+            formState,
+            onFormChange: onFormChangeSpy
+        };
+
+        return mount(<SummarySection {...props} />);
+    };
+
+    beforeEach(() => {
+        onFormChangeSpy = cy.stub().as('formChangeStub');
+        mountComponent();
+    });
+
+    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');
+    });
+
+    it('renders in read-only mode correctly', () => {
+        const props = {
+            readOnly: true,
+            formState: {},
+            onFormChange: onFormChangeSpy
+        };
+
+        mount(<SummarySection {...props} />);
+        cy.contains('Review a Progress Report').should('be.visible');
+    });
+
+    it('displays the correct descriptions for each section', () => {
+        cy.contains('Please summarize your research on this project since your initial request or most recent renewal').should('be.visible');
+        cy.contains('Have you generated any intellectual property since your last renewal').should('be.visible');
+        cy.contains('Have you published in any publications since your last renewal').should('be.visible');
+        cy.contains('Have you published in any presentations since your last renewal').should('be.visible');
+    });
+
+    it('handles summary text input', () => {
+        const testSummary = 'This is a test summary of my research progress.';
+        cy.get('#progressReportSummary').type(testSummary);
+        cy.get('#progressReportSummary').should('have.value', testSummary);
+
+        cy.get('@formChangeStub').should('have.been.called');
+    });
+
+    it('enforces character limit on summary text', () => {
+        const longText = 'A'.repeat(FORM_TEXT_AREA_MAX_LENGTH + 100);
+        const expectedText = longText.substring(0, FORM_TEXT_AREA_MAX_LENGTH);
+
+        cy.get('#progressReportSummary').type(longText, { delay: 0 });
+        cy.get('#progressReportSummary').should('have.value', expectedText);
+    });
+
+    it('handles intellectual property radio buttons', () => {
+        cy.contains('label', 'Yes').find('input[type="radio"]').first().click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { intellectualPropertyYesNo: true });
+
+        cy.contains('label', 'No').find('input[type="radio"]').first().click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { intellectualPropertyYesNo: false });
+    });
+
+    it('shows intellectual property details form when "Yes" is selected', () => {
+        mountComponent({ intellectualPropertyYesNo: true });
+        cy.get('#intellectualPropertySummary').should('exist');
+
+        const testDetails = 'Details about intellectual property.';
+        cy.get('#intellectualPropertySummary').type(testDetails);
+        cy.get('#intellectualPropertySummary').should('have.value', testDetails);
+
+        cy.get('@formChangeStub').should('have.been.called');
+    });
+
+    it('handles publications radio buttons', () => {
+        cy.get('#publicationsYesNo').parent().contains('label', 'Yes').find('input[type="radio"]').click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { publicationsYesNo: true });
+
+        cy.get('#publicationsYesNo').parent().contains('label', 'No').find('input[type="radio"]').click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { publicationsYesNo: false });
+    });
+
+    it('shows publications list when "Yes" is selected', () => {
+        mountComponent({ publicationsYesNo: true });
+        cy.contains('Add Publication').should('exist');
+    });
+
+    it('handles presentations radio buttons', () => {
+        cy.get('#presentationsYesNo').parent().contains('label', 'Yes').find('input[type="radio"]').click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { presentationsYesNo: true });
+
+        cy.get('#presentationsYesNo').parent().contains('label', 'No').find('input[type="radio"]').click({force: true});
+        cy.get('@formChangeStub').should('have.been.calledWith', { presentationsYesNo: false });
+    });
+
+    it('shows presentations list when "Yes" is selected', () => {
+        mountComponent({ presentationsYesNo: true });
+        cy.contains('Add Presentation').should('exist');
+    });
+
+    it('displays preloaded publications', () => {
+        mountComponent({ publicationsYesNo: true, publications: initialPublications });
+
+        cy.contains('Test Publication 1').should('be.visible');
+        cy.contains('Test Publication 2').should('be.visible');
+        cy.contains('2022-01-01').should('be.visible');
+        cy.contains('2022-02-01').should('be.visible');
+    });
+
+    it('displays preloaded presentations', () => {
+        mountComponent({ presentationsYesNo: true, presentations: initialPublications });
+
+        cy.contains('Test Publication 1').should('be.visible');
+        cy.contains('Test Publication 2').should('be.visible');
+        cy.contains('2022-01-01').should('be.visible');
+        cy.contains('2022-02-01').should('be.visible');
+    });
+});
diff --git a/src/components/forms/formConstants.ts b/src/components/forms/formConstants.ts
new file mode 100644
index 0000000000..a9dd6aeaa5
--- /dev/null
+++ b/src/components/forms/formConstants.ts
@@ -0,0 +1,2 @@
+// Form constants for the application
+export const FORM_TEXT_AREA_MAX_LENGTH = 2200;
diff --git a/src/components/publications_list/Publication.tsx b/src/components/publications_list/Publication.tsx
new file mode 100644
index 0000000000..9acf0c2afe
--- /dev/null
+++ b/src/components/publications_list/Publication.tsx
@@ -0,0 +1,10 @@
+export interface Publication {
+    title: string;
+    date: string;
+    bibliographic_citation: string;
+    did_cite: boolean;
+    pubmed_id: string;
+    authors: string;
+    dataset_citation: string;
+    link: string;
+}
diff --git a/src/components/publications_list/PublicationAddEdit.tsx b/src/components/publications_list/PublicationAddEdit.tsx
new file mode 100644
index 0000000000..b23d024762
--- /dev/null
+++ b/src/components/publications_list/PublicationAddEdit.tsx
@@ -0,0 +1,176 @@
+import React, { useState } from 'react';
+import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms';
+import { Publication } from 'src/components/publications_list/Publication';
+
+interface FormFieldChange {
+    key: string;
+    value: string;
+}
+
+interface PublicationAddEditProps {
+    readonly id: number;
+    publication?: Publication;
+    readonly publicationText: string;
+    readonly publications: Publication[];
+    readonly closeAction: () => void;
+    readonly onPublicationChange: (publications: Publication[]) => void;
+}
+
+export default function PublicationAddEdit(props: PublicationAddEditProps): React.JSX.Element {
+    const { id, publication, publicationText, publications, closeAction, onPublicationChange } = props;
+
+    const [newPublication, setNewPublication] = useState(publication);
+
+    return (
+        <div className='form-group row no-margin'>
+            <div className='col-lg-12 col-md-12 col-sm-12 col-xs-12 collaborator-form-card'>
+                <div className='row'>
+                    <h2>{publication === undefined ? `New ${publicationText} Information` : `Edit ${publication.title} Information`}</h2>
+                    <FormField
+                        id='title'
+                        title={`${publicationText} Title`}
+                        defaultValue={publication?.title}
+                        placeholder='Title'
+                        validators={[FormValidators.REQUIRED]}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            const setPublication = {
+                                ...newPublication,
+                                [key]: value
+                            } as Publication;
+                            setNewPublication(setPublication);
+                        }}
+                    />
+                    <FormField
+                        id='date'
+                        title={`${publicationText} Date`}
+                        defaultValue={publication?.date}
+                        placeholder='Date'
+                        validators={[FormValidators.REQUIRED, FormValidators.DATE]}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            const setPublication = {
+                                ...newPublication,
+                                [key]: value
+                            } as Publication;
+                            setNewPublication(setPublication);
+                        }}
+                    />
+                    <FormField
+                        id='authors'
+                        title={`${publicationText} Authors`}
+                        defaultValue={publication?.authors}
+                        placeholder='Authors'
+                        validators={[FormValidators.REQUIRED]}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            const setPublication = {
+                                ...newPublication,
+                                [key]: value
+                            } as Publication;
+                            setNewPublication(setPublication);
+                        }}
+                    />
+                    {publicationText === 'Publication' ?
+                        <>
+                            <FormField
+                                id='pubmed_id'
+                                title={`${publicationText} PubMed ID`}
+                                defaultValue={publication?.pubmed_id}
+                                placeholder='PubMed ID'
+                                validators={[FormValidators.REQUIRED]}
+                                onChange={({ key, value }: FormFieldChange) => {
+                                    const setPublication = {
+                                        ...newPublication,
+                                        [key]: value
+                                    } as Publication;
+                                    setNewPublication(setPublication);
+                                }}
+                            />
+                            <FormField
+                                id='bibliographic_citation'
+                                title={`${publicationText} Bibliographic Citation`}
+                                defaultValue={publication?.bibliographic_citation}
+                                placeholder='Date'
+                                validators={[FormValidators.REQUIRED]}
+                                onChange={({ key, value }: FormFieldChange) => {
+                                    const setPublication = {
+                                        ...newPublication,
+                                        [key]: value
+                                    } as Publication;
+                                    setNewPublication(setPublication);
+                                }}
+                            />
+                        </> : // otherwise it's a presentation
+                        <FormField
+                            id='link'
+                            title={`${publicationText} Link`}
+                            defaultValue={publication?.link}
+                            placeholder='Link'
+                            validators={[FormValidators.REQUIRED]}
+                            onChange={({ key, value }: FormFieldChange) => {
+                                const setPublication = {
+                                    ...newPublication,
+                                    [key]: value
+                                } as Publication;
+                                setNewPublication(setPublication);
+                            }}
+                        />
+                    }
+                    <FormField
+                        id='dataset_citation'
+                        title={`Dataset citation used in this publication`}
+                        defaultValue={publication?.dataset_citation}
+                        placeholder='Dataset Citation'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            const setPublication = {
+                                ...newPublication,
+                                [key]: value
+                            } as Publication;
+                            setNewPublication(setPublication);
+                        }}
+                    />
+                    <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 Publication;
+                            setNewPublication(setPublication);
+                        }}
+                    />
+                </div>
+                <div className='row' style={{ marginTop: 20 }}>
+                    {/* add/save button */}
+                    <div
+                        className='collaborator-form-add-save-button f-left btn'
+                        role='button'
+                        onClick={() => {
+                            if (id < 0) {
+                                onPublicationChange([...publications, newPublication]);
+                                setNewPublication(undefined);
+                            } else if (newPublication !== undefined) {
+                                const publicationsCopy = [...publications];
+                                publicationsCopy[id] = newPublication;
+                                onPublicationChange(publicationsCopy);
+                                setNewPublication(undefined);
+                            }
+                            closeAction();
+                        }}
+                    >
+                        {publication === undefined ? 'Add' : 'Save'}
+                    </div>
+                    {/* cancel button */}
+                    <div
+                        className='collaborator-form-cancel-button f-left btn'
+                        role='button'
+                        onClick={closeAction}
+                    >
+                        Cancel
+                    </div>
+                </div>
+            </div>
+        </div>
+    )
+}
diff --git a/src/components/publications_list/PublicationDelete.css b/src/components/publications_list/PublicationDelete.css
new file mode 100644
index 0000000000..358ef0588b
--- /dev/null
+++ b/src/components/publications_list/PublicationDelete.css
@@ -0,0 +1,54 @@
+.delete-modal {
+  inset: 30% 42%;
+  padding: 2%;
+  display: flex;
+  justify-content: left;
+  position: absolute;
+  height: 208px;
+  width: 480px;
+  border-radius: 4px;
+  background-color: #FFFFFF;
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.5);
+}
+
+.delete-modal-header {
+  height: 24px;
+  width: 250px;
+  color: #333F52;
+  font-family: Montserrat;
+  font-size: 20px;
+  font-weight: bold;
+  line-height: 24px;
+  margin-bottom: 4%;
+  align-items: left;
+}
+
+.delete-modal-title {
+  height: 28px;
+  width: 365px;
+  color: #333F52;
+  font-family: Montserrat;
+  font-size: 14px;
+  letter-spacing: 0;
+  line-height: 24px;
+  align-items: left;
+}
+
+.delete-modal-message {
+  height: 48px;
+  width: 365px;
+  color: #333F52;
+  font-family: Montserrat;
+  font-size: 14px;
+  font-style: italic;
+  letter-spacing: 0;
+  line-height: 24px;
+  align-items: left;
+}
+
+.delete-modal-actions {
+  display: flex;
+  justify-content: left;
+  height: 41px;
+  width: 89px;
+}
diff --git a/src/components/publications_list/PublicationDelete.tsx b/src/components/publications_list/PublicationDelete.tsx
new file mode 100644
index 0000000000..502a1e443d
--- /dev/null
+++ b/src/components/publications_list/PublicationDelete.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import ModalWrapper from 'src/components/collaborator_list/ModalWrapper';
+import './PublicationDelete.css';
+import CloseIconComponent from '../CloseIconComponent';
+import { styled } from '@mui/material/styles';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+
+interface PublicationDeleteProps {
+    readonly publicationName: string;
+    readonly showDelete: boolean;
+    readonly confirmAction: () => void;
+    readonly closeAction: () => void;
+}
+
+export default function PublicationDelete(props: PublicationDeleteProps): React.JSX.Element {
+    const { publicationName, showDelete, confirmAction, closeAction } = props;
+
+    const duosBlue = '#0948B7';
+    const duosBlueHover = 'rgb(9,72,183)';
+
+    const SecondaryButton = styled(Button)(() => ({
+        fontFamily: 'Montserrat, sans-serif',
+        color: duosBlue,
+        backgroundColor: 'white',
+        borderRadius: '4px',
+        fontSize: '1.45rem',
+        borderColor: duosBlue,
+        '&:hover': {
+            borderColor: duosBlueHover,
+            color: duosBlueHover,
+        },
+    }));
+
+    const PrimaryButton = styled(Button)(({ theme }) => ({
+        fontFamily: 'Montserrat, sans-serif',
+        color: theme.palette.getContrastText(duosBlue),
+        backgroundColor: duosBlue,
+        borderRadius: '4px',
+        fontSize: '1.45rem',
+        '&:hover': {
+            backgroundColor: duosBlueHover,
+        },
+    }));
+
+    return (
+        <ModalWrapper
+            isOpen={showDelete}
+            onRequestClose={closeAction}
+            shouldCloseOnEsc={true}
+            shouldCloseOnOverlayClick={true}
+            className={'delete-modal'}
+        >
+            <div>
+                <CloseIconComponent closeFn={closeAction} />
+                <div className={'delete-modal-header'}>Delete Publication</div>
+                <div className={'delete-modal-title'}>Are you sure you want to delete <strong>{publicationName}</strong>?</div>
+                <div className={'delete-modal-message'}><i>This action is permanent and cannot be undone.</i></div>
+                <div className={'delete-modal-actions'}>
+                    <Stack spacing={2} direction={'row'}>
+                        <PrimaryButton variant={'contained'} className={'delete-modal-primary-button'} onClick={confirmAction}>
+                            Delete
+                        </PrimaryButton>
+                        <SecondaryButton variant={'outlined'} className={'delete-modal-secondary-button'} onClick={closeAction}>
+                            Cancel
+                        </SecondaryButton>
+                    </Stack>
+                </div>
+            </div>
+        </ModalWrapper>
+    );
+}
diff --git a/src/components/publications_list/PublicationList.tsx b/src/components/publications_list/PublicationList.tsx
new file mode 100644
index 0000000000..23732a268e
--- /dev/null
+++ b/src/components/publications_list/PublicationList.tsx
@@ -0,0 +1,78 @@
+import React, { useState } from 'react';
+import PublicationAddEdit from './PublicationAddEdit';
+import PublicationRow from './PublicationRow';
+import { Publication } from './Publication';
+
+interface PublicationListProps {
+    publications: Publication[];
+    readonly publicationText: string;
+    readonly columnsToShow?: string[];
+    readonly onPublicationChange: (publications: Publication[]) => void;
+    readonly disabled?: boolean;
+}
+
+export default function PublicationList(props: PublicationListProps): React.JSX.Element {
+    const { publications, publicationText, columnsToShow = [], onPublicationChange, disabled = false } = props;
+
+    const [showAddEdit, setShowAddEdit] = useState(false);
+    const [editState, setEditState] = useState(publications.map(() => false));
+
+    const toggleEditState = (index: number) => {
+        const editStateCopy = [...editState];
+        editStateCopy[index] = !editStateCopy[index];
+        setEditState(editStateCopy);
+    }
+
+    const handleDeletePublication = (index: number) => {
+        const updatedPublications = publications.filter((_, i) => i !== index);
+        onPublicationChange(updatedPublications);
+    }
+
+    return (
+        <div className="publication-list-component">
+            <div className="row no-margin">
+                <button
+                    id={`add-${publicationText}-btn`}
+                    type="button"
+                    className="button button-white"
+                    style={{
+                        marginTop: 25,
+                        marginBottom: 5,
+                        ...(disabled ? { cursor: 'not-allowed' } : {}),
+                    }}
+                    onClick={() => !disabled && setShowAddEdit(true) }
+                    disabled={disabled}
+                >
+                    Add {publicationText}
+                </button>
+                {showAddEdit && (
+                    <PublicationAddEdit
+                        id={-1}
+                        publicationText={publicationText}
+                        publications={publications}
+                        closeAction={() => setShowAddEdit(false)}
+                        onPublicationChange={onPublicationChange}
+                    />
+                )}
+            </div>
+            <div className="form-group row no-margin">
+                {publications.map((publication: Publication, index: number) => {
+                    return <PublicationRow
+                        key={index}
+                        id={index}
+                        editMode={editState[index]}
+                        publication={publication}
+                        publicationText={publicationText}
+                        publications={publications}
+                        columnsToShow={columnsToShow}
+                        editAction={() => toggleEditState(index)}
+                        deleteAction={() => { handleDeletePublication(index); }}
+                        closeAction={() => { toggleEditState(index); setShowAddEdit(false); }}
+                        onPublicationChange={onPublicationChange}
+                        disabled={disabled}
+                    />
+                })}
+            </div>
+        </div>
+    );
+}
diff --git a/src/components/publications_list/PublicationRow.tsx b/src/components/publications_list/PublicationRow.tsx
new file mode 100644
index 0000000000..f02cffb71b
--- /dev/null
+++ b/src/components/publications_list/PublicationRow.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Publication } from './Publication';
+import PublicationAddEdit from './PublicationAddEdit';
+import PublicationSummary from './PublicationSummary';
+
+interface PublicationRowProps {
+    readonly id: number;
+    readonly editMode: boolean;
+    publication: Publication;
+    readonly publicationText: string;
+    readonly publications: Publication[];
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly closeAction: () => void;
+    readonly onPublicationChange: (publications: Publication[]) => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationRow(props: PublicationRowProps): React.JSX.Element {
+    const { id, editMode, publication, publicationText, publications, columnsToShow, editAction, deleteAction, closeAction, onPublicationChange, disabled } = props;
+
+    return (
+        <div>
+            {editMode && (
+                <PublicationAddEdit
+                    id={id}
+                    publication={publication}
+                    publicationText={publicationText}
+                    publications={publications}
+                    closeAction={closeAction}
+                    onPublicationChange={onPublicationChange}
+                />)}
+            {!editMode && (
+                <PublicationSummary
+                    publication={publication}
+                    columnsToShow={columnsToShow}
+                    editAction={editAction}
+                    deleteAction={deleteAction}
+                    disabled={disabled}
+                />)}
+        </div>
+    );
+}
diff --git a/src/components/publications_list/PublicationSummary.tsx b/src/components/publications_list/PublicationSummary.tsx
new file mode 100644
index 0000000000..03307345cc
--- /dev/null
+++ b/src/components/publications_list/PublicationSummary.tsx
@@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
+                    style={{ marginLeft: 10, marginRight: 10, ...buttonStyle }}
+                    onClick={() => !disabled && editAction()}
+                >
+                    <span
+                        className='glyphicon glyphicon-pencil caret-margin collaborator-edit-icon'
+                        aria-hidden='true'
+                        data-tip='Edit dataset'
+                        data-for='tip_edit'
+                    ></span>
+                    <span style={{ marginLeft: '1rem' }}></span>
+                </a>
+            </div>
+            {/* delete button */}
+            <a
+                style={{ marginLeft: 10, ...buttonStyle }}
+                onClick={() => !disabled && setShowDeleteModal(true) }
+            >
+                <span
+                    className='glyphicon glyphicon-trash publication-delete-icon'
+                    aria-hidden='true'
+                    data-tip='Delete dataset'
+                    data-for='tip_delete'
+                ></span>
+                <span style={{ marginLeft: '1rem' }}></span>
+            </a>
+            {/* delete modal */}
+            <PublicationDelete
+                publicationName={publication.title}
+                showDelete={showDeleteModal}
+                confirmAction={() => { deleteAction(); setShowDeleteModal(false); }}
+                closeAction={() => setShowDeleteModal(false)}
+            />
+        </div>
+    );
+}
diff --git a/src/pages/dar_application/DataAccessRequest.jsx b/src/pages/dar_application/DataAccessRequest.jsx
index ba1c98263e..3dbd476ee2 100644
--- a/src/pages/dar_application/DataAccessRequest.jsx
+++ b/src/pages/dar_application/DataAccessRequest.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import {DataSet} from '../../libs/ajax/DataSet';
 import {DAR} from '../../libs/ajax/DAR';
 import {FormField, FormFieldTitle, FormFieldTypes, FormValidators} from '../../components/forms/forms';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 import {
   needsDsAcknowledgement,
   needsPubAcknowledgement,
@@ -226,9 +227,9 @@ export default function DataAccessRequest(props) {
               </p>
             </>
           }
-          placeholder={'Please limit your RUS to 2200 characters.'}
+          placeholder={`Please limit your RUS to ${FORM_TEXT_AREA_MAX_LENGTH} characters.`}
           rows={6}
-          maxLength={2200}
+          maxLength={FORM_TEXT_AREA_MAX_LENGTH}
           ariaLevel={ariaLevel + 3}
           defaultValue={formData.rus}
           validation={validation.rus}
diff --git a/src/pages/dar_application/ProgressReportApplication.tsx b/src/pages/dar_application/ProgressReportApplication.tsx
index beb1f20ef4..0c997a9db0 100644
--- a/src/pages/dar_application/ProgressReportApplication.tsx
+++ b/src/pages/dar_application/ProgressReportApplication.tsx
@@ -1,11 +1,12 @@
-import React, { useEffect, useState } from 'react';
+import React, { useState, useEffect } from 'react';
 import { DataAccessRequest, Dataset } from 'src/types/model';
+import { FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+import SummarySection from 'src/pages/progress_reports/SummarySection';
 import SelectableDatasets from 'src/pages/dar_application/SelectableDatasets';
-import SubmitProgressReport from 'src/pages/progress_reports/SubmitProgressReport';
 import CollaboratorChanges from 'src/pages/progress_reports/CollaboratorChanges';
 import DataManagementIncident from 'src/pages/progress_reports/DataManagementIncident';
 import DarCloseout from 'src/pages/progress_reports/DarCloseout';
-import { FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+import SubmitProgressReport from 'src/pages/progress_reports/SubmitProgressReport';
 
 type ProgressReportApplicationProps = {
     dar?: DataAccessRequest, // Dar will be empty if this is an application
@@ -31,19 +32,12 @@ export const ProgressReportApplication = ({ dar, parentDar, datasets, readOnlyMo
 
     return (
         <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
-            {!readOnlyMode && <h3>Submit a progress report</h3>}
-            {/*TODO we'll want each of these to be components that accept a 'readOnly' flag*/}
-            <div>
-                <h4>Progress Report Summary</h4>
-                {dar?.progressReportSummary ?? "PLACEHOLDER Progress Report Summary"}
-            </div>
-            <div>
-                <h4>Intellectual Property Summary</h4>
-                {dar?.intellectualPropertySummary ?? "PLACEHOLDER Intellectual Property Summary"}
+            <div className={readOnlyMode ? 'accordion-step-container' : 'step-container'}>
+                <SummarySection readOnly={readOnlyMode} formState={formState} onFormChange={onFormChange} />
             </div>
             <div data-cy='remove-datasets'>
                 <div className='progress-report-step-card'>
-                    <h2>Step 3: Dataset(s) in this DAR</h2>
+                    <h2>Step 2: Dataset(s) in this DAR</h2>
                     <p style={{ marginBottom: '1rem' }}>Currently selected datasets:</p>
                     <SelectableDatasets
                         disabled={readOnlyMode}
diff --git a/src/pages/progress_reports/CollaboratorChanges.tsx b/src/pages/progress_reports/CollaboratorChanges.tsx
index e97c9858c5..12a66bc7ce 100644
--- a/src/pages/progress_reports/CollaboratorChanges.tsx
+++ b/src/pages/progress_reports/CollaboratorChanges.tsx
@@ -36,11 +36,11 @@ export default function CollaboratorChanges(props: CollaboratorProps): React.JSX
     return (
         <div data-cy='dar-closeout'>
             <div className='progress-report-step-card'>
-                <h2>Step 2: Add or Remove Collaborators</h2>
+                <h2>Step 3: Add or Remove Collaborators</h2>
 
                 <div className='progress-report-row'>
                     <div>
-                        <h3>2.1 Internal Lab Staff</h3>
+                        <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>
@@ -55,7 +55,7 @@ export default function CollaboratorChanges(props: CollaboratorProps): React.JSX
                 </div>
                 <div className='progress-report-row'>
                     <div>
-                        <h3>2.2 Internal Collaborators</h3>
+                        <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>
@@ -70,7 +70,7 @@ export default function CollaboratorChanges(props: CollaboratorProps): React.JSX
                 </div>
                 <div className='progress-report-row'>
                     <div>
-                        <h3>2.3 External Collaborators</h3>
+                        <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>
diff --git a/src/pages/progress_reports/DarCloseout.tsx b/src/pages/progress_reports/DarCloseout.tsx
index 5e9b174b57..161ce4e629 100644
--- a/src/pages/progress_reports/DarCloseout.tsx
+++ b/src/pages/progress_reports/DarCloseout.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 import { FormField, FormFieldTypes } from 'src/components/forms/forms';
 import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
 
@@ -14,11 +15,11 @@ export default function DarCloseout(props: DarCloseoutProps): React.JSX.Element
     return (
         <div data-cy='dar-closeout'>
             <div className='progress-report-step-card'>
-                <h2>Step 4: DAR Closeout</h2>
+                <h2>Step 5: DAR Closeout</h2>
 
                 <div className='progress-report-row'>
                     <div>
-                        <h3>4.1 Closeouts</h3>
+                        <h3>5.1 Closeouts</h3>
                         <p>
                             If you are ready to finish work on this project, please complete this DAR Closeout section.
                             <br /><br />
@@ -79,7 +80,7 @@ export default function DarCloseout(props: DarCloseoutProps): React.JSX.Element
                                     type={FormFieldTypes.TEXTAREA}
                                     placeholder='Please provide context for the other reason.'
                                     rows={6}
-                                    maxLength={2200}
+                                    maxLength={FORM_TEXT_AREA_MAX_LENGTH}
                                     onChange={({ key, value }: FormFieldChange) => {
                                         onFormChange({ [key]: value });
                                     }}
diff --git a/src/pages/progress_reports/DataManagementIncident.tsx b/src/pages/progress_reports/DataManagementIncident.tsx
index 8813a8fea6..2f0c215d21 100644
--- a/src/pages/progress_reports/DataManagementIncident.tsx
+++ b/src/pages/progress_reports/DataManagementIncident.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import { FORM_TEXT_AREA_MAX_LENGTH } from 'src/components/forms/formConstants';
 import { FormField, FormFieldTypes } from 'src/components/forms/forms';
 import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
 
@@ -16,14 +17,14 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
     return (
         <div data-cy='data-management-incident'>
             <div className='progress-report-step-card'>
-                <h2>Step 3: Data Management Incident</h2>
+                <h2>Step 4: Data Management Incident</h2>
 
                 <div className='progress-report-row'>
                     <div>
                         <FormField
                             id='dmiYesNo'
                             type={FormFieldTypes.YESNORADIOGROUP}
-                            title='3.1 Data Management Incident'
+                            title='4.1 Data Management Incident'
                             titleStyle={titleStyle}
                             description='Have there been any incidents related to mismanagement or misuse of data?'
                             orientation='horizontal'
@@ -128,9 +129,9 @@ export default function DataManagementIncident(props: DataManagementIncidentProp
                                     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 2200 characters.'
+                                    placeholder={`Please limit your Data Management Incident to ${FORM_TEXT_AREA_MAX_LENGTH} characters.`}
                                     rows={6}
-                                    maxLength={2200}
+                                    maxLength={FORM_TEXT_AREA_MAX_LENGTH}
                                     onChange={({ key, value }: FormFieldChange) => {
                                         onFormChange({ [key]: value });
                                     }}
diff --git a/src/pages/progress_reports/SummarySection.tsx b/src/pages/progress_reports/SummarySection.tsx
new file mode 100644
index 0000000000..bcf4a61ae1
--- /dev/null
+++ b/src/pages/progress_reports/SummarySection.tsx
@@ -0,0 +1,121 @@
+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 { Publication } from 'src/components/publications_list/Publication';
+import PublicationList from 'src/components/publications_list/PublicationList';
+import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+
+interface SummarySectionProps {
+    readonly readOnly: boolean;
+    formState: FormState;
+    onFormChange: (newState: Partial<FormState>) => void;
+}
+
+export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
+    const { readOnly, formState, onFormChange } = props;
+
+    const [publications, setPublications] = useState(formState.publications || []);
+    const [presentations, setPresentations] = useState(formState.presentations || []);
+
+    const onStateChange = (key: string, setState: React.Dispatch<Publication[]>, publications: Publication[]) => {
+        onFormChange({ [key]: publications });
+        setState(publications);
+    };
+
+    const onPublicationChange = (publications: Publication[]) => {
+        onStateChange('publications', setPublications, publications);
+    }
+
+    const onPresentationChange = (presentations: Publication[]) => {
+        onStateChange('presentations', setPresentations, presentations);
+    }
+
+    return (
+        <div data-cy='summary-section'>
+            <div className='progress-report-step-card'>
+                {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
+
+                <div className='progress-report-row'>
+                    <FormField
+                        id='progressReportSummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        title='1.1 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}
+                        maxLength={FORM_TEXT_AREA_MAX_LENGTH}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                </div>
+                <div className='progress-report-row'>
+                    <FormField
+                        id='intellectualPropertyYesNo'
+                        type={FormFieldTypes.YESNORADIOGROUP}
+                        title='1.2 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'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                    {formState.intellectualPropertyYesNo === true && <FormField
+                        id='intellectualPropertySummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        description='Please describe the intellectual property resulting from analysis of the requested dataset(s).'
+                        placeholder='Please provide an update here.'
+                        rows={6}
+                        maxLength={FORM_TEXT_AREA_MAX_LENGTH}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />}
+                </div>
+                <div className='progress-report-row'>
+                    <FormField
+                        id='publicationsYesNo'
+                        type={FormFieldTypes.YESNORADIOGROUP}
+                        title='1.3 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'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                    {formState.publicationsYesNo === true && <PublicationList
+                        publications={publications}
+                        publicationText='Publication'
+                        columnsToShow={['title', 'date']}
+                        onPublicationChange={onPublicationChange}
+                        disabled={readOnly}
+                    />}
+                </div>
+                <div className='progress-report-row'>
+                    <FormField
+                        id='presentationsYesNo'
+                        type={FormFieldTypes.YESNORADIOGROUP}
+                        title='1.4 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'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                    {formState.presentationsYesNo === true && <PublicationList
+                        publications={presentations}
+                        publicationText='Presentation'
+                        columnsToShow={['title', 'date']}
+                        onPublicationChange={onPresentationChange}
+                        disabled={readOnly}
+                    />}
+                </div>
+            </div>
+        </div>
+    );
+}


Reviews:
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:28:10Z
user: login: rushtong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:40:26Z
user: login: rushtong
body: Thank you, looks good 👍🏽 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:43:19Z
user: login: pshapiro4broad
body: This is a large change, but looks OK as far as I can tell.

I worry that our exact matching on outline numbers is making our tests too fragile, requiring many updates when the text is changed. It would be even better if we had a CSS or some other way to automatically number outlines, since DUOS has so many of them.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T13:38:42Z
user: login: pshapiro4broad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T14:10:37Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:13:50Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:15:30Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:16:33Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:17:53Z
user: login: fboulnois
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:24:51Z

Review comments:
diff_hunk: @@ -0,0 +1,10 @@
+export interface Publication {
path: src/components/publications_list/Publication.tsx
user: login: fboulnois
body: Note: the entire `publications_list` component is essentially a duplicate of the collaborators component with different fields. In the future it might make sense to combine these components. As a result I haven't added tests for this component (practically, they would be nearly identical to those in the `collaborator_list` component).
created_at: 2025-05-15T19:28:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import React from 'react';
+import { Publication } from './Publication';
+import PublicationAddEdit from './PublicationAddEdit';
+import PublicationSummary from './PublicationSummary';
+
+interface PublicationRowProps {
+    readonly id: number;
+    readonly editMode: boolean;
+    publication: Publication;
+    readonly publicationText: string;
+    readonly publications: Publication[];
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly closeAction: () => void;
+    readonly onPublicationChange: (publications: Publication[]) => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationRow(props: PublicationRowProps): React.JSX.Element {
+    const { id, editMode, publication, publicationText, publications, columnsToShow, editAction, deleteAction, closeAction, onPublicationChange, disabled } = props;
+
+    return (
+        <div>
+            {editMode && (
+                <PublicationAddEdit
+                    id={id}
+                    publication={publication}
+                    publicationText={publicationText}
+                    publications={publications}
+                    closeAction={closeAction}
+                    onPublicationChange={onPublicationChange}
+                />)}
+            {!editMode && (
+                <PublicationSummary
+                    publication={publication}
+                    columnsToShow={columnsToShow}
+                    editAction={editAction}
+                    deleteAction={deleteAction}
+                    disabled={disabled}
path: src/components/publications_list/PublicationRow.tsx
user: login: rushtong
body: Could have have a case where `editMode` is false and `disabled` is true? IOW, could we default to `editMode`?
created_at: 2025-05-16T11:40:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,10 +44,10 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('renders the component correctly', () => {
     cy.get('[data-cy=dar-closeout]').should('exist');
-    cy.contains('Step 2: Add or Remove Collaborators').should('be.visible');
-    cy.contains('2.1 Internal Lab Staff').should('be.visible');
-    cy.contains('2.2 Internal Collaborators').should('be.visible');
-    cy.contains('2.3 External Collaborators').should('be.visible');
+    cy.contains('Step 3: Add or Remove Collaborators').should('be.visible');
path: cypress/component/ProgressReports/collaborator_changes.spec.tsx
user: login: pshapiro4broad
body: To avoid making continual updates to these tests, they could use regular expressions. Assuming that the exact outline level is not something we need to test.
```suggestion
    cy.contains(/^Step \d+: Add or Remove Collaborators$/).should('be.visible');
```
created_at: 2025-05-16T13:13:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { FormField, FormFieldTypes } from 'src/components/forms/forms';
+import { Publication } from 'src/components/publications_list/Publication';
+import PublicationList from 'src/components/publications_list/PublicationList';
+import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+
+interface SummarySectionProps {
+    readonly readOnly: boolean;
+    formState: FormState;
+    onFormChange: (newState: Partial<FormState>) => void;
+}
+
+export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
+    const { readOnly, formState, onFormChange } = props;
+
+    const [publications, setPublications] = useState(formState.publications || []);
+    const [presentations, setPresentations] = useState(formState.presentations || []);
+
+    const onStateChange = (key: string, setState: React.Dispatch<Publication[]>, publications: Publication[]) => {
+        onFormChange({ [key]: publications });
+        setState(publications);
+    };
+
+    const onPublicationChange = (publications: Publication[]) => {
+        onStateChange('publications', setPublications, publications);
+    }
+
+    const onPresentationChange = (presentations: Publication[]) => {
+        onStateChange('presentations', setPresentations, presentations);
+    }
+
+    return (
+        <div data-cy='summary-section'>
+            <div className='progress-report-step-card'>
+                {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
+
+                <div className='progress-report-row'>
+                    <FormField
+                        id='progressReportSummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        title='1.1 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}
+                        maxLength={2200}
path: src/pages/progress_reports/SummarySection.tsx
user: login: pshapiro4broad
body: It would be nice to avoid a magic number here, in case this value changes in the future. Also I see `2200` in many other places in the UI code, so this appears to be an existing magic number.
created_at: 2025-05-16T13:18:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { FormField, FormFieldTypes } from 'src/components/forms/forms';
+import { Publication } from 'src/components/publications_list/Publication';
+import PublicationList from 'src/components/publications_list/PublicationList';
+import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+
+interface SummarySectionProps {
+    readonly readOnly: boolean;
+    formState: FormState;
+    onFormChange: (newState: Partial<FormState>) => void;
+}
+
+export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
+    const { readOnly, formState, onFormChange } = props;
+
+    const [publications, setPublications] = useState(formState.publications || []);
+    const [presentations, setPresentations] = useState(formState.presentations || []);
+
+    const onStateChange = (key: string, setState: React.Dispatch<Publication[]>, publications: Publication[]) => {
+        onFormChange({ [key]: publications });
+        setState(publications);
+    };
+
+    const onPublicationChange = (publications: Publication[]) => {
+        onStateChange('publications', setPublications, publications);
+    }
+
+    const onPresentationChange = (presentations: Publication[]) => {
+        onStateChange('presentations', setPresentations, presentations);
+    }
+
+    return (
+        <div data-cy='summary-section'>
+            <div className='progress-report-step-card'>
+                {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
+
+                <div className='progress-report-row'>
+                    <FormField
+                        id='progressReportSummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        title='1.1 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}
+                        maxLength={2200}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                </div>
+                <div className='progress-report-row'>
+                    <FormField
+                        id='intellectualPropertyYesNo'
+                        type={FormFieldTypes.YESNORADIOGROUP}
+                        title='1.2 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'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                    {formState.intellectualPropertyYesNo === true && <FormField
path: src/pages/progress_reports/SummarySection.tsx
user: login: pshapiro4broad
body: Why does this code use `=== true`? Is is possible that a non-boolean value will be stored in `intellectualPropertyYesNo`?
created_at: 2025-05-16T13:21:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
+                    style={{ marginLeft: 10, marginRight: 10, ...buttonStyle }}
+                    onClick={() => !disabled && editAction()}
+                >
+                    <span
+                        className='glyphicon glyphicon-pencil caret-margin collaborator-edit-icon'
+                        aria-hidden='true'
+                        data-tip='Edit dataset'
+                        data-for='tip_edit'
+                    ></span>
+                    <span style={{ marginLeft: '1rem' }}></span>
+                </a>
+            </div>
+            {/* delete button */}
+            <a
+                style={{ marginLeft: 10, ...buttonStyle }}
+                onClick={() => !disabled && setShowDeleteModal(true) }
+            >
+                <span
+                    className='glyphicon glyphicon-trash publication-delete-icon'
+                    aria-hidden='true'
+                    data-tip='Delete dataset'
+                    data-for='tip_delete'
+                ></span>
+                <span style={{ marginLeft: '1rem' }}></span>
+            </a>
+            {/* delete modal */}
+            <PublicationDelete
+                publicationName={publication.title}
+                showDelete={showDeleteModal}
+                confirmAction={() => { deleteAction(); setShowDeleteModal(false); }}
path: src/components/publications_list/PublicationSummary.tsx
user: login: pshapiro4broad
body: I'm surprised that eslint doesn't flag this, I would expect it to be formatted across lines
```suggestion
                confirmAction={() => {
                    deleteAction();
                    setShowDeleteModal(false);
                }}
```
created_at: 2025-05-16T13:26:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
path: src/components/publications_list/PublicationSummary.tsx
user: login: pshapiro4broad
body: Sonar flags this use of an anchor tag with a number of issues. For example, it says that without a keyboard listener users must use a mouse to interact with this element
created_at: 2025-05-16T13:29:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
path: src/components/publications_list/PublicationSummary.tsx
user: login: pshapiro4broad
body: https://sonarcloud.io/project/issues?sinceLeakPeriod=true&issueStatuses=OPEN%2CCONFIRMED&pullRequest=2870&id=DataBiosphere_duos-ui&open=AZbZWjdtzsbsOc2-Lz_i
created_at: 2025-05-16T14:10:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { FormField, FormFieldTypes } from 'src/components/forms/forms';
+import { Publication } from 'src/components/publications_list/Publication';
+import PublicationList from 'src/components/publications_list/PublicationList';
+import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+
+interface SummarySectionProps {
+    readonly readOnly: boolean;
+    formState: FormState;
+    onFormChange: (newState: Partial<FormState>) => void;
+}
+
+export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
+    const { readOnly, formState, onFormChange } = props;
+
+    const [publications, setPublications] = useState(formState.publications || []);
+    const [presentations, setPresentations] = useState(formState.presentations || []);
+
+    const onStateChange = (key: string, setState: React.Dispatch<Publication[]>, publications: Publication[]) => {
+        onFormChange({ [key]: publications });
+        setState(publications);
+    };
+
+    const onPublicationChange = (publications: Publication[]) => {
+        onStateChange('publications', setPublications, publications);
+    }
+
+    const onPresentationChange = (presentations: Publication[]) => {
+        onStateChange('presentations', setPresentations, presentations);
+    }
+
+    return (
+        <div data-cy='summary-section'>
+            <div className='progress-report-step-card'>
+                {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
+
+                <div className='progress-report-row'>
+                    <FormField
+                        id='progressReportSummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        title='1.1 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}
+                        maxLength={2200}
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                </div>
+                <div className='progress-report-row'>
+                    <FormField
+                        id='intellectualPropertyYesNo'
+                        type={FormFieldTypes.YESNORADIOGROUP}
+                        title='1.2 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'
+                        onChange={({ key, value }: FormFieldChange) => {
+                            onFormChange({ [key]: value });
+                        }}
+                        disabled={readOnly}
+                    />
+                    {formState.intellectualPropertyYesNo === true && <FormField
path: src/pages/progress_reports/SummarySection.tsx
user: login: fboulnois
body: It's mainly for clarity, since our "Yes / No" radio buttons actually map to booleans, not "Yes" / "No" strings.
created_at: 2025-05-16T14:13:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { FormField, FormFieldTypes } from 'src/components/forms/forms';
+import { Publication } from 'src/components/publications_list/Publication';
+import PublicationList from 'src/components/publications_list/PublicationList';
+import { FormFieldChange, FormState } from 'src/pages/progress_reports/ProgressReportFormState';
+
+interface SummarySectionProps {
+    readonly readOnly: boolean;
+    formState: FormState;
+    onFormChange: (newState: Partial<FormState>) => void;
+}
+
+export default function SummarySection(props: SummarySectionProps): React.JSX.Element {
+    const { readOnly, formState, onFormChange } = props;
+
+    const [publications, setPublications] = useState(formState.publications || []);
+    const [presentations, setPresentations] = useState(formState.presentations || []);
+
+    const onStateChange = (key: string, setState: React.Dispatch<Publication[]>, publications: Publication[]) => {
+        onFormChange({ [key]: publications });
+        setState(publications);
+    };
+
+    const onPublicationChange = (publications: Publication[]) => {
+        onStateChange('publications', setPublications, publications);
+    }
+
+    const onPresentationChange = (presentations: Publication[]) => {
+        onStateChange('presentations', setPresentations, presentations);
+    }
+
+    return (
+        <div data-cy='summary-section'>
+            <div className='progress-report-step-card'>
+                {readOnly ? <h3>Review a Progress Report</h3> : <h3>Step 1: Submit a Progress Report</h3>}
+
+                <div className='progress-report-row'>
+                    <FormField
+                        id='progressReportSummary'
+                        type={FormFieldTypes.TEXTAREA}
+                        title='1.1 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}
+                        maxLength={2200}
path: src/pages/progress_reports/SummarySection.tsx
user: login: fboulnois
body: Is the suggestion to pull out this value into a common constant?
created_at: 2025-05-16T14:15:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
+                    style={{ marginLeft: 10, marginRight: 10, ...buttonStyle }}
+                    onClick={() => !disabled && editAction()}
+                >
+                    <span
+                        className='glyphicon glyphicon-pencil caret-margin collaborator-edit-icon'
+                        aria-hidden='true'
+                        data-tip='Edit dataset'
+                        data-for='tip_edit'
+                    ></span>
+                    <span style={{ marginLeft: '1rem' }}></span>
+                </a>
+            </div>
+            {/* delete button */}
+            <a
+                style={{ marginLeft: 10, ...buttonStyle }}
+                onClick={() => !disabled && setShowDeleteModal(true) }
+            >
+                <span
+                    className='glyphicon glyphicon-trash publication-delete-icon'
+                    aria-hidden='true'
+                    data-tip='Delete dataset'
+                    data-for='tip_delete'
+                ></span>
+                <span style={{ marginLeft: '1rem' }}></span>
+            </a>
+            {/* delete modal */}
+            <PublicationDelete
+                publicationName={publication.title}
+                showDelete={showDeleteModal}
+                confirmAction={() => { deleteAction(); setShowDeleteModal(false); }}
path: src/components/publications_list/PublicationSummary.tsx
user: login: fboulnois
body: This seems ok to me, the line is short.
created_at: 2025-05-16T14:16:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import React, { useState } from "react";
+import PublicationDelete from "./PublicationDelete";
+import { Publication } from "./Publication";
+
+interface PublicationSummaryProps {
+    publication: Publication;
+    readonly columnsToShow: string[];
+    readonly editAction: () => void;
+    readonly deleteAction: () => void;
+    readonly disabled: boolean;
+}
+
+export default function PublicationSummary(props: PublicationSummaryProps): React.JSX.Element {
+    const { publication, columnsToShow, editAction, deleteAction, disabled } = props;
+
+    const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+    const disabledStyle = {
+        cursor: 'not-allowed',
+        opacity: 0.5
+    };
+
+    const buttonStyle = disabled ? disabledStyle : {};
+
+    return (
+        <div className='collaborator-summary-card'>
+            {/* data elements to show in the row summary */}
+            {columnsToShow.map((column, index) => {
+                const columnContent = publication[column as keyof Publication];
+                return columnContent && (
+                    <div key={'publication_summary_column_' + index} style={{ flex: '1 1 100%', marginRight: '1.5rem' }}>
+                        <span>
+                            {columnContent}
+                        </span>
+                    </div>
+                );
+            })}
+            {/* edit button */}
+            <div className='collaborator-summary-edit-delete-buttons'>
+                <a
path: src/components/publications_list/PublicationSummary.tsx
user: login: fboulnois
body: This is an existing issue that we have with the old and new collaborator component (and now publication list component), but we can pull this into the larger refactoring ticket.
created_at: 2025-05-16T14:17:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,10 +44,10 @@ describe('Collaborator Changes - Component Tests', () => {
 
   it('renders the component correctly', () => {
     cy.get('[data-cy=dar-closeout]').should('exist');
-    cy.contains('Step 2: Add or Remove Collaborators').should('be.visible');
-    cy.contains('2.1 Internal Lab Staff').should('be.visible');
-    cy.contains('2.2 Internal Collaborators').should('be.visible');
-    cy.contains('2.3 External Collaborators').should('be.visible');
+    cy.contains('Step 3: Add or Remove Collaborators').should('be.visible');
path: cypress/component/ProgressReports/collaborator_changes.spec.tsx
user: login: fboulnois
body: I agree that the tests are somewhat fragile due to the numbering scheme, but ultimately this form will change (and the numbers will probably be removed) which will require the tests to be updated anyways, so this seems like a reasonable stopgap until then.
created_at: 2025-05-16T14:24:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-16T15:36:43Z
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=2870) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [21 New issues](https://sonarcloud.io/project/issues?id=DataBiosphere_duos-ui&pullRequest=2870&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=2870&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=2870&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=2870&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12.4% Duplication on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_duos-ui&pullRequest=2870&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DataBiosphere_duos-ui&pullRequest=2870)


author_association: NONE