Skip to content

Commit bc76e51

Browse files
authored
Merge pull request #4779 from cloud-gov/fix-file-view-details-alert-flash
fix: Error alert flash when loading file details directly
2 parents 4ec625e + 2ed8065 commit bc76e51

File tree

8 files changed

+165
-93
lines changed

8 files changed

+165
-93
lines changed

api/serializers/site.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const allowedAttributes = [
1111
'defaultBranch',
1212
'domain',
1313
'engine',
14+
'liveDomain',
1415
'owner',
1516
'publishedAt',
1617
'repository',
@@ -58,6 +59,30 @@ const viewLinks = {
5859
viewLink: 'site',
5960
};
6061

62+
function getLiveDomain(branchConfigs, domains) {
63+
if (!branchConfigs || !domains) {
64+
return '';
65+
}
66+
67+
const config = branchConfigs.find((c) => c.context === 'site');
68+
69+
if (!config) {
70+
return '';
71+
}
72+
73+
const domain = domains
74+
.filter((d) => d.state === 'provisioned')
75+
.find((d) => d.siteBranchConfigId === config.id);
76+
77+
if (!domain || !domain?.names) {
78+
return '';
79+
}
80+
81+
const domainName = domain.names.split(',')[0];
82+
83+
return `https://${domainName}`;
84+
}
85+
6186
// Eventually replace `serialize`
6287
function serializeNew(site, isSystemAdmin = false) {
6388
const object = site.get({
@@ -96,6 +121,9 @@ function serializeNew(site, isSystemAdmin = false) {
96121
const serializeObject = (site, isSystemAdmin) => {
97122
const json = serializeNew(site, isSystemAdmin);
98123

124+
const liveDomain = getLiveDomain(json.SiteBranchConfigs, json.Domains);
125+
json.liveDomain = liveDomain;
126+
99127
if (json.Domains) {
100128
json.domains = site.Domains.map((d) =>
101129
pick(

frontend/hooks/useFileStorage.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ export default function useFileStorage(
6060
enabled: !!fileStorageId,
6161
staleTime: 2000,
6262
placeholderData: previousData.current || INITIAL_DATA,
63-
onError: (err) => {
64-
// using an empty string so that we don't end up with "undefined" at the end
65-
throw new Error('Failed to fetch public files ' + (err?.message || ''));
66-
},
6763
});
6864
useEffect(() => {
6965
if (data !== undefined) {

frontend/pages/sites/$siteId/storage/FileDetails.jsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const FileDetails = ({
88
name,
99
id,
1010
fullPath,
11-
updatedBy,
12-
updatedAt,
11+
lastModifiedBy,
12+
lastModifiedAt,
1313
size,
1414
mimeType,
1515
onDelete,
@@ -61,12 +61,12 @@ const FileDetails = ({
6161
</td>
6262
</tr>
6363
<tr>
64-
<th scope="row">Uploaded by</th>
65-
<td className="text-bold">{updatedBy}</td>
64+
<th scope="row">Last modified by</th>
65+
<td className="text-bold">{lastModifiedBy}</td>
6666
</tr>
6767
<tr>
68-
<th scope="row">Uploaded at</th>
69-
<td>{updatedAt && dateAndTimeSimple(updatedAt)}</td>
68+
<th scope="row">Last modified at</th>
69+
<td>{lastModifiedAt && dateAndTimeSimple(lastModifiedAt)}</td>
7070
</tr>
7171
<tr>
7272
<th scope="row">File size</th>
@@ -86,9 +86,10 @@ const FileDetails = ({
8686
type="button"
8787
title="Remove from public storage"
8888
className="usa-button usa-button--outline delete-button"
89-
onClick={() => {
89+
onClick={(e) => {
90+
e.preventDefault();
91+
9092
onDelete(thisItem);
91-
onClose();
9293
}}
9394
>
9495
Delete
@@ -105,8 +106,8 @@ FileDetails.propTypes = {
105106
name: PropTypes.string.isRequired,
106107
id: PropTypes.number.isRequired,
107108
fullPath: PropTypes.string.isRequired,
108-
updatedBy: PropTypes.string.isRequired,
109-
updatedAt: PropTypes.string.isRequired,
109+
lastModifiedBy: PropTypes.string.isRequired,
110+
lastModifiedAt: PropTypes.string.isRequired,
110111
size: PropTypes.number.isRequired,
111112
mimeType: PropTypes.string.isRequired,
112113
onDelete: PropTypes.func.isRequired,

frontend/pages/sites/$siteId/storage/FileDetails.test.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ describe('FileDetails', () => {
1717
id: 20,
1818
name: 'test-document.pdf',
1919
fullPath: 'https://example.com/files/test-document.pdf',
20-
updatedBy: '[email protected]',
21-
updatedAt: '2025-02-19T12:00:00Z',
20+
lastModifiedBy: '[email protected]',
21+
lastModifiedAt: '2025-02-19T12:00:00Z',
2222
size: 1048576, // 1MB
2323
mimeType: 'application/pdf',
2424
onDelete: mockOnDelete,
@@ -47,7 +47,7 @@ describe('FileDetails', () => {
4747
);
4848

4949
// Uploaded by
50-
expect(screen.getByText(fileProps.updatedBy)).toBeInTheDocument();
50+
expect(screen.getByText(fileProps.lastModifiedBy)).toBeInTheDocument();
5151

5252
// Uploaded on (formatted date check)
5353
expect(screen.getByText(/Feb 19, 2025/i)).toBeInTheDocument();

frontend/pages/sites/$siteId/storage/LocationBar.jsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@ import React, { useMemo } from 'react';
22
import PropTypes from 'prop-types';
33
import { IconGlobe } from '@shared/icons';
44

5-
const LocationBar = ({ path, storageRoot, onNavigate }) => {
5+
const LocationBar = ({ path, storageRoot, onNavigate, trailingSlash = true }) => {
66
// Only trim leading slashes, keep trailing
77
const cleanedPath = path.replace(/^\/+/, '');
88
const segments = cleanedPath ? cleanedPath.split('/').filter(Boolean) : [];
99
// Show storageRoot only if it's the current or parent folder
1010
const showDomain = segments.length <= 1;
11-
const grandparentPath = segments.slice(0, segments.length - 1).join('/') + '/';
11+
const grandparentPath = segments.slice(0, -2).join('/') + '/';
1212

1313
const displayedBreadcrumbs = useMemo(() => {
14+
const trailer = trailingSlash ? '/' : '';
1415
if (segments.length > 1) {
1516
// Ensure it only includes the last two segments
16-
return segments.slice(-2).map((seg) => `${seg}/`);
17+
return segments.slice(-2).map((seg, idx) => {
18+
if (idx === 1) return `${seg}${trailer}`;
19+
return `${seg}/`;
20+
});
1721
}
18-
return segments.length === 1 ? [`${segments[0]}/`] : [];
22+
return segments.length === 1 ? [`${segments[0]}${trailer}`] : [];
1923
}, [segments]);
2024

2125
return (
@@ -110,7 +114,8 @@ LocationBar.propTypes = {
110114
path: PropTypes.string.isRequired,
111115
siteId: PropTypes.string.isRequired,
112116
storageRoot: PropTypes.string.isRequired,
113-
onNavigate: PropTypes.func,
117+
onNavigate: PropTypes.func.isRequired,
118+
trailingSlash: PropTypes.bool,
114119
};
115120

116121
export default LocationBar;
Lines changed: 98 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import '@testing-library/jest-dom';
33
import { render, screen, fireEvent } from '@testing-library/react';
4-
import { MemoryRouter } from 'react-router-dom';
54
import LocationBar from './LocationBar';
65

76
describe('LocationBar Component', () => {
@@ -16,14 +15,12 @@ describe('LocationBar Component', () => {
1615
it('renders root directory correctly, in text (not link) if current directory', () => {
1716
render(
1817
/* MemoryRouter allows us to render components that use react Link */
19-
<MemoryRouter>
20-
<LocationBar
21-
path=""
22-
siteId={siteId}
23-
storageRoot={storageRoot}
24-
onNavigate={mockOnNavigate}
25-
/>
26-
</MemoryRouter>,
18+
<LocationBar
19+
path=""
20+
siteId={siteId}
21+
storageRoot={storageRoot}
22+
onNavigate={mockOnNavigate}
23+
/>,
2724
);
2825

2926
expect(screen.getByText(`${storageRoot}/`)).toBeInTheDocument();
@@ -35,65 +32,57 @@ describe('LocationBar Component', () => {
3532

3633
it('renders single-level folder correctly, with parent as link', () => {
3734
render(
38-
<MemoryRouter>
39-
<LocationBar
40-
path="foo"
41-
siteId={siteId}
42-
storageRoot={storageRoot}
43-
onNavigate={mockOnNavigate}
44-
/>
45-
</MemoryRouter>,
35+
<LocationBar
36+
path="foo"
37+
siteId={siteId}
38+
storageRoot={storageRoot}
39+
onNavigate={mockOnNavigate}
40+
/>,
4641
);
4742

4843
expect(screen.getByRole('link', { name: `${storageRoot}/` })).toBeInTheDocument();
4944
expect(screen.getByText('foo/')).toBeInTheDocument();
5045
});
5146

52-
test('renders nested folders with ../ link', () => {
47+
it('renders nested folders with ../ link', () => {
5348
render(
54-
<MemoryRouter>
55-
<LocationBar
56-
path="foo/bar"
57-
siteId={siteId}
58-
storageRoot={storageRoot}
59-
onNavigate={mockOnNavigate}
60-
/>
61-
</MemoryRouter>,
49+
<LocationBar
50+
path="foo/bar"
51+
siteId={siteId}
52+
storageRoot={storageRoot}
53+
onNavigate={mockOnNavigate}
54+
/>,
6255
);
6356

6457
expect(screen.getByRole('link', { name: '../' })).toBeInTheDocument();
6558
expect(screen.getByText('bar/')).toBeInTheDocument();
6659
});
6760

68-
it('clicking a breadcrumb calls onNavigate', () => {
61+
it('clicking a breadcrumb calls on parent and grandparent path', () => {
6962
render(
70-
<MemoryRouter>
71-
<LocationBar
72-
path="foo/bar"
73-
siteId={siteId}
74-
storageRoot={storageRoot}
75-
onNavigate={mockOnNavigate}
76-
/>
77-
</MemoryRouter>,
63+
<LocationBar
64+
path="baz/foo/bar"
65+
siteId={siteId}
66+
storageRoot={storageRoot}
67+
onNavigate={mockOnNavigate}
68+
/>,
7869
);
7970

8071
fireEvent.click(screen.getByRole('link', { name: '../' }));
81-
expect(mockOnNavigate).toHaveBeenCalledWith('foo/');
72+
expect(mockOnNavigate).toHaveBeenCalledWith('baz/');
8273

8374
fireEvent.click(screen.getByRole('link', { name: 'foo/' }));
84-
expect(mockOnNavigate).toHaveBeenCalledWith('foo/');
75+
expect(mockOnNavigate).toHaveBeenCalledWith('baz/foo/');
8576
});
8677

8778
it('renders deep nested path correctly', () => {
8879
render(
89-
<MemoryRouter>
90-
<LocationBar
91-
path="foo/bar/baz"
92-
siteId={siteId}
93-
storageRoot={storageRoot}
94-
onNavigate={mockOnNavigate}
95-
/>
96-
</MemoryRouter>,
80+
<LocationBar
81+
path="foo/bar/baz"
82+
siteId={siteId}
83+
storageRoot={storageRoot}
84+
onNavigate={mockOnNavigate}
85+
/>,
9786
);
9887

9988
expect(screen.getByRole('link', { name: '../' })).toBeInTheDocument();
@@ -102,31 +91,78 @@ describe('LocationBar Component', () => {
10291

10392
it('does not render ../ at root level', () => {
10493
render(
105-
<MemoryRouter>
106-
<LocationBar
107-
path=""
108-
siteId={siteId}
109-
storageRoot={storageRoot}
110-
onNavigate={mockOnNavigate}
111-
/>
112-
</MemoryRouter>,
94+
<LocationBar
95+
path=""
96+
siteId={siteId}
97+
storageRoot={storageRoot}
98+
onNavigate={mockOnNavigate}
99+
/>,
113100
);
114101

115102
expect(screen.queryByRole('link', { name: '../' })).not.toBeInTheDocument();
116103
});
117104

118-
test('clicking domain/~assets resets to root if it is a link', () => {
105+
it('clicking domain/~assets resets to root if it is a link', () => {
119106
render(
120-
<MemoryRouter>
121-
<LocationBar
122-
path="foo"
123-
siteId={siteId}
124-
storageRoot={storageRoot}
125-
onNavigate={mockOnNavigate}
126-
/>
127-
</MemoryRouter>,
107+
<LocationBar
108+
path="foo"
109+
siteId={siteId}
110+
storageRoot={storageRoot}
111+
onNavigate={mockOnNavigate}
112+
/>,
128113
);
129114
fireEvent.click(screen.getByRole('link', { name: `${storageRoot}/` }));
130115
expect(mockOnNavigate).toHaveBeenCalledWith('/');
131116
});
117+
118+
it('removes the trailing slash in deep nested node', () => {
119+
const fileName = 'qux.txt';
120+
const path = `foo/bar/bash/${fileName}`;
121+
render(
122+
<LocationBar
123+
path={path}
124+
siteId={siteId}
125+
storageRoot={storageRoot}
126+
onNavigate={mockOnNavigate}
127+
trailingSlash={false}
128+
/>,
129+
);
130+
131+
const span = screen.getByTitle(fileName);
132+
expect(span.textContent).toEqual(fileName);
133+
});
134+
135+
it('removes the trailing slash in root', () => {
136+
const fileName = 'qux.txt';
137+
const path = `/${fileName}`;
138+
render(
139+
<LocationBar
140+
path={path}
141+
siteId={siteId}
142+
storageRoot={storageRoot}
143+
onNavigate={mockOnNavigate}
144+
trailingSlash={false}
145+
/>,
146+
);
147+
148+
const span = screen.getByTitle(fileName);
149+
expect(span.textContent).toEqual(fileName);
150+
});
151+
152+
it('appends the trailing slash by default', () => {
153+
const fileName = 'qux.txt';
154+
const path = `foo/bar/bash/${fileName}`;
155+
render(
156+
<LocationBar
157+
path={path}
158+
siteId={siteId}
159+
storageRoot={storageRoot}
160+
onNavigate={mockOnNavigate}
161+
/>,
162+
);
163+
164+
const elementName = `${fileName}/`;
165+
const span = screen.getByTitle(elementName);
166+
expect(span.textContent).toEqual(elementName);
167+
});
132168
});

0 commit comments

Comments
 (0)