Skip to content

Commit e48427e

Browse files
authored
feat(ui): expose refresh method to list drawer context (#13173)
1 parent 7ae4f8c commit e48427e

File tree

9 files changed

+294
-51
lines changed

9 files changed

+294
-51
lines changed

packages/ui/src/elements/ListDrawer/DrawerContent.tsx

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
'use client'
2-
import type { ListQuery } from 'payload'
2+
import type { CollectionSlug, ListQuery } from 'payload'
33

44
import { useModal } from '@faceless-ui/modal'
55
import { hoistQueryParamsToAnd } from 'payload/shared'
66
import React, { useCallback, useEffect, useState } from 'react'
77

8+
import type { ListDrawerContextProps, ListDrawerContextType } from '../ListDrawer/Provider.js'
89
import type { ListDrawerProps } from './types.js'
910

1011
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
@@ -25,7 +26,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
2526
onBulkSelect,
2627
onSelect,
2728
overrideEntityVisibility = true,
28-
selectedCollection: selectedCollectionFromProps,
29+
selectedCollection: collectionSlugFromProps,
2930
}) => {
3031
const { closeModal, isModalOpen } = useModal()
3132

@@ -45,7 +46,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
4546
})
4647

4748
const [selectedOption, setSelectedOption] = useState<Option<string>>(() => {
48-
const initialSelection = selectedCollectionFromProps || enabledCollections[0]?.slug
49+
const initialSelection = collectionSlugFromProps || enabledCollections[0]?.slug
4950
const found = getEntityConfig({ collectionSlug: initialSelection })
5051

5152
return found
@@ -61,20 +62,25 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
6162
collectionSlug: selectedOption.value,
6263
})
6364

64-
const updateSelectedOption = useEffectEvent((selectedCollectionFromProps: string) => {
65-
if (selectedCollectionFromProps && selectedCollectionFromProps !== selectedOption?.value) {
65+
const updateSelectedOption = useEffectEvent((collectionSlug: CollectionSlug) => {
66+
if (collectionSlug && collectionSlug !== selectedOption?.value) {
6667
setSelectedOption({
67-
label: getEntityConfig({ collectionSlug: selectedCollectionFromProps })?.labels,
68-
value: selectedCollectionFromProps,
68+
label: getEntityConfig({ collectionSlug })?.labels,
69+
value: collectionSlug,
6970
})
7071
}
7172
})
7273

7374
useEffect(() => {
74-
updateSelectedOption(selectedCollectionFromProps)
75-
}, [selectedCollectionFromProps])
76-
77-
const renderList = useCallback(
75+
updateSelectedOption(collectionSlugFromProps)
76+
}, [collectionSlugFromProps])
77+
78+
/**
79+
* This performs a full server round trip to get the list view for the selected collection.
80+
* On the server, the data is freshly queried for the list view and all components are fully rendered.
81+
* This work includes building column state, rendering custom components, etc.
82+
*/
83+
const refresh = useCallback(
7884
async ({ slug, query }: { query?: ListQuery; slug: string }) => {
7985
try {
8086
const newQuery: ListQuery = { ...(query || {}), where: { ...(query?.where || {}) } }
@@ -129,9 +135,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
129135

130136
useEffect(() => {
131137
if (!ListView) {
132-
void renderList({ slug: selectedOption?.value })
138+
void refresh({ slug: selectedOption?.value })
133139
}
134-
}, [renderList, ListView, selectedOption.value])
140+
}, [refresh, ListView, selectedOption.value])
135141

136142
const onCreateNew = useCallback(
137143
({ doc }) => {
@@ -149,19 +155,33 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
149155
[closeModal, documentDrawerSlug, drawerSlug, onSelect, selectedOption.value],
150156
)
151157

152-
const onQueryChange = useCallback(
153-
(query: ListQuery) => {
154-
void renderList({ slug: selectedOption?.value, query })
158+
const onQueryChange: ListDrawerContextProps['onQueryChange'] = useCallback(
159+
(query) => {
160+
void refresh({ slug: selectedOption?.value, query })
155161
},
156-
[renderList, selectedOption.value],
162+
[refresh, selectedOption.value],
157163
)
158164

159-
const setMySelectedOption = useCallback(
160-
(incomingSelection: Option<string>) => {
165+
const setMySelectedOption: ListDrawerContextProps['setSelectedOption'] = useCallback(
166+
(incomingSelection) => {
161167
setSelectedOption(incomingSelection)
162-
void renderList({ slug: incomingSelection?.value })
168+
void refresh({ slug: incomingSelection?.value })
169+
},
170+
[refresh],
171+
)
172+
173+
const refreshSelf: ListDrawerContextType['refresh'] = useCallback(
174+
async (incomingCollectionSlug) => {
175+
if (incomingCollectionSlug) {
176+
setSelectedOption({
177+
label: getEntityConfig({ collectionSlug: incomingCollectionSlug })?.labels,
178+
value: incomingCollectionSlug,
179+
})
180+
}
181+
182+
await refresh({ slug: selectedOption.value || incomingCollectionSlug })
163183
},
164-
[renderList],
184+
[getEntityConfig, refresh, selectedOption.value],
165185
)
166186

167187
if (isLoading) {
@@ -178,6 +198,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
178198
onBulkSelect={onBulkSelect}
179199
onQueryChange={onQueryChange}
180200
onSelect={onSelect}
201+
refresh={refreshSelf}
181202
selectedOption={selectedOption}
182203
setSelectedOption={setMySelectedOption}
183204
>

packages/ui/src/elements/ListDrawer/Provider.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@ export type ListDrawerContextProps = {
2424
*/
2525
docID: string
2626
}) => void
27-
readonly selectedOption?: Option<string>
28-
readonly setSelectedOption?: (option: Option<string>) => void
27+
readonly selectedOption?: Option<CollectionSlug>
28+
readonly setSelectedOption?: (option: Option<CollectionSlug>) => void
2929
}
3030

3131
export type ListDrawerContextType = {
32-
isInDrawer: boolean
32+
readonly isInDrawer: boolean
33+
/**
34+
* When called, will either refresh the list view with its currently selected collection.
35+
* If an collection slug is provided, will use that instead of the currently selected one.
36+
*/
37+
readonly refresh: (collectionSlug?: CollectionSlug) => Promise<void>
3338
} & ListDrawerContextProps
3439

3540
export const ListDrawerContext = createContext({} as ListDrawerContextType)
3641

3742
export const ListDrawerContextProvider: React.FC<
3843
{
3944
children: React.ReactNode
45+
refresh: ListDrawerContextType['refresh']
4046
} & ListDrawerContextProps
4147
> = ({ children, ...rest }) => {
4248
return (

packages/ui/src/elements/ListDrawer/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ export const ListDrawer: React.FC<ListDrawerProps> = (props) => {
5151
)
5252
}
5353

54+
/**
55+
* Returns an array containing the ListDrawer component, the ListDrawerToggler component, and an object with state and methods for controlling the drawer.
56+
* @example
57+
* import { useListDrawer } from '@payloadcms/ui'
58+
*
59+
* // inside a React component
60+
* const [ListDrawer, ListDrawerToggler, { closeDrawer, openDrawer }] = useListDrawer({
61+
* collectionSlugs: ['users'],
62+
* selectedCollection: 'users',
63+
* })
64+
*
65+
* // inside the return statement
66+
* return (
67+
* <>
68+
* <ListDrawer />
69+
* <ListDrawerToggler onClick={openDrawer}>Open List Drawer</ListDrawerToggler>
70+
* </>
71+
* )
72+
*/
5473
export const useListDrawer: UseListDrawer = ({
5574
collectionSlugs: collectionSlugsFromProps,
5675
filterOptions,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
import { toast, useListDrawer, useListDrawerContext, useTranslation } from '@payloadcms/ui'
3+
import React, { useCallback } from 'react'
4+
5+
export const CustomListDrawer = () => {
6+
const [isCreating, setIsCreating] = React.useState(false)
7+
8+
// this is the _outer_ drawer context (if any), not the one for the list drawer below
9+
const { refresh } = useListDrawerContext()
10+
const { t } = useTranslation()
11+
12+
const [ListDrawer, ListDrawerToggler] = useListDrawer({
13+
collectionSlugs: ['custom-list-drawer'],
14+
})
15+
16+
const createDoc = useCallback(async () => {
17+
if (isCreating) {
18+
return
19+
}
20+
21+
setIsCreating(true)
22+
23+
try {
24+
await fetch('/api/custom-list-drawer', {
25+
body: JSON.stringify({}),
26+
credentials: 'include',
27+
headers: {
28+
'Content-Type': 'application/json',
29+
},
30+
method: 'POST',
31+
})
32+
33+
setIsCreating(false)
34+
35+
toast.success(
36+
t('general:successfullyCreated', {
37+
label: 'Custom List Drawer',
38+
}),
39+
)
40+
41+
// In the root document view, there is no outer drawer context, so this will be `undefined`
42+
if (typeof refresh === 'function') {
43+
await refresh()
44+
}
45+
} catch (_err) {
46+
console.error('Error creating document:', _err) // eslint-disable-line no-console
47+
setIsCreating(false)
48+
}
49+
}, [isCreating, refresh, t])
50+
51+
return (
52+
<div>
53+
<button id="create-custom-list-drawer-doc" onClick={createDoc} type="button">
54+
{isCreating ? 'Creating...' : 'Create Document'}
55+
</button>
56+
<ListDrawer />
57+
<ListDrawerToggler id="open-custom-list-drawer">Open list drawer</ListDrawerToggler>
58+
</div>
59+
)
60+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const CustomListDrawer: CollectionConfig = {
4+
slug: 'custom-list-drawer',
5+
fields: [
6+
{
7+
name: 'customListDrawer',
8+
type: 'ui',
9+
admin: {
10+
components: {
11+
Field: '/collections/CustomListDrawer/Component.js#CustomListDrawer',
12+
},
13+
},
14+
},
15+
],
16+
}

test/admin/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
55
import { Array } from './collections/Array.js'
66
import { BaseListFilter } from './collections/BaseListFilter.js'
77
import { CustomFields } from './collections/CustomFields/index.js'
8+
import { CustomListDrawer } from './collections/CustomListDrawer/index.js'
89
import { CustomViews1 } from './collections/CustomViews1.js'
910
import { CustomViews2 } from './collections/CustomViews2.js'
1011
import { DisableBulkEdit } from './collections/DisableBulkEdit.js'
@@ -185,6 +186,7 @@ export default buildConfigWithDefaults({
185186
Placeholder,
186187
UseAsTitleGroupField,
187188
DisableBulkEdit,
189+
CustomListDrawer,
188190
],
189191
globals: [
190192
GlobalHidden,

test/admin/e2e/list-view/e2e.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,42 @@ describe('List View', () => {
16761676

16771677
await expect(page.locator('.list-selection')).toContainText('2 selected')
16781678
})
1679+
1680+
test('should refresh custom list drawer using the refresh method from context', async () => {
1681+
const url = new AdminUrlUtil(serverURL, 'custom-list-drawer')
1682+
1683+
await payload.delete({
1684+
collection: 'custom-list-drawer',
1685+
where: { id: { exists: true } },
1686+
})
1687+
1688+
const { id } = await payload.create({
1689+
collection: 'custom-list-drawer',
1690+
data: {},
1691+
})
1692+
1693+
await page.goto(url.list)
1694+
1695+
await expect(page.locator('.table > table > tbody > tr')).toHaveCount(1)
1696+
1697+
await page.goto(url.edit(id))
1698+
1699+
await page.locator('#open-custom-list-drawer').click()
1700+
const drawer = page.locator('[id^=list-drawer_1_]')
1701+
await expect(drawer).toBeVisible()
1702+
1703+
await expect(drawer.locator('.table > table > tbody > tr')).toHaveCount(1)
1704+
1705+
await drawer.locator('.list-header__create-new-button.doc-drawer__toggler').click()
1706+
const createNewDrawer = page.locator('[id^=doc-drawer_custom-list-drawer_1_]')
1707+
await createNewDrawer.locator('#create-custom-list-drawer-doc').click()
1708+
1709+
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
1710+
1711+
await createNewDrawer.locator('.doc-drawer__header-close').click()
1712+
1713+
await expect(drawer.locator('.table > table > tbody > tr')).toHaveCount(2)
1714+
})
16791715
})
16801716

16811717
async function createPost(overrides?: Partial<Post>): Promise<Post> {

test/admin/payload-types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export interface Config {
9393
placeholder: Placeholder;
9494
'use-as-title-group-field': UseAsTitleGroupField;
9595
'disable-bulk-edit': DisableBulkEdit;
96+
'custom-list-drawer': CustomListDrawer;
9697
'payload-locked-documents': PayloadLockedDocument;
9798
'payload-preferences': PayloadPreference;
9899
'payload-migrations': PayloadMigration;
@@ -125,6 +126,7 @@ export interface Config {
125126
placeholder: PlaceholderSelect<false> | PlaceholderSelect<true>;
126127
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
127128
'disable-bulk-edit': DisableBulkEditSelect<false> | DisableBulkEditSelect<true>;
129+
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
128130
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
129131
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
130132
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -565,6 +567,15 @@ export interface DisableBulkEdit {
565567
updatedAt: string;
566568
createdAt: string;
567569
}
570+
/**
571+
* This interface was referenced by `Config`'s JSON-Schema
572+
* via the `definition` "custom-list-drawer".
573+
*/
574+
export interface CustomListDrawer {
575+
id: string;
576+
updatedAt: string;
577+
createdAt: string;
578+
}
568579
/**
569580
* This interface was referenced by `Config`'s JSON-Schema
570581
* via the `definition` "payload-locked-documents".
@@ -675,6 +686,10 @@ export interface PayloadLockedDocument {
675686
| ({
676687
relationTo: 'disable-bulk-edit';
677688
value: string | DisableBulkEdit;
689+
} | null)
690+
| ({
691+
relationTo: 'custom-list-drawer';
692+
value: string | CustomListDrawer;
678693
} | null);
679694
globalSlug?: string | null;
680695
user: {
@@ -1074,6 +1089,14 @@ export interface DisableBulkEditSelect<T extends boolean = true> {
10741089
updatedAt?: T;
10751090
createdAt?: T;
10761091
}
1092+
/**
1093+
* This interface was referenced by `Config`'s JSON-Schema
1094+
* via the `definition` "custom-list-drawer_select".
1095+
*/
1096+
export interface CustomListDrawerSelect<T extends boolean = true> {
1097+
updatedAt?: T;
1098+
createdAt?: T;
1099+
}
10771100
/**
10781101
* This interface was referenced by `Config`'s JSON-Schema
10791102
* via the `definition` "payload-locked-documents_select".

0 commit comments

Comments
 (0)