Skip to content

Commit 99d61db

Browse files
chore(ui): move unpublish actions into document controls dropdown (#15417)
### What / Why This PR updates and fixes aspects of the `unpublish` UI: 1. **Button location**: The **unpublish** button update in [v3.72.0](https://github.com/payloadcms/payload/releases/tag/v3.72.0) was reported as too prominent by the community. We have moved it into the additional document actions dropdown. 2. **Bug**: The `Unpublish in [locale]` option should only be displayed when `localizeStatus` is enabled. 3. **Performance**: In the `unpublish` component, several variables were recalculating on every render, causing unnecessary load and causing ESLint warnings. ### Where 1. In `DocumentControls`, the `unpublish` button has been moved from the top level into the additional document actions dropdown. 2. Within the `unpublish` button component, the conditional logic for `canUnpublishCurrentLocale` has been corrected so it will only show when `localizeStatus` is enabled. 3. Lastly, the variables `canUnpublish` and `canUnpublishCurrentLocale` have been wrapped in `useMemo` to prevent excessive recalculating #### Fixes #15291 <img width="1013" height="409" alt="Screenshot 2026-01-29 at 11 51 47 AM" src="https://github.com/user-attachments/assets/a630406d-e444-4ecd-ab69-ba84f85d4b3e" /> --------- Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
1 parent bd80784 commit 99d61db

4 files changed

Lines changed: 77 additions & 45 deletions

File tree

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,11 @@ export const DocumentControls: React.FC<{
277277
{(unsavedDraftWithValidations ||
278278
!autosaveEnabled ||
279279
(autosaveEnabled && showSaveDraftButton)) && (
280-
<RenderCustomComponent
281-
CustomComponent={CustomSaveDraftButton}
282-
Fallback={<SaveDraftButton />}
283-
/>
284-
)}
285-
<RenderCustomComponent
286-
CustomComponent={CustomUnpublishButton}
287-
Fallback={<UnpublishButton />}
288-
/>
280+
<RenderCustomComponent
281+
CustomComponent={CustomSaveDraftButton}
282+
Fallback={<SaveDraftButton />}
283+
/>
284+
)}
289285
<RenderCustomComponent
290286
CustomComponent={CustomPublishButton}
291287
Fallback={<PublishButton />}
@@ -403,6 +399,10 @@ export const DocumentControls: React.FC<{
403399
useAsTitle={collectionConfig?.admin?.useAsTitle}
404400
/>
405401
)}
402+
<RenderCustomComponent
403+
CustomComponent={CustomUnpublishButton}
404+
Fallback={<UnpublishButton />}
405+
/>
406406
{EditMenuItems}
407407
</PopupList.ButtonGroup>
408408
</Popup>

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

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import React, { useCallback, useEffect, useState } from 'react'
1010
import { toast } from 'sonner'
1111

1212
import { useForm } from '../../forms/Form/context.js'
13-
import { FormSubmit } from '../../forms/Submit/index.js'
1413
import { useConfig } from '../../providers/Config/index.js'
1514
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
1615
import { useLocale } from '../../providers/Locale/index.js'
@@ -41,7 +40,6 @@ export function UnpublishButton({
4140
const { reset: resetForm } = useForm()
4241
const { code: localeCode, label: localeLabel } = useLocale()
4342
const [unpublishAll, setUnpublishAll] = useState(false)
44-
4543
const unPublishModalSlug = `confirm-un-publish-${id}`
4644

4745
const {
@@ -63,7 +61,7 @@ export function UnpublishButton({
6361

6462
const unpublish = useCallback(
6563
(unpublishAll?: boolean) => {
66-
;(async () => {
64+
; (async () => {
6765
let url
6866
let method
6967

@@ -161,50 +159,57 @@ export function UnpublishButton({
161159
setHasLocalizedFields(hasLocalizedField)
162160
}, [entityConfig?.fields])
163161

164-
const canUnpublish = hasPublishPermission && hasPublishedDoc && !isTrashed
165-
const canUnpublishCurrentLocale = hasLocalizedFields && canUnpublish
162+
const canUnpublish = React.useMemo(
163+
() => hasPublishPermission && hasPublishedDoc && !isTrashed,
164+
[hasPublishPermission, hasPublishedDoc, isTrashed],
165+
)
166+
167+
const canUnpublishCurrentLocale = React.useMemo(() => {
168+
if (!canUnpublish || !hasLocalizedFields) { return false }
169+
170+
const drafts = entityConfig?.versions?.drafts
171+
const hasDraftsConfig = typeof drafts === 'object' && drafts !== null
172+
const localizeStatusConfigured = hasDraftsConfig && drafts.localizeStatus === true
173+
const experimentalLocalizeStatus =
174+
config.experimental &&
175+
'localizeStatus' in config.experimental &&
176+
config.experimental.localizeStatus === true
177+
178+
return localizeStatusConfigured && experimentalLocalizeStatus
179+
}, [canUnpublish, hasLocalizedFields, entityConfig?.versions?.drafts, config.experimental])
180+
181+
182+
const label = getTranslation(localeLabel, i18n)
166183

167184
return (
168185
<React.Fragment>
169186
{canUnpublish && (
170187
<>
171-
<FormSubmit
172-
buttonId="action-unpublish"
173-
disabled={!canUnpublish}
174-
enableSubMenu={canUnpublishCurrentLocale}
188+
{canUnpublish && <PopupList.Button
189+
id="action-unpublish"
175190
onClick={() => {
176191
setUnpublishAll(true)
177192
toggleModal(unPublishModalSlug)
178193
}}
179-
size="medium"
180-
SubMenuPopupContent={
181-
canUnpublishCurrentLocale
182-
? ({ close }) => {
183-
return (
184-
<PopupList.ButtonGroup>
185-
<PopupList.Button
186-
id="action-unpublish-locale"
187-
onClick={() => {
188-
setUnpublishAll(false)
189-
toggleModal(unPublishModalSlug)
190-
close()
191-
}}
192-
>
193-
{t('version:unpublishIn', { locale: getTranslation(localeLabel, i18n) })}
194-
</PopupList.Button>
195-
</PopupList.ButtonGroup>
196-
)
197-
}
198-
: undefined
199-
}
200-
type="button"
201194
>
202195
{labelProp || t('version:unpublish')}
203-
</FormSubmit>
196+
</PopupList.Button >}
197+
{
198+
canUnpublishCurrentLocale && <PopupList.Button
199+
id="action-unpublish-locale"
200+
onClick={() => {
201+
setUnpublishAll(false)
202+
toggleModal(unPublishModalSlug)
203+
close()
204+
}}
205+
>
206+
{labelProp ? labelProp + ` [${label}]` : t('version:unpublishIn', { locale: label })}
207+
</PopupList.Button>
208+
}
204209
<ConfirmationModal
205210
body={
206211
!unpublishAll
207-
? t('version:aboutToUnpublishIn', { locale: getTranslation(localeLabel, i18n) })
212+
? t('version:aboutToUnpublishIn', { locale: label })
208213
: t('version:aboutToUnpublish')
209214
}
210215
confirmingLabel={t('version:unpublishing')}
@@ -214,6 +219,6 @@ export function UnpublishButton({
214219
/>
215220
</>
216221
)}
217-
</React.Fragment>
222+
</React.Fragment >
218223
)
219224
}

test/localization/e2e.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,28 @@ describe('Localization', () => {
766766
expect(isActuallyVisible).toBe(true)
767767
})
768768

769+
describe('unpublish button', () => {
770+
test('should show unpublish in specific locale when localizeStatus is enabled', async () => {
771+
await page.goto(urlAllFieldsLocalized.create)
772+
await page.locator('#field-text').fill('EN Published')
773+
await saveDocAndAssert(page, '#publish-locale')
774+
await openDocControls(page)
775+
776+
await expect(page.locator('#action-unpublish')).toBeVisible()
777+
await expect(page.locator('#action-unpublish-locale')).toBeVisible()
778+
})
779+
780+
test('should not show unpublish in specific locale when localizeStatus is not enabled', async () => {
781+
await page.goto(urlPostsWithDrafts.create)
782+
await page.locator('#field-title').fill('EN Published')
783+
await saveDocAndAssert(page, '#publish-locale')
784+
await openDocControls(page)
785+
786+
await expect(page.locator('#action-unpublish')).toBeVisible()
787+
await expect(page.locator('#action-unpublish-locale')).toHaveCount(0)
788+
})
789+
})
790+
769791
test('should show locale in slate rich text field label', async () => {
770792
await page.goto(urlAllFieldsLocalized.create)
771793

test/versions/e2e.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
5151
import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js'
5252
import { navigateToDiffVersionView as _navigateToDiffVersionView } from '../helpers/e2e/navigateToDiffVersionView.js'
53+
import { openDocControls } from '../helpers/e2e/openDocControls.js'
5354
import { waitForAutoSaveToRunAndComplete } from '../helpers/e2e/waitForAutoSaveToRunAndComplete.js'
5455
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
5556
import { reInitializeDB } from '../helpers/reInitializeDB.js'
@@ -794,6 +795,7 @@ describe('Versions', () => {
794795
await page.goto(disablePublishURL.edit(String(publishedDoc.id)))
795796

796797
// Verify unpublish button is hidden when user doesn't have publish permission
798+
await openDocControls(page)
797799
await expect(page.locator('#action-unpublish')).not.toBeAttached()
798800
})
799801

@@ -806,6 +808,7 @@ describe('Versions', () => {
806808
},
807809
})
808810
await page.goto(errorOnUnpublishURL.edit(String(publishedDoc.id)))
811+
await openDocControls(page)
809812
await page.locator('#action-unpublish').click()
810813
await page.locator('[id^="confirm-un-publish-"] #confirm-action').click()
811814
await expect(
@@ -825,6 +828,7 @@ describe('Versions', () => {
825828
const customUnpublishURL = new AdminUrlUtil(serverURL, draftWithCustomUnpublishSlug)
826829
await page.goto(customUnpublishURL.edit(String(publishedDoc.id)))
827830

831+
await openDocControls(page)
828832
await expect(page.getByRole('button', { name: 'Custom Unpublish' })).toBeVisible()
829833

830834
await payload.delete({
@@ -1037,7 +1041,7 @@ describe('Versions', () => {
10371041
await saveDocAndAssert(page, '#action-save-draft')
10381042

10391043
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
1040-
1044+
await openDocControls(page)
10411045
await expect(page.locator('#action-unpublish')).toBeHidden()
10421046
})
10431047

@@ -1055,7 +1059,7 @@ describe('Versions', () => {
10551059
await expect(page.locator('.doc-controls__status .status__value')).toContainText(
10561060
'Published',
10571061
)
1058-
1062+
await openDocControls(page)
10591063
await expect(page.locator('#action-unpublish')).toBeVisible()
10601064
})
10611065

@@ -1072,6 +1076,7 @@ describe('Versions', () => {
10721076
await saveDocAndAssert(page, '#action-save-draft')
10731077

10741078
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
1079+
await openDocControls(page)
10751080
await expect(page.locator('#action-unpublish')).toBeHidden()
10761081
})
10771082
})

0 commit comments

Comments
 (0)