Skip to content

Commit cc3bf4a

Browse files
authored
Feature/handle non renderable content 1067 (#1095)
This adds a new diff type that renders a "file preview," which lists some basic metadata about the file and a button to view/download the raw content. For most media types, this replaces the "raw content" view, which just puts the content in an iframe, and often caused the browser to download the file instead of showing you anything meaningful (for types the browser can’t render natively). Fixes #1067.
1 parent 59fe113 commit cc3bf4a

File tree

11 files changed

+412
-2
lines changed

11 files changed

+412
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ This project wouldn’t exist without a lot of amazing people’s help. Thanks t
122122
| [📖](# "Documentation") | [Patrick Connolly](https://github.com/patcon) |
123123
| [📖](# "Documentation") | [Manaswini Das](https://github.com/manaswinidas) |
124124
| [💻](# "Code") [⚠️](# "Tests") | [Nick Echols](https://github.com/steryereo) |
125+
| [💻](# "Code") [⚠️](# "Tests") | [Beckett Frey](https://github.com/BeckettFrey) |
125126
| [💻](# "Code") [⚠️](# "Tests") | [Katie Jones](https://github.com/katjone) |
126127
| [💡](# "Examples") | [@lh00000000](https://github.com/lh00000000) |
127128
| [💻](# "Code") [⚠️](# "Tests") | [Greg Merrill](https://github.com/g-merrill) |

src/components/diff-view.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import SideBySideRenderedDiff from './side-by-side-rendered-diff';
99
import ChangesOnlyDiff from './changes-only-diff';
1010
import RawVersion from './raw-version';
1111
import SideBySideRawVersions from './side-by-side-raw-versions';
12+
import SideBySideFilePreview from './side-by-side-file-preview/side-by-side-file-preview';
1213

1314
import styles from '../css/base.css';
1415

@@ -186,6 +187,10 @@ export default class DiffView extends Component {
186187
return (
187188
<ChangesOnlyDiff diffData={this.state.diffData} className='diff-source-inline' />
188189
);
190+
case diffTypes.SIDE_BY_SIDE_FILE_PREVIEW.value:
191+
return (
192+
<SideBySideFilePreview {...commonProps} />
193+
);
189194
default:
190195
return null;
191196
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* eslint-env jest */
2+
3+
/**
4+
* @jest-environment jsdom
5+
*/
6+
7+
import { render } from '@testing-library/react';
8+
import FilePreview from '../file-preview';
9+
10+
describe('FilePreview', () => {
11+
12+
const mockVersion = {
13+
uuid: 'mock-version-uuid',
14+
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
15+
url: 'https://example.com/versions/test.xlsx',
16+
body_url: 'https://example.com/versions/scscs28u2882',
17+
body_hash: 'abc123def456',
18+
capture_time: '2023-01-01T12:00:00Z'
19+
};
20+
21+
it('renders file information correctly', () => {
22+
const { getByText } = render(
23+
<FilePreview version={mockVersion} />
24+
);
25+
26+
expect(getByText('test.xlsx')).toBeInTheDocument();
27+
expect(getByText(mockVersion.media_type)).toBeInTheDocument();
28+
expect(getByText(mockVersion.body_hash)).toBeInTheDocument();
29+
});
30+
31+
it('renders download and view buttons', () => {
32+
const { getByText } = render(
33+
<FilePreview version={mockVersion} />
34+
);
35+
const viewButton = getByText('View Raw File');
36+
37+
expect(viewButton).toBeInTheDocument();
38+
expect(viewButton.getAttribute('href')).toBe(mockVersion.body_url);
39+
});
40+
41+
it('shows non-renderable content warning', () => {
42+
const { getByText } = render(
43+
<FilePreview version={mockVersion} />
44+
);
45+
46+
expect(getByText('File Information')).toBeInTheDocument();
47+
});
48+
49+
it('handles missing filename gracefully', () => {
50+
const versionWithoutFilename = {
51+
...mockVersion,
52+
url: 'https://example.com/'
53+
};
54+
55+
const { getByText } = render(
56+
<FilePreview version={versionWithoutFilename} />
57+
);
58+
59+
// Check for fallback behavior
60+
expect(getByText(versionWithoutFilename.url)).toBeInTheDocument();
61+
});
62+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* Main container for the file preview */
2+
.file-preview {
3+
max-width: 600px;
4+
margin: 0 auto;
5+
font-family: Arial, sans-serif;
6+
}
7+
8+
/* Info section - acts like a card */
9+
.file-preview-info {
10+
border: 1px solid #ddd;
11+
border-radius: 8px;
12+
padding: 20px;
13+
background-color: #f9f9f9;
14+
margin-bottom: 20px;
15+
}
16+
17+
/* Title of the info section */
18+
.file-preview-title {
19+
margin: 0 0 15px 0;
20+
font-size: 1.5em;
21+
color: #333;
22+
}
23+
24+
/* Container for all details */
25+
.file-preview-details {
26+
margin-bottom: 20px;
27+
}
28+
29+
/* Individual detail item */
30+
.file-preview-detail {
31+
margin-bottom: 10px;
32+
line-height: 1.4;
33+
}
34+
35+
.file-preview-detail strong {
36+
display: inline-block;
37+
min-width: 100px;
38+
color: #555;
39+
}
40+
41+
/* Hash display */
42+
.file-preview-hash {
43+
background-color: #eee;
44+
padding: 2px 4px;
45+
border-radius: 3px;
46+
font-family: monospace;
47+
font-size: 0.9em;
48+
padding: 2px 4px;
49+
border-radius: 3px;
50+
word-break: break-all;
51+
}
52+
53+
/* Actions container */
54+
.file-preview-actions {
55+
text-align: center;
56+
margin-top: 1em;
57+
display: flex;
58+
gap: 1em;
59+
flex-wrap: wrap;
60+
}
61+
62+
/* View link button */
63+
/* TODO: should share a unified, standard button style with rest of the app. */
64+
.file-preview-view {
65+
display: inline-block;
66+
padding: 10px 20px;
67+
background-color: #007bff;
68+
color: white;
69+
text-decoration: none;
70+
border-radius: 5px;
71+
transition: background-color 0.3s;
72+
}
73+
74+
.file-preview-view:hover,
75+
.file-preview-view:focus {
76+
background-color: #5b5b5b;
77+
color: white;
78+
text-decoration: none;
79+
}
80+
81+
/* Warning/note section */
82+
.file-preview-warning {
83+
background-color: #fff3cd;
84+
border: 1px solid #ffeaa7;
85+
border-radius: 5px;
86+
padding: 15px;
87+
color: #856404;
88+
}
89+
90+
.file-preview-warning p {
91+
margin: 0;
92+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { parseMediaType } from '../../scripts/media-type';
2+
import { humanReadableSize } from '../../scripts/formatters';
3+
import styles from './file-preview.css';
4+
5+
/**
6+
* @typedef {Object} FilePreviewProps
7+
* @property {Version} version
8+
*/
9+
10+
/**
11+
* Display basic information about a non-renderable file.
12+
*
13+
* @param {FilePreviewProps} props
14+
*/
15+
export default function FilePreview ({ version }) {
16+
const mediaType = parseMediaType(version.media_type);
17+
const fileName = extractFileName(version.url);
18+
const fileSize = formatFileSize(version.content_length);
19+
return (
20+
<div className={styles.filePreview}>
21+
<div className={styles.filePreviewInfo}>
22+
<h3 className={styles.filePreviewTitle}>File Information</h3>
23+
<div className={styles.filePreviewDetails}>
24+
<div className={styles.filePreviewDetail}>
25+
<strong>File Name:</strong> {fileName || version.url}
26+
</div>
27+
<div className={styles.filePreviewDetail}>
28+
<strong>Media Type:</strong> {mediaType.essence}
29+
</div>
30+
31+
<div className={styles.filePreviewDetail}>
32+
<strong>Size:</strong> {fileSize ? fileSize : 'Unknown'} :
33+
</div>
34+
{version.body_hash && (
35+
<div className={styles.filePreviewDetail}>
36+
<strong>Hash:</strong>
37+
<code className={styles.filePreviewHash}>{version.body_hash}</code>
38+
</div>
39+
)}
40+
</div>
41+
<div className={styles.filePreviewActions}>
42+
<a
43+
href={version.body_url}
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
className={styles.filePreviewView}
47+
>
48+
View Raw File
49+
</a>
50+
</div>
51+
</div>
52+
<div className={styles.filePreviewWarning}>
53+
<p>
54+
<strong>Note:</strong> This file type cannot be rendered inline.
55+
The above information shows basic file metadata. Use the button above to view/download.
56+
</p>
57+
</div>
58+
</div>
59+
);
60+
}
61+
62+
/**
63+
* Extract a filename from a URL. This essentially gives you the final
64+
* component of the URL's path. Returns null if the URL does not include
65+
* anything that that can be treated as a file name.
66+
* @param {string} url
67+
* @returns {string|null}
68+
*/
69+
function extractFileName (url) {
70+
if (!url) return null;
71+
72+
try {
73+
const urlObj = new URL(url);
74+
const pathname = urlObj.pathname;
75+
const filename = pathname.split('/').pop();
76+
return filename || null;
77+
}
78+
catch (e) {
79+
console.warn('Failed to parse URL for filename extraction:', e);
80+
return null;
81+
}
82+
};
83+
84+
/**
85+
* Format file size for display
86+
* @param {number} content_length
87+
* @returns {string|null}
88+
*/
89+
function formatFileSize (content_length) {
90+
if (typeof content_length === 'number') {
91+
return humanReadableSize(content_length);
92+
}
93+
94+
return null;
95+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* eslint-env jest */
2+
3+
/**
4+
* @jest-environment jsdom
5+
*/
6+
7+
import { render } from '@testing-library/react';
8+
import SideBySideFilePreview from '../../side-by-side-file-preview/side-by-side-file-preview';
9+
10+
// Mock the FilePreview component since we test it separately
11+
jest.mock('../../file-preview/file-preview', () => {
12+
return function MockFilePreview ({ version }) {
13+
return <div data-testid="file-preview">{version.uuid}</div>;
14+
};
15+
});
16+
17+
describe('SideBySideFilePreview', () => {
18+
const mockVersionA = {
19+
uuid: '1257a5f5-143b-40cd-86ec-4dfceaf361f1',
20+
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
21+
url: 'https://test.com/test_a.xlsx',
22+
body_hash: '21807b63cb',
23+
};
24+
25+
const mockVersionB = {
26+
uuid: '2c598723-2222-412e-9f55-bf229fb6ca6c',
27+
media_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28+
url: 'https://test.com/test_b.xlsx',
29+
body_hash: '21807b63fccb',
30+
};
31+
32+
it('renders both versions side by side', () => {
33+
const { getByText, getAllByTestId } = render(
34+
<SideBySideFilePreview
35+
a={mockVersionA}
36+
b={mockVersionB}
37+
/>
38+
);
39+
40+
expect(getByText('From Version')).toBeInTheDocument();
41+
expect(getByText('To Version')).toBeInTheDocument();
42+
43+
const filePreviews = getAllByTestId('file-preview');
44+
expect(filePreviews).toHaveLength(2);
45+
});
46+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.side-by-side-file-preview-container {
2+
display: flex;
3+
justify-content: center;
4+
gap: 20px;
5+
flex-wrap: wrap;
6+
}
7+
8+
.side-by-side-file-preview-container > * {
9+
flex: 1 1 16em;
10+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import FilePreview from '../file-preview/file-preview';
2+
import styles from './side-by-side-file-preview.css';
3+
4+
/**
5+
* @typedef {Object} SideBySideFilePreviewProps
6+
* @property {Version} a
7+
* @property {Version} b
8+
*/
9+
10+
/**
11+
* Display two non-renderable file previews, side-by-side.
12+
*
13+
* @param {SideBySideFilePreviewProps} props
14+
*/
15+
export default function SideBySideFilePreview ({ a, b }) {
16+
return (
17+
<div className={styles.sideBySideFilePreviewContainer}>
18+
<div>
19+
<h3>From Version</h3>
20+
<FilePreview version={a} />
21+
</div>
22+
23+
<div>
24+
<h3>To Version</h3>
25+
<FilePreview version={b} />
26+
</div>
27+
</div>
28+
);
29+
}

0 commit comments

Comments
 (0)