Skip to content

Commit 7c670de

Browse files
fiskusnl0
andauthored
Respect cfg.packageRoot when creating text files using UI (#4397)
Co-authored-by: Alexei Mochalov <[email protected]>
1 parent 21fbe9a commit 7c670de

File tree

10 files changed

+150
-94
lines changed

10 files changed

+150
-94
lines changed

catalog/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ where verb is one of
1818

1919
## Changes
2020

21+
- [Fixed] Respect `packageRoot` config property when creating text files using Catalog UI ([#4397](https://github.com/quiltdata/quilt/pull/4397))
2122
- [Changed] Qurator: Switch to Claude 3.7 Sonnet ([#4343](https://github.com/quiltdata/quilt/pull/4343))
2223
- [Fixed] Qurator: Ensure tools schemas adhere to draft 2020-12 ([#4343](https://github.com/quiltdata/quilt/pull/4343))
2324
- [Added] Support setting the location where files are uploaded while creating or promoting packages (using `packageRoot` config property) ([#4384](https://github.com/quiltdata/quilt/pull/4384))

catalog/app/components/FileEditor/CreateFile.tsx

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { join, extname } from 'path'
22

3+
import invariant from 'invariant'
34
import * as React from 'react'
45
import * as RRDom from 'react-router-dom'
56

67
import * as Dialog from 'components/Dialog'
7-
import * as NamedRoutes from 'utils/NamedRoutes'
88
import type { PackageHandle } from 'utils/packageHandle'
99

1010
import { isSupportedFileType } from './loader'
11+
import { useAddFileInBucket, useAddFileInPackage } from './routes'
1112

1213
function validateFileName(value: string) {
1314
if (!value) {
@@ -20,60 +21,46 @@ function validateFileName(value: string) {
2021
}
2122

2223
export function useCreateFileInBucket(bucket: string, path: string) {
23-
const { urls } = NamedRoutes.use()
2424
const history = RRDom.useHistory()
25+
const toFile = useAddFileInBucket(bucket)
2526

26-
// TODO: put this into FileEditor/routes
27-
const toFile = React.useCallback(
28-
(name: string) => urls.bucketFile(bucket, join(path, name), { edit: true }),
29-
[bucket, path, urls],
30-
)
31-
32-
const createFile = React.useCallback(
27+
const onSubmit = React.useCallback(
3328
(name: string) => {
34-
if (!name) return
35-
history.push(toFile(name))
29+
invariant(
30+
name,
31+
'`name` should be invalidated, and `onSubmit` should not be triggered',
32+
)
33+
history.push(toFile(join(path, name)))
3634
},
37-
[history, toFile],
35+
[history, toFile, path],
3836
)
3937

4038
return Dialog.usePrompt({
41-
onSubmit: createFile,
39+
onSubmit,
4240
initialValue: 'README.md',
4341
title: 'Enter file name',
4442
validate: validateFileName,
4543
})
4644
}
4745

48-
export function useCreateFileInPackage({ bucket, name }: PackageHandle, prefix?: string) {
49-
const { urls } = NamedRoutes.use()
46+
export function useCreateFileInPackage(packageHandle: PackageHandle, prefix?: string) {
5047
const history = RRDom.useHistory()
48+
const toFile = useAddFileInPackage(packageHandle)
5149

52-
// TODO: put this into FileEditor/routes
53-
const toFile = React.useCallback(
54-
(fileName: string) => {
55-
const next = urls.bucketPackageDetail(bucket, name, { action: 'revisePackage' })
56-
const key = join(name, fileName)
57-
return urls.bucketFile(bucket, key, {
58-
add: fileName,
59-
edit: true,
60-
next,
61-
})
62-
},
63-
[bucket, name, urls],
64-
)
65-
66-
const createFile = React.useCallback(
50+
const onSubmit = React.useCallback(
6751
(fileName: string) => {
68-
if (!fileName) return
52+
invariant(
53+
fileName,
54+
'`fileName` should be invalidated, and `onSubmit` should not be triggered',
55+
)
6956
history.push(toFile(fileName))
7057
},
7158
[history, toFile],
7259
)
7360

7461
const defaultFileName = 'README.md'
7562
return Dialog.usePrompt({
76-
onSubmit: createFile,
63+
onSubmit,
7764
initialValue: prefix ? `${prefix}${defaultFileName}` : defaultFileName,
7865
title: 'Enter file name',
7966
validate: validateFileName,

catalog/app/components/FileEditor/routes.spec.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { renderHook } from '@testing-library/react-hooks'
22

33
import {
4-
editFileInPackage,
54
useAddFileInPackage,
5+
useAddFileInBucket,
66
useEditFileInPackage,
77
useParams,
88
} from './routes'
99

10+
jest.mock(
11+
'constants/config',
12+
jest.fn(() => ({
13+
packageRoot: 'ro/ot',
14+
})),
15+
)
16+
1017
const useParamsInternal = jest.fn(
1118
() =>
1219
({
@@ -34,24 +41,15 @@ jest.mock('utils/NamedRoutes', () => ({
3441
}))
3542

3643
describe('components/FileEditor/routes', () => {
37-
describe('editFileInPackage', () => {
38-
it('should create url', () => {
39-
expect(
40-
editFileInPackage(urls, { bucket: 'bucket', key: 'key' }, 'logicalKey', 'next'),
41-
).toEqual('bucketFile(bucket, key, {"add":"logicalKey","edit":true,"next":"next"})')
42-
})
43-
})
44-
4544
describe('useEditFileInPackage', () => {
4645
it('should create url with redirect to package', () => {
4746
const { result } = renderHook(() =>
4847
useEditFileInPackage(
4948
{ bucket: 'b', name: 'n', hash: 'h' },
5049
{ bucket: 'b', key: 'k' },
51-
'lk',
5250
),
5351
)
54-
expect(result.current).toBe(
52+
expect(result.current('lk')).toBe(
5553
'bucketFile(b, k, {"add":"lk","edit":true,"next":"bucketPackageDetail(b, n, {\\"action\\":\\"revisePackage\\"})"})',
5654
)
5755
})
@@ -60,14 +58,21 @@ describe('components/FileEditor/routes', () => {
6058
describe('useAddFileInPackage', () => {
6159
it('should create url for the new file', () => {
6260
const { result } = renderHook(() =>
63-
useAddFileInPackage({ bucket: 'b', name: 'n', hash: 'h' }, 'lk'),
61+
useAddFileInPackage({ bucket: 'b', name: 'n', hash: 'h' }),
6462
)
65-
expect(result.current).toBe(
66-
'bucketFile(b, n/lk, {"add":"lk","edit":true,"next":"bucketPackageDetail(b, n, {\\"action\\":\\"revisePackage\\"})"})',
63+
expect(result.current('lk')).toBe(
64+
'bucketFile(b, ro/ot/n/lk, {"add":"lk","edit":true,"next":"bucketPackageDetail(b, n, {\\"action\\":\\"revisePackage\\"})"})',
6765
)
6866
})
6967
})
7068

69+
describe('useAddFileInBucket', () => {
70+
it('should create url for the new file in a bucket', () => {
71+
const { result } = renderHook(() => useAddFileInBucket('b'))
72+
expect(result.current('lk')).toBe(`bucketFile(b, lk, {"edit":true})`)
73+
})
74+
})
75+
7176
describe('useParams', () => {
7277
it('should throw error when no bucket', () => {
7378
useParamsInternal.mockImplementationOnce(() => ({}))

catalog/app/components/FileEditor/routes.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { join } from 'path'
2-
31
import invariant from 'invariant'
42
import * as React from 'react'
53
import * as RRDom from 'react-router-dom'
64

5+
import cfg from 'constants/config'
76
import type * as Routes from 'constants/routes'
87
import type * as Model from 'model'
98
import * as NamedRoutes from 'utils/NamedRoutes'
@@ -15,38 +14,52 @@ interface RouteMap {
1514
bucketPackageDetail: Routes.BucketPackageDetailArgs
1615
}
1716

18-
export function editFileInPackage(
19-
urls: NamedRoutes.Urls<RouteMap>,
20-
handle: Model.S3.S3ObjectLocation,
21-
logicalKey: string,
22-
next: string,
23-
) {
24-
return urls.bucketFile(handle.bucket, handle.key, {
25-
add: logicalKey,
26-
edit: true,
27-
next,
28-
})
29-
}
30-
3117
export function useEditFileInPackage(
3218
packageHandle: PackageHandle,
3319
fileHandle: Model.S3.S3ObjectLocation,
34-
logicalKey: string,
35-
) {
20+
): (logicalKey: string) => string {
3621
const { urls } = NamedRoutes.use<RouteMap>()
37-
const { bucket, name } = packageHandle
38-
const next = urls.bucketPackageDetail(bucket, name, { action: 'revisePackage' })
39-
return editFileInPackage(urls, fileHandle, logicalKey, next)
22+
return React.useCallback(
23+
(logicalKey: string) =>
24+
urls.bucketFile(fileHandle.bucket, fileHandle.key, {
25+
add: logicalKey,
26+
edit: true,
27+
next: urls.bucketPackageDetail(packageHandle.bucket, packageHandle.name, {
28+
action: 'revisePackage',
29+
}),
30+
}),
31+
[fileHandle, packageHandle, urls],
32+
)
33+
}
34+
35+
export function useAddFileInPackage(
36+
packageHandle: PackageHandle,
37+
): (logicalKey: string) => string {
38+
const { urls } = NamedRoutes.use<RouteMap>()
39+
return React.useCallback(
40+
(logicalKey: string) => {
41+
const { bucket, name } = packageHandle
42+
invariant(logicalKey, '`logicalKey` can not be empty')
43+
return urls.bucketFile(
44+
bucket,
45+
s3paths.canonicalKey(name, logicalKey, cfg.packageRoot),
46+
{
47+
add: logicalKey,
48+
edit: true,
49+
next: urls.bucketPackageDetail(bucket, name, { action: 'revisePackage' }),
50+
},
51+
)
52+
},
53+
[packageHandle, urls],
54+
)
4055
}
4156

42-
export function useAddFileInPackage({ bucket, name }: PackageHandle, logicalKey: string) {
57+
export function useAddFileInBucket(bucket: string): (logicalKey: string) => string {
4358
const { urls } = NamedRoutes.use<RouteMap>()
44-
const next = urls.bucketPackageDetail(bucket, name, { action: 'revisePackage' })
45-
const fileHandle = React.useMemo(
46-
() => ({ bucket, key: join(name, logicalKey) }),
47-
[bucket, logicalKey, name],
59+
return React.useCallback(
60+
(logicalKey: string) => urls.bucketFile(bucket, logicalKey, { edit: true }),
61+
[bucket, urls],
4862
)
49-
return editFileInPackage(urls, fileHandle, logicalKey, next)
5063
}
5164

5265
export function useParams() {

catalog/app/containers/Bucket/PackageDialog/PackageCreationForm.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,17 +332,12 @@ function PackageCreationForm({
332332
}
333333
}
334334

335-
const prefix = s3paths.withoutPrefix(
336-
'/',
337-
`${s3paths.ensureNoSlash(cfg.packageRoot || '')}/${name}`,
338-
)
339-
340335
let uploadedEntries
341336
try {
342337
uploadedEntries = await uploads.upload({
343338
files: toUpload,
344339
bucket: successor.slug,
345-
prefix,
340+
getCanonicalKey: (path) => s3paths.canonicalKey(name, path, cfg.packageRoot),
346341
getMeta: (path) => files.existing[path]?.meta || files.added[path]?.meta,
347342
})
348343
} catch (e) {

catalog/app/containers/Bucket/PackageDialog/Uploads.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ export function useUploads() {
6868
async ({
6969
files,
7070
bucket,
71-
prefix,
71+
getCanonicalKey,
7272
getMeta,
7373
}: {
7474
files: { path: string; file: LocalFile }[]
7575
bucket: string
76-
prefix: string
76+
getCanonicalKey: (path: string) => string
7777
getMeta?: (path: string) => Model.EntryMeta | undefined
7878
}) => {
7979
const limit = pLimit(2)
@@ -89,7 +89,7 @@ export function useUploads() {
8989
const upload: S3.ManagedUpload = s3.upload(
9090
{
9191
Bucket: bucket,
92-
Key: `${prefix}/${path}`,
92+
Key: getCanonicalKey(path),
9393
Body: file,
9494
},
9595
{

catalog/app/containers/Bucket/PackageTree/PackageTree.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,11 @@ function FileDisplay({
697697
[file, packageHandle],
698698
)
699699

700-
const editUrl = FileEditor.useEditFileInPackage(packageHandle, handle, path)
701-
const handleEdit = React.useCallback(() => history.push(editUrl), [editUrl, history])
700+
const editUrl = FileEditor.useEditFileInPackage(packageHandle, handle)
701+
const handleEdit = React.useCallback(
702+
() => history.push(editUrl(path)),
703+
[editUrl, history, path],
704+
)
702705
const packageUri = React.useMemo(
703706
() => ({
704707
...packageHandle,

catalog/app/containers/Bucket/Summarize.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -692,22 +692,18 @@ export function ConfigureAppearance({
692692
path,
693693
}: ConfigureAppearanceProps) {
694694
const classes = useConfigureAppearanceStyles()
695-
const readme = FileEditor.useAddFileInPackage(
696-
packageHandle,
697-
join(path || '', 'README.md'),
698-
)
699-
const summarize = FileEditor.useAddFileInPackage(
700-
packageHandle,
701-
join(path || '', 'quilt_summarize.json'),
702-
)
695+
const toFile = FileEditor.useAddFileInPackage(packageHandle)
703696
return (
704697
<div className={classes.root}>
705698
{!hasSummarizeJson && (
706699
<StyledTooltip
707700
maxWidth="md"
708701
title="Open the editor to author a quilt_summarize.json file. Upon saving, a package revision dialog will show up, letting you add that file to the package."
709702
>
710-
<RRDom.Link to={summarize} className={classes.button}>
703+
<RRDom.Link
704+
to={toFile(join(path || '', 'quilt_summarize.json'))}
705+
className={classes.button}
706+
>
711707
<M.Button color="primary" size="small" variant="outlined">
712708
Configure Summary
713709
</M.Button>
@@ -719,7 +715,10 @@ export function ConfigureAppearance({
719715
maxWidth="md"
720716
title="Open the editor to author a README file. Upon saving, a package revision dialog will show up, letting you add that file to the package."
721717
>
722-
<RRDom.Link to={readme} className={classes.button}>
718+
<RRDom.Link
719+
to={toFile(join(path || '', 'README.md'))}
720+
className={classes.button}
721+
>
723722
<M.Button color="primary" size="small" variant="contained">
724723
Add README
725724
</M.Button>

catalog/app/utils/s3paths.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { canonicalKey } from './s3paths'
2+
3+
describe('utils/s3paths', () => {
4+
describe('canonicalKey', () => {
5+
it('produces the key prefixed by package name', () => {
6+
expect(canonicalKey('foo/bar', 'README.md')).toBe('foo/bar/README.md')
7+
expect(canonicalKey('foo/bar', 'one/two two/three three three/README.md')).toBe(
8+
'foo/bar/one/two two/three three three/README.md',
9+
)
10+
expect(canonicalKey('foo/bar', 'one/two two/three three three/README.md', '')).toBe(
11+
'foo/bar/one/two two/three three three/README.md',
12+
)
13+
})
14+
15+
it('throws when logicalKey or package name is empty', () => {
16+
expect(() => canonicalKey('foo/bar', '', 'root')).toThrow()
17+
expect(() => canonicalKey('', 'foo/bar', 'root')).toThrow()
18+
})
19+
20+
it('produces the key prefixed by package name and packageRoot', () => {
21+
expect(canonicalKey('foo/bar', 'READ/ME.md', 'root')).toBe(
22+
'root/foo/bar/READ/ME.md',
23+
)
24+
expect(canonicalKey('foo/bar', 'READ/ME.md', '/root/')).toBe(
25+
'root/foo/bar/READ/ME.md',
26+
)
27+
expect(canonicalKey('foo/bar', 'READ?/!ME.md', '//root//')).toBe(
28+
'root/foo/bar/READ?/!ME.md',
29+
)
30+
expect(canonicalKey('foo/bar', 'READ/ME.md', 'one/two two/three three three')).toBe(
31+
'one/two two/three three three/foo/bar/READ/ME.md',
32+
)
33+
})
34+
})
35+
})

0 commit comments

Comments
 (0)