-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add components, view, and route for file storatge ui
- Loading branch information
Showing
22 changed files
with
1,653 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { IconAttachment } from '@shared/icons'; | ||
import { dateAndTimeSimple } from '@util/datetime'; | ||
import prettyBytes from 'pretty-bytes'; | ||
|
||
const FileDetails = ({ | ||
name, | ||
fullPath, | ||
updatedBy, | ||
updatedAt, | ||
size, | ||
mimeType, | ||
onDelete, | ||
onClose, | ||
}) => { | ||
return ( | ||
<div className="file-details"> | ||
<div className="file-details__header bg-base-lightest"> | ||
<IconAttachment className="usa-icon margin-right-1" /> | ||
<h3>File details</h3> | ||
<button | ||
title="close file details" | ||
className="usa-button usa-button--unstyled file-details__close" | ||
onClick={onClose} | ||
> | ||
<svg className="usa-icon" aria-hidden="true" role="img"> | ||
<use href="/img/sprite.svg#close"></use> | ||
</svg> | ||
</button> | ||
</div> | ||
<table className="usa-table usa-table--borderless file-details__table"> | ||
<thead className="usa-sr-only"> | ||
<tr> | ||
<th scope="col">Property</th> | ||
<th scope="col">Value</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<th scope="row">File name</th> | ||
<td className="font-mono-xs text-ls-neg-1 text-bold">{name}</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">Full path</th> | ||
<td className="font-mono-xs text-ls-neg-1"> | ||
<a | ||
style={{ wordBreak: 'break-all' }} | ||
href={fullPath} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
{fullPath} | ||
</a> | ||
</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">Uploaded by</th> | ||
<td className="text-bold">{updatedBy}</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">Uploaded at</th> | ||
<td>{updatedAt && dateAndTimeSimple(updatedAt)}</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">File size</th> | ||
<td>{size && prettyBytes(size)}</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">MIME type</th> | ||
<td>{mimeType}</td> | ||
</tr> | ||
<tr> | ||
<th scope="row">Actions</th> | ||
<td> | ||
<a href={fullPath} download className="usa-button"> | ||
Download | ||
</a> | ||
<button | ||
type="button" | ||
title="Remove from public storage" | ||
className="usa-button usa-button--outline delete-button" | ||
onClick={() => onDelete({ type: 'file', name: name })} | ||
> | ||
Delete | ||
</button> | ||
</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
); | ||
}; | ||
|
||
FileDetails.propTypes = { | ||
name: PropTypes.string.isRequired, | ||
fullPath: PropTypes.string.isRequired, | ||
updatedBy: PropTypes.string.isRequired, | ||
updatedAt: PropTypes.string.isRequired, | ||
size: PropTypes.number.isRequired, | ||
mimeType: PropTypes.string.isRequired, | ||
onDelete: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default FileDetails; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import React from 'react'; | ||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
import '@testing-library/jest-dom'; | ||
import { MemoryRouter } from 'react-router-dom'; | ||
import FileDetails from './FileDetails'; | ||
import prettyBytes from 'pretty-bytes'; | ||
|
||
jest.mock('pretty-bytes', () => ({ | ||
__esModule: true, | ||
default: jest.fn(), | ||
})); | ||
|
||
describe('FileDetails', () => { | ||
const mockOnDelete = jest.fn(); | ||
const mockOnClose = jest.fn(); | ||
const fileProps = { | ||
name: 'test-document.pdf', | ||
fullPath: 'https://example.com/files/test-document.pdf', | ||
updatedBy: 'user@example.com', | ||
updatedAt: '2025-02-19T12:00:00Z', | ||
size: 1048576, // 1MB | ||
mimeType: 'application/pdf', | ||
onDelete: mockOnDelete, | ||
onClose: mockOnClose, | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
prettyBytes.mockReturnValue('1 MB'); | ||
}); | ||
|
||
it('renders file details correctly', () => { | ||
render( | ||
<MemoryRouter> | ||
<FileDetails {...fileProps} /> | ||
</MemoryRouter>, | ||
); | ||
|
||
// File name | ||
expect(screen.getByText(fileProps.name)).toBeInTheDocument(); | ||
|
||
// Full path (link) | ||
expect(screen.getByRole('link', { name: fileProps.fullPath })).toHaveAttribute( | ||
'href', | ||
fileProps.fullPath, | ||
); | ||
|
||
// Uploaded by | ||
expect(screen.getByText(fileProps.updatedBy)).toBeInTheDocument(); | ||
|
||
// Uploaded on (formatted date check) | ||
expect(screen.getByText(/Feb 19, 2025/i)).toBeInTheDocument(); | ||
|
||
// File size | ||
expect(screen.getByText('1 MB')).toBeInTheDocument(); | ||
|
||
// MIME type | ||
expect(screen.getByText(fileProps.mimeType)).toBeInTheDocument(); | ||
}); | ||
|
||
it('calls onClose when the close button is clicked', () => { | ||
render( | ||
<MemoryRouter> | ||
<FileDetails {...fileProps} /> | ||
</MemoryRouter>, | ||
); | ||
|
||
const closeButton = screen.getByTitle('close file details'); | ||
fireEvent.click(closeButton); | ||
|
||
expect(mockOnClose).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('calls onDelete when the delete button is clicked', () => { | ||
render( | ||
<MemoryRouter> | ||
<FileDetails {...fileProps} /> | ||
</MemoryRouter>, | ||
); | ||
|
||
const deleteButton = screen.getByRole('button', { name: /delete/i }); | ||
fireEvent.click(deleteButton); | ||
|
||
expect(mockOnDelete).toHaveBeenCalledTimes(1); | ||
expect(mockOnDelete).toHaveBeenCalledWith({ type: 'file', name: fileProps.name }); | ||
}); | ||
|
||
it('renders a working download link', () => { | ||
render( | ||
<MemoryRouter> | ||
<FileDetails {...fileProps} /> | ||
</MemoryRouter>, | ||
); | ||
|
||
const downloadLink = screen.getByRole('link', { name: /download/i }); | ||
expect(downloadLink).toHaveAttribute('href', fileProps.fullPath); | ||
expect(downloadLink).toHaveAttribute('download'); | ||
}); | ||
}); |
Oops, something went wrong.