Skip to content

Commit 1ef1c55

Browse files
feat(ui): add option to open related documents in a new tab (#11939)
### What? Selected documents in a relationship field can be opened in a new tab. ### Why? Related documents can be edited using the edit icon which opens the document in a drawer. Sometimes users would like to open the document in a new tab instead to e.g. modify the related document at a later point in time. This currently requires users to find the related document via the list view and open it there. There is no easy way to find and open a related document. ### How? Adds custom handling to the relationship edit button to support opening it in a new tab via middle-click, Ctrl+click, or right-click → 'Open in new tab'. --------- Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
1 parent 055a263 commit 1ef1c55

File tree

7 files changed

+85
-24
lines changed

7 files changed

+85
-24
lines changed

packages/ui/src/elements/ReactSelect/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { LabelFunction } from 'payload'
22
import type { CommonProps, GroupBase, Props as ReactSelectStateManagerProps } from 'react-select'
33

4-
import type { DocumentDrawerProps, UseDocumentDrawer } from '../DocumentDrawer/types.js'
4+
import type { DocumentDrawerProps } from '../DocumentDrawer/types.js'
55

66
type CustomSelectProps = {
77
disableKeyDown?: boolean
88
disableMouseDown?: boolean
9-
DocumentDrawerToggler?: ReturnType<UseDocumentDrawer>[1]
109
draggableProps?: any
1110
droppableRef?: React.RefObject<HTMLDivElement | null>
1211
editableProps?: (
@@ -15,10 +14,11 @@ type CustomSelectProps = {
1514
selectProps: ReactSelectStateManagerProps,
1615
) => any
1716
onDelete?: DocumentDrawerProps['onDelete']
18-
onDocumentDrawerOpen?: (args: {
17+
onDocumentOpen?: (args: {
1918
collectionSlug: string
2019
hasReadPermission: boolean
2120
id: number | string
21+
openInNewTab?: boolean
2222
}) => void
2323
onDuplicate?: DocumentDrawerProps['onSave']
2424
onSave?: DocumentDrawerProps['onSave']

packages/ui/src/fields/Relationship/index.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from 'payload'
88

99
import { dequal } from 'dequal/lite'
10-
import { wordBoundariesRegex } from 'payload/shared'
10+
import { formatAdminURL, wordBoundariesRegex } from 'payload/shared'
1111
import * as qs from 'qs-esm'
1212
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
1313

@@ -83,7 +83,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
8383
const hasMultipleRelations = Array.isArray(relationTo)
8484

8585
const [currentlyOpenRelationship, setCurrentlyOpenRelationship] = useState<
86-
Parameters<ReactSelectAdapterProps['customProps']['onDocumentDrawerOpen']>[0]
86+
Parameters<ReactSelectAdapterProps['customProps']['onDocumentOpen']>[0]
8787
>({
8888
id: undefined,
8989
collectionSlug: undefined,
@@ -631,16 +631,29 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
631631
return r.test(labelString.slice(-breakApartThreshold))
632632
}, [])
633633

634-
const onDocumentDrawerOpen = useCallback<
635-
ReactSelectAdapterProps['customProps']['onDocumentDrawerOpen']
636-
>(({ id, collectionSlug, hasReadPermission }) => {
637-
openDrawerWhenRelationChanges.current = true
638-
setCurrentlyOpenRelationship({
639-
id,
640-
collectionSlug,
641-
hasReadPermission,
642-
})
643-
}, [])
634+
const onDocumentOpen = useCallback<ReactSelectAdapterProps['customProps']['onDocumentOpen']>(
635+
({ id, collectionSlug, hasReadPermission, openInNewTab }) => {
636+
if (openInNewTab) {
637+
if (hasReadPermission && id && collectionSlug) {
638+
const docUrl = formatAdminURL({
639+
adminRoute: config.routes.admin,
640+
path: `/collections/${collectionSlug}/${id}`,
641+
})
642+
643+
window.open(docUrl, '_blank')
644+
}
645+
} else {
646+
openDrawerWhenRelationChanges.current = true
647+
648+
setCurrentlyOpenRelationship({
649+
id,
650+
collectionSlug,
651+
hasReadPermission,
652+
})
653+
}
654+
},
655+
[setCurrentlyOpenRelationship, config.routes.admin],
656+
)
644657

645658
useEffect(() => {
646659
if (openDrawerWhenRelationChanges.current) {
@@ -697,7 +710,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
697710
customProps={{
698711
disableKeyDown: isDrawerOpen || isListDrawerOpen,
699712
disableMouseDown: isDrawerOpen || isListDrawerOpen,
700-
onDocumentDrawerOpen,
713+
onDocumentOpen,
701714
onSave,
702715
}}
703716
disabled={readOnly || disabled || isDrawerOpen || isListDrawerOpen}

packages/ui/src/fields/Relationship/select-components/MultiValueLabel/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const MultiValueLabel: React.FC<
2525
> = (props) => {
2626
const {
2727
data: { allowEdit, label, relationTo, value },
28-
selectProps: { customProps: { draggableProps, onDocumentDrawerOpen } = {} } = {},
28+
selectProps: { customProps: { draggableProps, onDocumentOpen } = {} } = {},
2929
} = props
3030

3131
const { permissions } = useAuth()
@@ -49,12 +49,13 @@ export const MultiValueLabel: React.FC<
4949
<button
5050
aria-label={`Edit ${label}`}
5151
className={`${baseClass}__drawer-toggler`}
52-
onClick={() => {
52+
onClick={(event) => {
5353
setShowTooltip(false)
54-
onDocumentDrawerOpen({
54+
onDocumentOpen({
5555
id: value,
5656
collectionSlug: relationTo,
5757
hasReadPermission,
58+
openInNewTab: event.metaKey || event.ctrlKey,
5859
})
5960
}}
6061
onKeyDown={(e) => {

packages/ui/src/fields/Relationship/select-components/SingleValue/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const SingleValue: React.FC<
2626
const {
2727
children,
2828
data: { allowEdit, label, relationTo, value },
29-
selectProps: { customProps: { onDocumentDrawerOpen } = {} } = {},
29+
selectProps: { customProps: { onDocumentOpen } = {} } = {},
3030
} = props
3131

3232
const [showTooltip, setShowTooltip] = useState(false)
@@ -44,12 +44,13 @@ export const SingleValue: React.FC<
4444
<button
4545
aria-label={t('general:editLabel', { label })}
4646
className={`${baseClass}__drawer-toggler`}
47-
onClick={() => {
47+
onClick={(event) => {
4848
setShowTooltip(false)
49-
onDocumentDrawerOpen({
49+
onDocumentOpen({
5050
id: value,
5151
collectionSlug: relationTo,
5252
hasReadPermission,
53+
openInNewTab: event.metaKey || event.ctrlKey,
5354
})
5455
}}
5556
onKeyDown={(e) => {

test/fields/collections/Relationship/e2e.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,46 @@ describe('relationship', () => {
358358
).toHaveText(`${value}123456`)
359359
})
360360

361+
test('should open related document in a new tab when meta key is applied', async () => {
362+
await page.goto(url.create)
363+
364+
const [newPage] = await Promise.all([
365+
page.context().waitForEvent('page'),
366+
await openDocDrawer({
367+
page,
368+
selector:
369+
'#field-relationWithAllowCreateToFalse .relationship--single-value__drawer-toggler',
370+
withMetaKey: true,
371+
}),
372+
])
373+
374+
// Wait for navigation to complete in the new tab and ensure the edit view is open
375+
await expect(newPage.locator('.collection-edit')).toBeVisible()
376+
})
377+
378+
test('multi value relationship should open document in a new tab', async () => {
379+
await page.goto(url.create)
380+
381+
// Select "Seeded text document" relationship
382+
await page.locator('#field-relationshipHasMany .rs__control').click()
383+
await page.locator('.rs__option:has-text("Seeded text document")').click()
384+
await expect(
385+
page.locator('#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler'),
386+
).toBeVisible()
387+
388+
const [newPage] = await Promise.all([
389+
page.context().waitForEvent('page'),
390+
await openDocDrawer({
391+
page,
392+
selector: '#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler',
393+
withMetaKey: true,
394+
}),
395+
])
396+
397+
// Wait for navigation to complete in the new tab and ensure the edit view is open
398+
await expect(newPage.locator('.collection-edit')).toBeVisible()
399+
})
400+
361401
// Drawers opened through the edit button are prone to issues due to the use of stopPropagation for certain
362402
// events - specifically for drawers opened through the edit button. This test is to ensure that drawers
363403
// opened through the edit button can be saved using the hotkey.

test/helpers/e2e/toggleDocDrawer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ import { wait } from 'payload/shared'
66
export async function openDocDrawer({
77
page,
88
selector,
9+
withMetaKey = false,
910
}: {
1011
page: Page
1112
selector: string
13+
withMetaKey?: boolean
1214
}): Promise<void> {
15+
let clickProperties = {}
16+
if (withMetaKey) {
17+
clickProperties = { modifiers: ['ControlOrMeta'] }
18+
}
1319
await wait(500) // wait for parent form state to initialize
14-
await page.locator(selector).click()
20+
await page.locator(selector).click(clickProperties)
1521
await wait(500) // wait for drawer form state to initialize
1622
}
1723

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
}
3232
],
3333
"paths": {
34-
"@payload-config": ["./test/_community/config.ts"],
34+
"@payload-config": ["./test/fields/config.ts"],
3535
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
3636
"@payloadcms/live-preview": ["./packages/live-preview/src"],
3737
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],

0 commit comments

Comments
 (0)