diff --git a/packages/payload/src/admin/components/elements/Publish/index.tsx b/packages/payload/src/admin/components/elements/Publish/index.tsx index ef8470f582c..b199e0adff7 100644 --- a/packages/payload/src/admin/components/elements/Publish/index.tsx +++ b/packages/payload/src/admin/components/elements/Publish/index.tsx @@ -1,9 +1,12 @@ +import qs from 'qs' import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useForm, useFormModified } from '../../forms/Form/context' import FormSubmit from '../../forms/Submit' +import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' +import { useLocale } from '../../utilities/Locale' import RenderCustomComponent from '../../utilities/RenderCustomComponent' export type CustomPublishButtonProps = React.ComponentType< @@ -12,6 +15,7 @@ export type CustomPublishButtonProps = React.ComponentType< } > export type DefaultPublishButtonProps = { + canPublish: boolean disabled: boolean id?: string label: string @@ -19,10 +23,13 @@ export type DefaultPublishButtonProps = { } const DefaultPublishButton: React.FC = ({ id, + canPublish, disabled, label, publish, }) => { + if (!canPublish) return null + return ( {label} @@ -35,22 +42,68 @@ type Props = { } export const Publish: React.FC = ({ CustomComponent }) => { - const { publishedDoc, unpublishedVersions } = useDocumentInfo() - const { submit } = useForm() + const { code } = useLocale() + const { id, collection, global, publishedDoc, unpublishedVersions } = useDocumentInfo() + const [hasPublishPermission, setHasPublishPermission] = React.useState(false) + const { getData, submit } = useForm() const modified = useFormModified() + const { + routes: { api }, + serverURL, + } = useConfig() const { t } = useTranslation('version') const hasNewerVersions = unpublishedVersions?.totalDocs > 0 const canPublish = modified || hasNewerVersions || !publishedDoc const publish = useCallback(() => { - submit({ + void submit({ overrides: { _status: 'published', }, }) }, [submit]) + React.useEffect(() => { + const fetchPublishAccess = async () => { + let docAccessURL: string + let operation = 'update' + + const params = { + locale: code || undefined, + } + if (global) { + docAccessURL = `/globals/${global.slug}/access` + } else if (collection) { + if (!id) operation = 'create' + docAccessURL = `/${collection.slug}/access${id ? `/${id}` : ''}` + } + + if (docAccessURL) { + const data = getData() + + const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, { + body: JSON.stringify({ + ...data, + _status: 'published', + }), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + method: 'post', + }) + const json = await res.json() + const result = Boolean(json?.[operation]?.permission) + setHasPublishPermission(result) + } else { + setHasPublishPermission(true) + } + } + + void fetchPublishAccess() + }, [api, code, collection, getData, global, id, serverURL]) + return ( = ({ CustomComponent }) => { componentProps={{ id: 'action-save', DefaultButton: DefaultPublishButton, + canPublish: hasPublishPermission, disabled: !canPublish, label: t('publishChanges'), publish, diff --git a/packages/payload/src/collections/buildEndpoints.ts b/packages/payload/src/collections/buildEndpoints.ts index bb49ef234ab..6178fb62ad7 100644 --- a/packages/payload/src/collections/buildEndpoints.ts +++ b/packages/payload/src/collections/buildEndpoints.ts @@ -129,6 +129,16 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => { method: 'get', path: '/access/:id', }, + { + handler: docAccessRequestHandler, + method: 'post', + path: '/access/:id', + }, + { + handler: docAccessRequestHandler, + method: 'post', + path: '/access', + }, { handler: deprecatedUpdate, method: 'put', diff --git a/packages/payload/src/globals/buildEndpoints.ts b/packages/payload/src/globals/buildEndpoints.ts index e085ddb8fca..fb850d962bd 100644 --- a/packages/payload/src/globals/buildEndpoints.ts +++ b/packages/payload/src/globals/buildEndpoints.ts @@ -38,6 +38,11 @@ const buildEndpoints = (global: SanitizedGlobalConfig): Endpoint[] => { method: 'get', path: '/access', }, + { + handler: async (req, res, next) => docAccessRequestHandler(req, res, next, global), + method: 'post', + path: '/access', + }, { handler: findOne(global), method: 'get', diff --git a/packages/payload/src/utilities/getEntityPolicies.ts b/packages/payload/src/utilities/getEntityPolicies.ts index 5726ca69dc1..7ba07f3cfe8 100644 --- a/packages/payload/src/utilities/getEntityPolicies.ts +++ b/packages/payload/src/utilities/getEntityPolicies.ts @@ -100,7 +100,10 @@ export async function getEntityPolicies(args: T): Promise { + return data._status !== 'published' + }, + update: ({ data }) => { + return data._status !== 'published' + }, + }, + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + required: true, + type: 'text', + }, + ], + slug: disablePublishSlug, + versions: { + drafts: true, + }, +} + +export default DisablePublish diff --git a/test/versions/collections/Drafts.ts b/test/versions/collections/Drafts.ts index be3787824b9..8f9669004ae 100644 --- a/test/versions/collections/Drafts.ts +++ b/test/versions/collections/Drafts.ts @@ -1,8 +1,11 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { extractTranslations } from '../../../packages/payload/src/translations/extractTranslations' import { CustomPublishButton } from '../elements/CustomSaveButton' import { draftCollectionSlug } from '../slugs' +const labels = extractTranslations(['version:draft', 'version:published', 'version:status']) + const DraftPosts: CollectionConfig = { access: { read: ({ req: { user } }) => { diff --git a/test/versions/config.ts b/test/versions/config.ts index e6f1fc7fc2e..fc56cc82ddc 100644 --- a/test/versions/config.ts +++ b/test/versions/config.ts @@ -2,21 +2,16 @@ import path from 'path' import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import AutosavePosts from './collections/Autosave' +import DisablePublish from './collections/DisablePublish' import DraftPosts from './collections/Drafts' import Posts from './collections/Posts' import VersionPosts from './collections/Versions' import AutosaveGlobal from './globals/Autosave' +import DisablePublishGlobal from './globals/DisablePublish' import DraftGlobal from './globals/Draft' import { clearAndSeedEverything } from './seed' export default buildConfigWithDefaults({ - collections: [Posts, AutosavePosts, DraftPosts, VersionPosts], - globals: [AutosaveGlobal, DraftGlobal], - indexSortableFields: true, - localization: { - defaultLocale: 'en', - locales: ['en', 'es'], - }, admin: { webpack: (config) => ({ ...config, @@ -29,6 +24,13 @@ export default buildConfigWithDefaults({ }, }), }, + collections: [DisablePublish, Posts, AutosavePosts, DraftPosts, VersionPosts], + globals: [AutosaveGlobal, DraftGlobal, DisablePublishGlobal], + indexSortableFields: true, + localization: { + defaultLocale: 'en', + locales: ['en', 'es'], + }, onInit: async (payload) => { await clearAndSeedEverything(payload) }, diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 41df5737aa0..3f102f68395 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -44,6 +44,8 @@ import { titleToDelete } from './shared' import { autoSaveGlobalSlug, autosaveCollectionSlug, + disablePublishGlobalSlug, + disablePublishSlug, draftCollectionSlug, draftGlobalSlug, } from './slugs' @@ -55,6 +57,7 @@ describe('versions', () => { let url: AdminUrlUtil let serverURL: string let autosaveURL: AdminUrlUtil + let disablePublishURL: AdminUrlUtil beforeAll(async ({ browser }) => { const config = await initPayloadE2E(__dirname) @@ -73,6 +76,7 @@ describe('versions', () => { beforeAll(() => { url = new AdminUrlUtil(serverURL, draftCollectionSlug) autosaveURL = new AdminUrlUtil(serverURL, autosaveCollectionSlug) + disablePublishURL = new AdminUrlUtil(serverURL, disablePublishSlug) }) // This test has to run before bulk updates that will rename the title @@ -345,5 +349,33 @@ describe('versions', () => { await expect(page.locator('#field-title')).toHaveValue('first post title') await expect(page.locator('#field-description')).toHaveValue('first post description') }) + + test('should hide publish when access control prevents updating on globals', async () => { + const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug) + await page.goto(url.global(disablePublishGlobalSlug)) + + await expect(page.locator('#action-save')).not.toBeAttached() + }) + + test('should hide publish when access control prevents create operation', async () => { + await page.goto(disablePublishURL.create) + + await expect(page.locator('#action-save')).not.toBeAttached() + }) + + test('should hide publish when access control prevents update operation', async () => { + const publishedDoc = await payload.create({ + collection: disablePublishSlug, + data: { + _status: 'published', + title: 'title', + }, + overrideAccess: true, + }) + + await page.goto(disablePublishURL.edit(String(publishedDoc.id))) + + await expect(page.locator('#action-save')).not.toBeAttached() + }) }) }) diff --git a/test/versions/globals/DisablePublish.ts b/test/versions/globals/DisablePublish.ts new file mode 100644 index 00000000000..aab9bb01ef9 --- /dev/null +++ b/test/versions/globals/DisablePublish.ts @@ -0,0 +1,23 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import { disablePublishGlobalSlug } from '../slugs' + +const DisablePublishGlobal: GlobalConfig = { + access: { + update: ({ data }) => { + return data._status !== 'published' + }, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], + slug: disablePublishGlobalSlug, + versions: { + drafts: true, + }, +} + +export default DisablePublishGlobal diff --git a/test/versions/slugs.ts b/test/versions/slugs.ts index 6eb1b50b57c..d7a9611ea5f 100644 --- a/test/versions/slugs.ts +++ b/test/versions/slugs.ts @@ -6,6 +6,10 @@ export const postCollectionSlug = 'posts' as const export const versionCollectionSlug = 'version-posts' as const +export const disablePublishSlug = 'disable-publish' as const + +export const disablePublishGlobalSlug = 'disable-publish-global' as const + export const collectionSlugs = [ autosaveCollectionSlug, draftCollectionSlug,