Skip to content

Commit

Permalink
feat: add components, view, and route for file storatge ui
Browse files Browse the repository at this point in the history
  • Loading branch information
sknep committed Feb 25, 2025
1 parent ed8395a commit 376fc40
Show file tree
Hide file tree
Showing 22 changed files with 1,653 additions and 12 deletions.
105 changes: 105 additions & 0 deletions frontend/pages/file-storage/FileDetails.jsx
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;
99 changes: 99 additions & 0 deletions frontend/pages/file-storage/FileDetails.test.jsx
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');
});
});
Loading

0 comments on commit 376fc40

Please sign in to comment.