Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JN-1411] Allow participants to delete unused documents #1478

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions ui-participant/src/hub/documents/DocumentLibrary.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { asMockedFn, MockI18nProvider, setupRouterTest, mockParticipantFile } from '@juniper/ui-core'
import { usePortalEnv } from 'providers/PortalProvider'
import { mockUsePortalEnv } from 'test-utils/test-portal-factory'
import { render, screen, waitFor } from '@testing-library/react'
import { mockPortal, mockUsePortalEnv } from 'test-utils/test-portal-factory'
import { act, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import DocumentLibrary from './DocumentLibrary'
import { useActiveUser } from 'providers/ActiveUserProvider'
Expand All @@ -16,7 +16,8 @@ jest.mock('providers/ActiveUserProvider', () => ({
}))

jest.mock('api/api', () => ({
listParticipantFiles: jest.fn()
listParticipantFiles: jest.fn(),
getPortal: jest.fn()
}))

beforeEach(() => {
Expand Down Expand Up @@ -58,10 +59,15 @@ describe('DocumentLibrary', () => {
expect(screen.getByText('file1.pdf')).toBeInTheDocument()
})

await act(async () => {
screen.getByText('Options').click()
})

expect(screen.getByText('Delete')).toBeInTheDocument()
expect(screen.getByText('{documentDownloadButton}')).toBeInTheDocument()
})

it('renders no associated tasks message', async () => {
it('does not render any associated tasks', async () => {
asMockedFn(Api.listParticipantFiles).mockResolvedValue([
mockParticipantFile('file1.pdf', [])
])
Expand All @@ -72,7 +78,7 @@ describe('DocumentLibrary', () => {
expect(screen.getByText('file1.pdf')).toBeInTheDocument()
})

expect(screen.getByText('not associated with any tasks')).toBeInTheDocument()
expect(screen.queryByText('shared in response to')).not.toBeInTheDocument()
})

it('renders associated tasks', async () => {
Expand Down Expand Up @@ -106,4 +112,72 @@ describe('DocumentLibrary', () => {
//note: this is looking at the i18n key for the task name
expect(screen.getByText('{researchSurvey1:1}')).toBeInTheDocument()
})


it('allows deleting documents that dont have any associated answers', async () => {
asMockedFn(Api.listParticipantFiles).mockResolvedValue([
mockParticipantFile('file1.pdf')
])

asMockedFn(Api.getPortal).mockResolvedValue(mockPortal())

const { RoutedComponent } = setupRouterTest(
<MockI18nProvider><DocumentLibrary/></MockI18nProvider>
)
render(RoutedComponent)

await waitFor(() => {
expect(screen.getByText('file1.pdf')).toBeInTheDocument()
})

await act(async () => {
screen.getByText('Options').click()
})

await act(async () => {
screen.getByText('Delete').click()
})

await waitFor(() => {
expect(screen.queryByText('Are you sure you want to delete this document?', { exact: false })).toBeInTheDocument()
})
})

it('shows warning modal when trying to delete a document that has associated answers', async () => {
asMockedFn(Api.listParticipantFiles).mockResolvedValue([
mockParticipantFile('file1.pdf', [{
format: 'FILE_NAME',
surveyVersion: 1,
stringValue: 'file1.pdf',
questionStableId: 'question1',
surveyResponseId: 'taskId1'
}])
])

asMockedFn(Api.getPortal).mockResolvedValue(mockPortal())

const { RoutedComponent } = setupRouterTest(
<MockI18nProvider><DocumentLibrary/></MockI18nProvider>
)
render(RoutedComponent)

await waitFor(() => {
expect(screen.getByText('file1.pdf')).toBeInTheDocument()
})

await act(async () => {
screen.getByText('Options').click()
})

await act(async () => {
screen.getByText('Delete').click()
})

await waitFor(() => {
expect(screen.queryByText('' +
'This document is currently shared in response to at least one survey. ' +
'Please remove it from the survey response(s) before deleting it.')
).toBeInTheDocument()
})
})
})
95 changes: 81 additions & 14 deletions ui-participant/src/hub/documents/DocumentLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import {
EnvironmentName,
I18nOptions,
instantToDateString,
ParticipantFile, ParticipantTask,
saveBlobAsDownload,
ParticipantFile, ParticipantTask, saveBlobAsDownload,
StudyEnvParams,
useI18n
} from '@juniper/ui-core'
import React, { useEffect, useState } from 'react'
import { useActiveUser } from 'providers/ActiveUserProvider'
import Api from 'api/api'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDownload, faFile, faFileImage, faFileLines, faFilePdf } from '@fortawesome/free-solid-svg-icons'
import { faFile, faFileImage, faFileLines, faFilePdf } from '@fortawesome/free-solid-svg-icons'
import { usePortalEnv } from 'providers/PortalProvider'
import { Link } from 'react-router-dom'
import { getTaskPath } from '../task/taskUtils'
import Modal from 'react-bootstrap/Modal'
import ThemedModal from 'components/ThemedModal'

export default function DocumentLibrary() {
const { i18n } = useI18n()
Expand Down Expand Up @@ -106,16 +107,11 @@ const DocumentsList = ({ studyName, studyEnvParams, enrollee }: {
</td>
<td className="align-middle">
<div className={'d-flex justify-content-end'}>
<button className="btn btn-outline-primary" onClick={async () => {
const response = await Api.downloadParticipantFile({
studyEnvParams, enrolleeShortcode: enrollee.shortcode, fileName: participantFile.fileName
})
saveBlobAsDownload(await response.blob(), participantFile.fileName)
}}>
<span className="d-flex align-items-center">
<FontAwesomeIcon className="pe-1" icon={faDownload}/>{i18n('documentDownloadButton')}
</span>
</button>
<FileOptionsDropdown
studyEnvParams={studyEnvParams}
loadDocuments={loadDocuments}
participantFile={participantFile}
enrollee={enrollee}/>
</div>
</td>
</tr>
Expand All @@ -138,7 +134,7 @@ const surveyResponseIdsToTaskNames = (
}).filter((task): task is ParticipantTask => task !== undefined)

if (associatedTasks.length === 0) {
return <div className={'mt-2 fst-italic text-muted'}>not associated with any tasks</div>
return null
}

return (
Expand Down Expand Up @@ -170,3 +166,74 @@ const fileTypeToIcon = (fileType: string) => {
return <FontAwesomeIcon className="me-2" icon={faFile}/>
}
}

const FileOptionsDropdown = ({ studyEnvParams, participantFile, enrollee, loadDocuments }: {
studyEnvParams: StudyEnvParams, participantFile: ParticipantFile, enrollee: Enrollee, loadDocuments: () => void
}) => {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const { i18n } = useI18n()
return (<>
<li className="nav-item dropdown d-flex flex-column">
<button className="btn btn-outline-primary dropdown-toggle" id="fileOptionsDropdown"
data-bs-toggle="dropdown" aria-expanded="false">
Options
</button>
<ul className="dropdown-menu" aria-labelledby="fileOptionsDropdown">
<li>
<a role={'button'} className="dropdown-item"
onClick={() => setShowConfirmDelete(true)}
>
Delete
</a>
</li>
<li>
<a className="dropdown-item" role={'button'} onClick={async () => {
const response = await Api.downloadParticipantFile({
studyEnvParams, enrolleeShortcode: enrollee.shortcode, fileName: participantFile.fileName
})
saveBlobAsDownload(await response.blob(), participantFile.fileName)
}}>
{i18n('documentDownloadButton')}
</a>
</li>
</ul>
</li>
{showConfirmDelete && <ThemedModal show={true}
onHide={() => setShowConfirmDelete(false)} size={'lg'} animation={true}>
<Modal.Header>
<Modal.Title>
<h2 className="fw-bold pb-0 mb-0">
{participantFile.associatedAnswers.length === 0 ? 'Are you sure?' : 'This document is in use'}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also check this on the backend? it looks like it naively will just delete it

</h2>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{participantFile.associatedAnswers.length === 0 ?
<p className="m-0">Are you sure you want to delete this document? This cannot be undone.</p> :
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC I think one of the things that was mentioned during demo was that the language here needs to be clear that this doesn't guarantee the files are actually gone - study staff might have saved them elsewhere, etc. Maybe some language here that, if you want to fully scrub from the system, they need to email staff, or something.

<p className="m-0">This document is currently shared in response to at least one survey. Please remove it
from the survey response(s) before deleting it.</p>
}
</Modal.Body>
<Modal.Footer>
<div className={'d-flex w-100'}>
<button className={'btn btn-primary m-2'}
disabled={participantFile.associatedAnswers.length > 0}
onClick={async () => {
await Api.deleteParticipantFile({
studyEnvParams, enrolleeShortcode: enrollee.shortcode, fileName: participantFile.fileName
})
loadDocuments()
setShowConfirmDelete(false)
}}>
Delete
</button>
<button className={'btn btn-outline-secondary m-2'}
onClick={() => setShowConfirmDelete(false)}>
{i18n('cancel')}
</button>
</div>
</Modal.Footer>
</ThemedModal> }
</>
)
}
3 changes: 2 additions & 1 deletion ui-participant/src/test-utils/test-portal-factory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export const mockLocalSiteContent = (): LocalSiteContent => {
landingPage: mockHtmlPage(),
navLogoCleanFileName: 'navLogo.png',
navLogoVersion: 1,
languageTextOverrides: []
languageTextOverrides: [],
primaryBrandColor: '#000000'
}
}

Expand Down
Loading