fix(next,ui): fixes global doc permissions and optimizes publish access data loading (#6451)

This commit is contained in:
Jacob Fletcher
2024-05-22 10:03:12 -04:00
committed by GitHub
parent db772a058c
commit 2b941b7e2c
19 changed files with 540 additions and 267 deletions

View File

@@ -6,15 +6,29 @@ import type { BaseRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js' import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const access: BaseRouteHandler = async ({ req }) => { export const access: BaseRouteHandler = async ({ req }) => {
const headers = headersWithCors({
headers: new Headers(),
req,
})
try {
const results = await accessOperation({ const results = await accessOperation({
req, req,
}) })
return Response.json(results, { return Response.json(results, {
headers: headersWithCors({ headers,
headers: new Headers(),
req,
}),
status: httpStatus.OK, status: httpStatus.OK,
}) })
} catch (e: unknown) {
return Response.json(
{
error: e,
},
{
headers,
status: httpStatus.INTERNAL_SERVER_ERROR,
},
)
}
} }

View File

@@ -9,15 +9,21 @@ import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParam
import { notFound } from 'next/navigation.js' import { notFound } from 'next/navigation.js'
import React from 'react' import React from 'react'
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
import { EditView } from '../Edit/index.js' import { EditView } from '../Edit/index.js'
import { Settings } from './Settings/index.js' import { Settings } from './Settings/index.js'
export { generateAccountMetadata } from './meta.js' export { generateAccountMetadata } from './meta.js'
export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, searchParams }) => { export const Account: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
const { const {
locale, locale,
permissions, permissions,
req,
req: { req: {
i18n, i18n,
payload, payload,
@@ -32,11 +38,17 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
serverURL, serverURL,
} = config } = config
const collectionPermissions = permissions?.collections?.[userSlug]
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug) const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
if (collectionConfig) { if (collectionConfig) {
const { docPermissions, hasPublishPermission, hasSavePermission } =
await getDocumentPermissions({
id: user.id,
collectionConfig,
data: user,
req,
})
const viewComponentProps: ServerSideEditViewProps = { const viewComponentProps: ServerSideEditViewProps = {
initPageResult, initPageResult,
params, params,
@@ -50,9 +62,10 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`} action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`} apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
collectionSlug={userSlug} collectionSlug={userSlug}
docPermissions={collectionPermissions} docPermissions={docPermissions}
hasSavePermission={collectionPermissions?.update?.permission} hasPublishPermission={hasPublishPermission}
id={user?.id} hasSavePermission={hasSavePermission}
id={user?.id.toString()}
isEditing isEditing
> >
<DocumentHeader <DocumentHeader

View File

@@ -0,0 +1,41 @@
import type {
Data,
Payload,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload/types'
export const getDocumentData = async (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
id?: number | string
locale: Locale
payload: Payload
req: PayloadRequest
}): Promise<Data> => {
const { id, collectionConfig, globalConfig, locale, payload, req } = args
let data: Data
if (collectionConfig && id !== undefined && id !== null) {
data = await payload.findByID({
id,
collection: collectionConfig.slug,
depth: 0,
locale: locale.code,
req,
})
}
if (globalConfig) {
data = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale: locale.code,
req,
})
}
return data
}

View File

@@ -0,0 +1,105 @@
import type { DocumentPermissions } from 'payload/auth'
import type {
Data,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload/types'
import { hasSavePermission as getHasSavePermission } from '@payloadcms/ui/utilities/hasSavePermission'
import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing'
import { docAccessOperation, docAccessOperationGlobal } from 'payload/operations'
export const getDocumentPermissions = async (args: {
collectionConfig?: SanitizedCollectionConfig
data: Data
globalConfig?: SanitizedGlobalConfig
id?: number | string
req: PayloadRequest
}): Promise<{
docPermissions: DocumentPermissions
hasPublishPermission: boolean
hasSavePermission: boolean
}> => {
const { id, collectionConfig, data = {}, globalConfig, req } = args
let docPermissions: DocumentPermissions
let hasPublishPermission = false
if (collectionConfig) {
try {
docPermissions = await docAccessOperation({
id: id?.toString(),
collection: {
config: collectionConfig,
},
req: {
...req,
data,
},
})
if (collectionConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperation({
id: id?.toString(),
collection: {
config: collectionConfig,
},
req: {
...req,
data: {
...data,
_status: 'published',
},
},
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
if (globalConfig) {
try {
docPermissions = await docAccessOperationGlobal({
globalConfig,
req: {
...req,
data,
},
})
if (globalConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperationGlobal({
globalConfig,
req: {
...req,
data: {
...data,
_status: 'published',
},
},
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
}
const hasSavePermission = getHasSavePermission({
collectionSlug: collectionConfig?.slug,
docPermissions,
globalSlug: globalConfig?.slug,
isEditing: getIsEditing({
id,
collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug,
}),
})
return {
docPermissions,
hasPublishPermission,
hasSavePermission,
}
}

View File

@@ -1,6 +1,5 @@
import type { EditViewComponent } from 'payload/config' import type { EditViewComponent } from 'payload/config'
import type { AdminViewComponent, ServerSideEditViewProps } from 'payload/types' import type { AdminViewComponent, ServerSideEditViewProps } from 'payload/types'
import type { DocumentPermissions } from 'payload/types'
import type { AdminViewProps } from 'payload/types' import type { AdminViewProps } from 'payload/types'
import { DocumentHeader } from '@payloadcms/ui/elements/DocumentHeader' import { DocumentHeader } from '@payloadcms/ui/elements/DocumentHeader'
@@ -9,15 +8,15 @@ import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomCompo
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo' import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth' import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams' import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { hasSavePermission as getHasSavePermission } from '@payloadcms/ui/utilities/hasSavePermission'
import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing' import { isEditing as getIsEditing } from '@payloadcms/ui/utilities/isEditing'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import { docAccessOperation } from 'payload/operations'
import React from 'react' import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js' import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import { NotFoundView } from '../NotFound/index.js' import { NotFoundView } from '../NotFound/index.js'
import { getDocumentData } from './getDocumentData.js'
import { getDocumentPermissions } from './getDocumentPermissions.js'
import { getMetaBySegment } from './getMetaBySegment.js' import { getMetaBySegment } from './getMetaBySegment.js'
import { getViewsFromConfig } from './getViewsFromConfig.js' import { getViewsFromConfig } from './getViewsFromConfig.js'
@@ -61,32 +60,33 @@ export const Document: React.FC<AdminViewProps> = async ({
let DefaultView: EditViewComponent let DefaultView: EditViewComponent
let ErrorView: AdminViewComponent let ErrorView: AdminViewComponent
let docPermissions: DocumentPermissions
let hasSavePermission: boolean
let apiURL: string let apiURL: string
let action: string let action: string
const data = await getDocumentData({
id,
collectionConfig,
globalConfig,
locale,
payload,
req,
})
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
id,
collectionConfig,
data,
globalConfig,
req,
})
if (collectionConfig) { if (collectionConfig) {
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) { if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
notFound() notFound()
} }
try {
docPermissions = await docAccessOperation({
id,
collection: {
config: collectionConfig,
},
req,
})
} catch (error) {
notFound()
}
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}` action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
hasSavePermission = getHasSavePermission({ collectionSlug, docPermissions, isEditing })
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale.code}${ apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale.code}${
collectionConfig.versions?.drafts ? '&draft=true' : '' collectionConfig.versions?.drafts ? '&draft=true' : ''
}` }`
@@ -117,9 +117,6 @@ export const Document: React.FC<AdminViewProps> = async ({
notFound() notFound()
} }
docPermissions = permissions?.globals?.[globalSlug]
hasSavePermission = getHasSavePermission({ docPermissions, globalSlug, isEditing })
action = `${serverURL}${apiRoute}/globals/${globalSlug}` action = `${serverURL}${apiRoute}/globals/${globalSlug}`
apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${ apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${
@@ -191,6 +188,7 @@ export const Document: React.FC<AdminViewProps> = async ({
disableActions={false} disableActions={false}
docPermissions={docPermissions} docPermissions={docPermissions}
globalSlug={globalConfig?.slug} globalSlug={globalConfig?.slug}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission} hasSavePermission={hasSavePermission}
id={id} id={id}
isEditing={isEditing} isEditing={isEditing}

View File

@@ -44,10 +44,10 @@ export const DefaultEditView: React.FC = () => {
disableActions, disableActions,
disableLeaveWithoutSaving, disableLeaveWithoutSaving,
docPermissions, docPermissions,
getDocPermissions,
getDocPreferences, getDocPreferences,
getVersions, getVersions,
globalSlug, globalSlug,
hasPublishPermission,
hasSavePermission, hasSavePermission,
initialData: data, initialData: data,
initialState, initialState,
@@ -115,7 +115,6 @@ export const DefaultEditView: React.FC = () => {
} }
void getVersions() void getVersions()
void getDocPermissions()
if (typeof onSaveFromContext === 'function') { if (typeof onSaveFromContext === 'function') {
void onSaveFromContext({ void onSaveFromContext({
@@ -147,7 +146,6 @@ export const DefaultEditView: React.FC = () => {
depth, depth,
collectionSlug, collectionSlug,
getVersions, getVersions,
getDocPermissions,
isEditing, isEditing,
refreshCookieAsync, refreshCookieAsync,
adminRoute, adminRoute,
@@ -221,6 +219,7 @@ export const DefaultEditView: React.FC = () => {
apiURL={apiURL} apiURL={apiURL}
data={data} data={data}
disableActions={disableActions} disableActions={disableActions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission} hasSavePermission={hasSavePermission}
id={id} id={id}
isEditing={isEditing} isEditing={isEditing}

View File

@@ -61,6 +61,7 @@ const PreviewView: React.FC<Props> = ({
docPermissions, docPermissions,
getDocPreferences, getDocPreferences,
globalSlug, globalSlug,
hasPublishPermission,
hasSavePermission, hasSavePermission,
initialData, initialData,
initialState, initialState,
@@ -160,6 +161,7 @@ const PreviewView: React.FC<Props> = ({
apiURL={apiURL} apiURL={apiURL}
data={initialData} data={initialData}
disableActions={disableActions} disableActions={disableActions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission} hasSavePermission={hasSavePermission}
id={id} id={id}
isEditing={isEditing} isEditing={isEditing}

View File

@@ -27,6 +27,7 @@ export const DocumentControls: React.FC<{
apiURL: string apiURL: string
data?: any data?: any
disableActions?: boolean disableActions?: boolean
hasPublishPermission?: boolean
hasSavePermission?: boolean hasSavePermission?: boolean
id?: number | string id?: number | string
isAccountView?: boolean isAccountView?: boolean

View File

@@ -111,12 +111,6 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
collectionSlug={collectionConfig.slug} collectionSlug={collectionConfig.slug}
disableActions disableActions
disableLeaveWithoutSaving disableLeaveWithoutSaving
// Do NOT pass in the docPermissions we have here. This is because the permissions we have here do not have their where: { } returns resolved.
// If we set it to null though, the DocumentInfoProvider will fully-fetch the permissions from the server, and the where: { } returns will be resolved.
docPermissions={null}
// Same reason as above. We need to fully-fetch the docPreferences from the server. This is done in DocumentInfoProvider if we set it to null here.
hasSavePermission={null}
// isLoading,
id={docID} id={docID}
isEditing={isEditing} isEditing={isEditing}
onLoadError={onLoadError} onLoadError={onLoadError}

View File

@@ -1,26 +1,18 @@
'use client' 'use client'
import qs from 'qs'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useForm, useFormModified } from '../../forms/Form/context.js' import { useForm, useFormModified } from '../../forms/Form/context.js'
import { FormSubmit } from '../../forms/Submit/index.js' import { FormSubmit } from '../../forms/Submit/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
export const DefaultPublishButton: React.FC<{ label?: string }> = ({ label: labelProp }) => { export const DefaultPublishButton: React.FC<{ label?: string }> = ({ label: labelProp }) => {
const { code } = useLocale() const { hasPublishPermission, publishedDoc, unpublishedVersions } = useDocumentInfo()
const { id, collectionSlug, globalSlug, publishedDoc, unpublishedVersions } = useDocumentInfo()
const [hasPublishPermission, setHasPublishPermission] = React.useState(false) const { submit } = useForm()
const { getData, submit } = useForm()
const modified = useFormModified() const modified = useFormModified()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation() const { t } = useTranslation()
const label = labelProp || t('version:publishChanges') const label = labelProp || t('version:publishChanges')
@@ -35,46 +27,6 @@ export const DefaultPublishButton: React.FC<{ label?: string }> = ({ label: labe
}) })
}, [submit]) }, [submit])
React.useEffect(() => {
const fetchPublishAccess = async () => {
let docAccessURL: string
let operation = 'update'
const params = {
locale: code || undefined,
}
if (globalSlug) {
docAccessURL = `/globals/${globalSlug}/access`
} else if (collectionSlug) {
if (!id) operation = 'create'
docAccessURL = `/${collectionSlug}/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, collectionSlug, getData, globalSlug, id, serverURL])
if (!hasPublishPermission) return null if (!hasPublishPermission) return null
return ( return (
@@ -96,6 +48,5 @@ type Props = {
export const PublishButton: React.FC<Props> = ({ CustomComponent }) => { export const PublishButton: React.FC<Props> = ({ CustomComponent }) => {
if (CustomComponent) return CustomComponent if (CustomComponent) return CustomComponent
return <DefaultPublishButton /> return <DefaultPublishButton />
} }

View File

@@ -488,6 +488,10 @@ export const Form: React.FC<FormProps> = (props) => {
[getFieldStateBySchemaPath, dispatchFields], [getFieldStateBySchemaPath, dispatchFields],
) )
useEffect(() => {
if (typeof disabledFromProps === 'boolean') setDisabled(disabledFromProps)
}, [disabledFromProps])
contextRef.current.submit = submit contextRef.current.submit = submit
contextRef.current.getFields = getFields contextRef.current.getFields = getFields
contextRef.current.getField = getField contextRef.current.getField = getField

View File

@@ -5,7 +5,7 @@ import type { DocumentPermissions, DocumentPreferences, TypeWithID, Where } from
import { notFound } from 'next/navigation.js' import { notFound } from 'next/navigation.js'
import qs from 'qs' import qs from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import type { DocumentInfoContext, DocumentInfoProps } from './types.js' import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
@@ -32,16 +32,28 @@ export const DocumentInfoProvider: React.FC<
children: React.ReactNode children: React.ReactNode
} }
> = ({ children, ...props }) => { > = ({ children, ...props }) => {
const { id, collectionSlug, globalSlug, onLoadError, onSave: onSaveFromProps } = props const {
id,
collectionSlug,
docPermissions: docPermissionsFromProps,
globalSlug,
hasPublishPermission: hasPublishPermissionFromProps,
hasSavePermission: hasSavePermissionFromProps,
onLoadError,
onSave: onSaveFromProps,
} = props
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
const [documentTitle, setDocumentTitle] = useState('') const [documentTitle, setDocumentTitle] = useState('')
const [initialData, setInitialData] = useState<Data>() const [data, setData] = useState<Data>()
const [initialState, setInitialState] = useState<FormState>() const [initialState, setInitialState] = useState<FormState>()
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null) const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
const [versions, setVersions] = useState<PaginatedDocs<TypeWithVersion<any>>>(null) const [versions, setVersions] = useState<PaginatedDocs<TypeWithVersion<any>>>(null)
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null) const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null)
const [hasSavePermission, setHasSavePermission] = useState<boolean>(null) const [hasSavePermission, setHasSavePermission] = useState<boolean>(null)
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(null)
const hasInitializedDocPermissions = useRef(false)
const [unpublishedVersions, setUnpublishedVersions] = const [unpublishedVersions, setUnpublishedVersions] =
useState<PaginatedDocs<TypeWithVersion<any>>>(null) useState<PaginatedDocs<TypeWithVersion<any>>>(null)
@@ -211,14 +223,17 @@ export const DocumentInfoProvider: React.FC<
} }
}, [i18n, globalSlug, collectionSlug, id, baseURL, locale, versionsConfig, shouldFetchVersions]) }, [i18n, globalSlug, collectionSlug, id, baseURL, locale, versionsConfig, shouldFetchVersions])
const getDocPermissions = React.useCallback(async () => { const getDocPermissions = React.useCallback(
async (data: Data) => {
const params = { const params = {
locale: locale || undefined, locale: locale || undefined,
} }
if (isEditing) { const newIsEditing = getIsEditing({ id: data?.id, collectionSlug, globalSlug })
if (newIsEditing) {
const docAccessURL = collectionSlug const docAccessURL = collectionSlug
? `/${collectionSlug}/access/${id}` ? `/${collectionSlug}/access/${data.id}`
: globalSlug : globalSlug
? `/globals/${globalSlug}/access` ? `/globals/${globalSlug}/access`
: null : null
@@ -232,12 +247,35 @@ export const DocumentInfoProvider: React.FC<
}) })
const json: DocumentPermissions = await res.json() const json: DocumentPermissions = await res.json()
const publishedAccessJSON = await fetch(
`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`,
{
body: JSON.stringify({
data: {
...(data || {}),
_status: 'published',
},
}),
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
method: 'POST',
},
).then((res) => res.json())
setDocPermissions(json) setDocPermissions(json)
setHasSavePermission( setHasSavePermission(
getHasSavePermission({ collectionSlug, docPermissions: json, globalSlug, isEditing }), getHasSavePermission({
collectionSlug,
docPermissions: json,
globalSlug,
isEditing: newIsEditing,
}),
) )
setHasPublishPermission(publishedAccessJSON?.update?.permission)
} }
} else { } else {
// when creating new documents, there is no permissions saved for this document yet // when creating new documents, there is no permissions saved for this document yet
@@ -253,21 +291,13 @@ export const DocumentInfoProvider: React.FC<
collectionSlug, collectionSlug,
docPermissions: newDocPermissions, docPermissions: newDocPermissions,
globalSlug, globalSlug,
isEditing, isEditing: newIsEditing,
}), }),
) )
} }
}, [ },
serverURL, [serverURL, api, permissions, i18n.language, locale, collectionSlug, globalSlug, isEditing],
api, )
id,
permissions,
i18n.language,
locale,
collectionSlug,
globalSlug,
isEditing,
])
const getDocPreferences = useCallback(() => { const getDocPreferences = useCallback(() => {
return getPreference<DocumentPreferences>(preferencesKey) return getPreference<DocumentPreferences>(preferencesKey)
@@ -320,8 +350,11 @@ export const DocumentInfoProvider: React.FC<
serverURL, serverURL,
}) })
const newData = json.doc
setInitialState(newState) setInitialState(newState)
setInitialData(json.doc) setData(newData)
await getDocPermissions(newData)
}, },
[ [
api, api,
@@ -333,6 +366,7 @@ export const DocumentInfoProvider: React.FC<
locale, locale,
onSaveFromProps, onSaveFromProps,
serverURL, serverURL,
getDocPermissions,
], ],
) )
@@ -359,7 +393,7 @@ export const DocumentInfoProvider: React.FC<
signal: abortController.signal, signal: abortController.signal,
}) })
setInitialData(reduceFieldsToValues(result, true)) setData(reduceFieldsToValues(result, true))
setInitialState(result) setInitialState(result)
} catch (err) { } catch (err) {
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
@@ -399,38 +433,49 @@ export const DocumentInfoProvider: React.FC<
setDocumentTitle( setDocumentTitle(
formatDocTitle({ formatDocTitle({
collectionConfig, collectionConfig,
data: { ...initialData, id }, data: { ...data, id },
dateFormat, dateFormat,
fallback: id?.toString(), fallback: id?.toString(),
globalConfig, globalConfig,
i18n, i18n,
}), }),
) )
}, [collectionConfig, initialData, dateFormat, i18n, id, globalConfig]) }, [collectionConfig, data, dateFormat, i18n, id, globalConfig])
useEffect(() => { useEffect(() => {
const loadDocPermissions = async () => { const loadDocPermissions = async () => {
const docPermissions: DocumentPermissions = props.docPermissions const docPermissions: DocumentPermissions = docPermissionsFromProps
const hasSavePermission: boolean = props.hasSavePermission const hasSavePermission: boolean = hasSavePermissionFromProps
const hasPublishPermission: boolean = hasPublishPermissionFromProps
if (!docPermissions || hasSavePermission === undefined || hasSavePermission === null) { if (
await getDocPermissions() !docPermissions ||
hasSavePermission === undefined ||
hasSavePermission === null ||
hasPublishPermission === undefined ||
hasPublishPermission === null
) {
await getDocPermissions(data)
} else { } else {
setDocPermissions(docPermissions) setDocPermissions(docPermissions)
setHasSavePermission(hasSavePermission) setHasSavePermission(hasSavePermission)
setHasPublishPermission(hasPublishPermission)
} }
} }
if (collectionSlug || globalSlug) { if (!hasInitializedDocPermissions.current && data && (collectionSlug || globalSlug)) {
hasInitializedDocPermissions.current = true
void loadDocPermissions() void loadDocPermissions()
} }
}, [ }, [
getDocPermissions, getDocPermissions,
props.docPermissions, docPermissionsFromProps,
props.hasSavePermission, hasSavePermissionFromProps,
hasPublishPermissionFromProps,
setDocPermissions, setDocPermissions,
collectionSlug, collectionSlug,
globalSlug, globalSlug,
data,
]) ])
if (isError) notFound() if (isError) notFound()
@@ -446,8 +491,9 @@ export const DocumentInfoProvider: React.FC<
getDocPermissions, getDocPermissions,
getDocPreferences, getDocPreferences,
getVersions, getVersions,
hasPublishPermission,
hasSavePermission, hasSavePermission,
initialData, initialData: data,
initialState, initialState,
onSave, onSave,
publishedDoc, publishedDoc,

View File

@@ -26,6 +26,7 @@ export type DocumentInfoProps = {
disableLeaveWithoutSaving?: boolean disableLeaveWithoutSaving?: boolean
docPermissions?: DocumentPermissions docPermissions?: DocumentPermissions
globalSlug?: SanitizedGlobalConfig['slug'] globalSlug?: SanitizedGlobalConfig['slug']
hasPublishPermission?: boolean
hasSavePermission?: boolean hasSavePermission?: boolean
id: null | number | string id: null | number | string
isEditing?: boolean isEditing?: boolean
@@ -35,7 +36,7 @@ export type DocumentInfoProps = {
export type DocumentInfoContext = DocumentInfoProps & { export type DocumentInfoContext = DocumentInfoProps & {
docConfig?: ClientCollectionConfig | ClientGlobalConfig docConfig?: ClientCollectionConfig | ClientGlobalConfig
getDocPermissions: () => Promise<void> getDocPermissions: (data?: Data) => Promise<void>
getDocPreferences: () => Promise<DocumentPreferences> getDocPreferences: () => Promise<DocumentPreferences>
getVersions: () => Promise<void> getVersions: () => Promise<void>
initialData: Data initialData: Data

View File

@@ -4,7 +4,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js' import { devUser } from '../credentials.js'
import { TestButton } from './TestButton.js' import { TestButton } from './TestButton.js'
import { import {
createNotUpdateSlug, createNotUpdateCollectionSlug,
docLevelAccessSlug, docLevelAccessSlug,
firstArrayText, firstArrayText,
fullyRestrictedSlug, fullyRestrictedSlug,
@@ -14,6 +14,7 @@ import {
noAdminAccessEmail, noAdminAccessEmail,
nonAdminUserEmail, nonAdminUserEmail,
nonAdminUserSlug, nonAdminUserSlug,
readNotUpdateGlobalSlug,
readOnlyGlobalSlug, readOnlyGlobalSlug,
readOnlySlug, readOnlySlug,
relyOnRequestHeadersSlug, relyOnRequestHeadersSlug,
@@ -22,7 +23,8 @@ import {
siblingDataSlug, siblingDataSlug,
slug, slug,
unrestrictedSlug, unrestrictedSlug,
userRestrictedSlug, userRestrictedCollectionSlug,
userRestrictedGlobalSlug,
} from './shared.js' } from './shared.js'
const openAccess = { const openAccess = {
@@ -90,6 +92,32 @@ export default buildConfigWithDefaults({
update: () => false, update: () => false,
}, },
}, },
{
slug: userRestrictedGlobalSlug,
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
read: () => true,
update: ({ req, data }) => data?.name === req.user?.email,
},
},
{
slug: readNotUpdateGlobalSlug,
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
read: () => true,
update: () => false,
},
},
], ],
collections: [ collections: [
{ {
@@ -201,13 +229,13 @@ export default buildConfigWithDefaults({
{ {
name: 'userRestrictedDocs', name: 'userRestrictedDocs',
type: 'relationship', type: 'relationship',
relationTo: userRestrictedSlug, relationTo: userRestrictedCollectionSlug,
hasMany: true, hasMany: true,
}, },
{ {
name: 'createNotUpdateDocs', name: 'createNotUpdateDocs',
type: 'relationship', type: 'relationship',
relationTo: 'create-not-update', relationTo: createNotUpdateCollectionSlug,
hasMany: true, hasMany: true,
}, },
], ],
@@ -243,7 +271,7 @@ export default buildConfigWithDefaults({
}, },
}, },
{ {
slug: userRestrictedSlug, slug: userRestrictedCollectionSlug,
admin: { admin: {
useAsTitle: 'name', useAsTitle: 'name',
}, },
@@ -265,7 +293,7 @@ export default buildConfigWithDefaults({
}, },
}, },
{ {
slug: createNotUpdateSlug, slug: createNotUpdateCollectionSlug,
admin: { admin: {
useAsTitle: 'name', useAsTitle: 'name',
}, },
@@ -563,5 +591,12 @@ export default buildConfigWithDefaults({
], ],
}, },
}) })
await payload.updateGlobal({
slug: userRestrictedGlobalSlug,
data: {
name: 'dev@payloadcms.com',
},
})
}, },
}) })

View File

@@ -4,7 +4,6 @@ import type { TypeWithID } from 'payload/types'
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js' import { devUser } from 'credentials.js'
import path from 'path' import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js' import type { PayloadTestSDK } from '../helpers/sdk/index.js'
@@ -30,17 +29,20 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { import {
createNotUpdateSlug, createNotUpdateCollectionSlug,
docLevelAccessSlug, docLevelAccessSlug,
fullyRestrictedSlug, fullyRestrictedSlug,
noAdminAccessEmail, noAdminAccessEmail,
nonAdminUserEmail, nonAdminUserEmail,
nonAdminUserSlug, nonAdminUserSlug,
readNotUpdateGlobalSlug,
readOnlyGlobalSlug, readOnlyGlobalSlug,
readOnlySlug, readOnlySlug,
restrictedVersionsSlug, restrictedVersionsSlug,
slug, slug,
unrestrictedSlug, unrestrictedSlug,
userRestrictedCollectionSlug,
userRestrictedGlobalSlug,
} from './shared.js' } from './shared.js'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -63,6 +65,8 @@ describe('access control', () => {
let readOnlyCollectionUrl: AdminUrlUtil let readOnlyCollectionUrl: AdminUrlUtil
let readOnlyGlobalUrl: AdminUrlUtil let readOnlyGlobalUrl: AdminUrlUtil
let restrictedVersionsUrl: AdminUrlUtil let restrictedVersionsUrl: AdminUrlUtil
let userRestrictedCollectionURL: AdminUrlUtil
let userRestrictedGlobalURL: AdminUrlUtil
let serverURL: string let serverURL: string
let context: BrowserContext let context: BrowserContext
let logoutURL: string let logoutURL: string
@@ -77,6 +81,8 @@ describe('access control', () => {
readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug) readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug)
readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug) readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug)
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug) restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
context = await browser.newContext() context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
@@ -134,7 +140,7 @@ describe('access control', () => {
}) })
}) })
describe('collection - fully restricted', () => { describe('collection fully restricted', () => {
let existingDoc: ReadOnlyCollection let existingDoc: ReadOnlyCollection
beforeAll(async () => { beforeAll(async () => {
@@ -178,7 +184,7 @@ describe('access control', () => {
}) })
}) })
describe('collection - read-only', () => { describe('collection read-only', () => {
let existingDoc: ReadOnlyCollection let existingDoc: ReadOnlyCollection
beforeAll(async () => { beforeAll(async () => {
@@ -215,7 +221,7 @@ describe('access control', () => {
await expect(page.locator(`#card-${readOnlySlug}`)).not.toHaveClass('card__actions') await expect(page.locator(`#card-${readOnlySlug}`)).not.toHaveClass('card__actions')
}) })
test('edit view should not have actions buttons', async () => { test('should not display actions on edit view', async () => {
await page.goto(readOnlyCollectionUrl.edit(existingDoc.id)) await page.goto(readOnlyCollectionUrl.edit(existingDoc.id))
await expect(page.locator('.collection-edit__collection-actions li')).toHaveCount(0) await expect(page.locator('.collection-edit__collection-actions li')).toHaveCount(0)
}) })
@@ -234,9 +240,9 @@ describe('access control', () => {
}) })
}) })
describe('collection - create but not edit', () => { describe('collection create but not edit', () => {
test('should not show edit button', async () => { test('should not show edit button', async () => {
const createNotUpdateURL = new AdminUrlUtil(serverURL, createNotUpdateSlug) const createNotUpdateURL = new AdminUrlUtil(serverURL, createNotUpdateCollectionSlug)
await page.goto(createNotUpdateURL.create) await page.goto(createNotUpdateURL.create)
await page.waitForURL(createNotUpdateURL.create) await page.waitForURL(createNotUpdateURL.create)
await expect(page.locator('#field-name')).toBeVisible() await expect(page.locator('#field-name')).toBeVisible()
@@ -265,7 +271,7 @@ describe('access control', () => {
await expect(addDocButton).toBeVisible() await expect(addDocButton).toBeVisible()
await addDocButton.click() await addDocButton.click()
const documentDrawer = page.locator('[id^=doc-drawer_create-not-update_1_]') const documentDrawer = page.locator(`[id^=doc-drawer_${createNotUpdateCollectionSlug}_1_]`)
await expect(documentDrawer).toBeVisible() await expect(documentDrawer).toBeVisible()
await expect(documentDrawer.locator('#action-save')).toBeVisible() await expect(documentDrawer.locator('#action-save')).toBeVisible()
await documentDrawer.locator('#field-name').fill('name') await documentDrawer.locator('#field-name').fill('name')
@@ -277,7 +283,37 @@ describe('access control', () => {
}) })
}) })
describe('collection - dynamic update access', () => { describe('global — read but not update', () => {
test('should not show edit button', async () => {
const createNotUpdateURL = new AdminUrlUtil(serverURL, readNotUpdateGlobalSlug)
await page.goto(createNotUpdateURL.global(readNotUpdateGlobalSlug))
await page.waitForURL(createNotUpdateURL.global(readNotUpdateGlobalSlug))
await expect(page.locator('#field-name')).toBeVisible()
await expect(page.locator('#field-name')).toBeDisabled()
await expect(page.locator('#action-save')).toBeHidden()
})
})
describe('dynamic update access', () => {
describe('collection', () => {
test('should restrict update access based on document field', async () => {
await page.goto(userRestrictedCollectionURL.create)
await expect(page.locator('#field-name')).toBeVisible()
await page.locator('#field-name').fill('anonymous@email.com')
await page.locator('#action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect(page.locator('#field-name')).toBeDisabled()
await expect(page.locator('#action-save')).toBeHidden()
await page.goto(userRestrictedCollectionURL.create)
await expect(page.locator('#field-name')).toBeVisible()
await page.locator('#field-name').fill(devUser.email)
await page.locator('#action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect(page.locator('#field-name')).toBeEnabled()
await expect(page.locator('#action-save')).toBeVisible()
})
test('maintain access control in document drawer', async () => { test('maintain access control in document drawer', async () => {
const unrestrictedDoc = await payload.create({ const unrestrictedDoc = await payload.create({
collection: unrestrictedSlug, collection: unrestrictedSlug,
@@ -293,7 +329,7 @@ describe('access control', () => {
) )
await addDocButton.click() await addDocButton.click()
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted_1_]') const documentDrawer = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
await expect(documentDrawer).toBeVisible() await expect(documentDrawer).toBeVisible()
await documentDrawer.locator('#field-name').fill('anonymous@email.com') await documentDrawer.locator('#field-name').fill('anonymous@email.com')
await documentDrawer.locator('#action-save').click() await documentDrawer.locator('#action-save').click()
@@ -302,7 +338,7 @@ describe('access control', () => {
await documentDrawer.locator('button.doc-drawer__header-close').click() await documentDrawer.locator('button.doc-drawer__header-close').click()
await expect(documentDrawer).toBeHidden() await expect(documentDrawer).toBeHidden()
await addDocButton.click() await addDocButton.click()
const documentDrawer2 = page.locator('[id^=doc-drawer_user-restricted_1_]') const documentDrawer2 = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
await expect(documentDrawer2).toBeVisible() await expect(documentDrawer2).toBeVisible()
await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com') await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com')
await documentDrawer2.locator('#action-save').click() await documentDrawer2.locator('#action-save').click()
@@ -311,7 +347,52 @@ describe('access control', () => {
}) })
}) })
describe('collection - restricted versions', () => { describe('global', () => {
test('should restrict update access based on document field', async () => {
await page.goto(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
await page.waitForURL(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
await expect(page.locator('#field-name')).toBeVisible()
await expect(page.locator('#field-name')).toHaveValue(devUser.email)
await expect(page.locator('#field-name')).toBeEnabled()
await page.locator('#field-name').fill('anonymous@email.com')
await page.locator('#action-save').click()
await expect(page.locator('.Toastify')).toContainText(
'You are not allowed to perform this action',
)
await payload.updateGlobal({
slug: userRestrictedGlobalSlug,
data: {
name: 'anonymous@payloadcms.com',
},
})
await page.goto(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
await page.waitForURL(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
await expect(page.locator('#field-name')).toBeDisabled()
await expect(page.locator('#action-save')).toBeHidden()
})
test('should restrict access based on user settings', async () => {
const url = `${serverURL}/admin/globals/settings`
await page.goto(url)
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain(url)
await openNav(page)
await expect(page.locator('#nav-global-settings')).toBeVisible()
await expect(page.locator('#nav-global-test')).toBeHidden()
await closeNav(page)
await page.locator('.checkbox-input:has(#field-test) input').check()
await saveDocAndAssert(page)
await openNav(page)
const globalTest = page.locator('#nav-global-test')
await expect(async () => await globalTest.isVisible()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
})
})
describe('collection — restricted versions', () => {
let existingDoc: RestrictedVersion let existingDoc: RestrictedVersion
beforeAll(async () => { beforeAll(async () => {
@@ -346,7 +427,7 @@ describe('access control', () => {
}) })
}) })
test('disable field based on document data', async () => { test('should disable field based on document data', async () => {
await page.goto(docLevelAccessURL.edit(existingDoc.id)) await page.goto(docLevelAccessURL.edit(existingDoc.id))
// validate that the text input is disabled because the field is "locked" // validate that the text input is disabled because the field is "locked"
@@ -354,7 +435,7 @@ describe('access control', () => {
await expect(isDisabled).toBeDisabled() await expect(isDisabled).toBeDisabled()
}) })
test('disable operation based on document data', async () => { test('should disable operation based on document data', async () => {
await page.goto(docLevelAccessURL.edit(existingDoc.id)) await page.goto(docLevelAccessURL.edit(existingDoc.id))
// validate that the delete action is not displayed // validate that the delete action is not displayed
@@ -371,37 +452,6 @@ describe('access control', () => {
}) })
}) })
// TODO: Test flakes. In CI, test global does not appear in nav. Perhaps the checkbox setValue is not triggered BEFORE the document is saved, as the custom save button can be clicked even if the form has not been set to modified.
test('should show test global immediately after allowing access', async () => {
const url = `${serverURL}/admin/globals/settings`
await page.goto(url)
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain(url)
await openNav(page)
// Ensure that we have loaded accesses by checking that settings collection
// at least is visible in the menu.
await expect(page.locator('#nav-global-settings')).toBeVisible()
// Test collection should be hidden at first.
await expect(page.locator('#nav-global-test')).toBeHidden()
await closeNav(page)
// Allow access to test global.
await page.locator('.checkbox-input:has(#field-test) input').check()
await saveDocAndAssert(page)
await openNav(page)
const globalTest = page.locator('#nav-global-test')
await expect(async () => await globalTest.isVisible()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
describe('admin access', () => { describe('admin access', () => {
test('should block admin access to admin user', async () => { test('should block admin access to admin user', async () => {
const adminURL = `${serverURL}/admin` const adminURL = `${serverURL}/admin`

View File

@@ -14,8 +14,8 @@ export interface Config {
unrestricted: Unrestricted; unrestricted: Unrestricted;
'fully-restricted': FullyRestricted; 'fully-restricted': FullyRestricted;
'read-only-collection': ReadOnlyCollection; 'read-only-collection': ReadOnlyCollection;
'user-restricted': UserRestricted; 'user-restricted-collection': UserRestrictedCollection;
'create-not-update': CreateNotUpdate; 'create-not-update-collection': CreateNotUpdateCollection;
'restricted-versions': RestrictedVersion; 'restricted-versions': RestrictedVersion;
'sibling-data': SiblingDatum; 'sibling-data': SiblingDatum;
'rely-on-request-headers': RelyOnRequestHeader; 'rely-on-request-headers': RelyOnRequestHeader;
@@ -30,6 +30,8 @@ export interface Config {
settings: Setting; settings: Setting;
test: Test; test: Test;
'read-only-global': ReadOnlyGlobal; 'read-only-global': ReadOnlyGlobal;
'user-restricted-global': UserRestrictedGlobal;
'read-not-update-global': ReadNotUpdateGlobal;
}; };
locale: null; locale: null;
user: user:
@@ -97,16 +99,16 @@ export interface Post {
export interface Unrestricted { export interface Unrestricted {
id: string; id: string;
name?: string | null; name?: string | null;
userRestrictedDocs?: (string | UserRestricted)[] | null; userRestrictedDocs?: (string | UserRestrictedCollection)[] | null;
createNotUpdateDocs?: (string | CreateNotUpdate)[] | null; createNotUpdateDocs?: (string | CreateNotUpdateCollection)[] | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "user-restricted". * via the `definition` "user-restricted-collection".
*/ */
export interface UserRestricted { export interface UserRestrictedCollection {
id: string; id: string;
name?: string | null; name?: string | null;
updatedAt: string; updatedAt: string;
@@ -114,9 +116,9 @@ export interface UserRestricted {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "create-not-update". * via the `definition` "create-not-update-collection".
*/ */
export interface CreateNotUpdate { export interface CreateNotUpdateCollection {
id: string; id: string;
name?: string | null; name?: string | null;
updatedAt: string; updatedAt: string;
@@ -303,6 +305,26 @@ export interface ReadOnlyGlobal {
updatedAt?: string | null; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "user-restricted-global".
*/
export interface UserRestrictedGlobal {
id: string;
name?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "read-not-update-global".
*/
export interface ReadNotUpdateGlobal {
id: string;
name?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
declare module 'payload' { declare module 'payload' {

View File

@@ -6,9 +6,11 @@ export const unrestrictedSlug = 'unrestricted'
export const readOnlySlug = 'read-only-collection' export const readOnlySlug = 'read-only-collection'
export const readOnlyGlobalSlug = 'read-only-global' export const readOnlyGlobalSlug = 'read-only-global'
export const userRestrictedSlug = 'user-restricted' export const userRestrictedCollectionSlug = 'user-restricted-collection'
export const fullyRestrictedSlug = 'fully-restricted' export const fullyRestrictedSlug = 'fully-restricted'
export const createNotUpdateSlug = 'create-not-update' export const createNotUpdateCollectionSlug = 'create-not-update-collection'
export const userRestrictedGlobalSlug = 'user-restricted-global'
export const readNotUpdateGlobalSlug = 'read-not-update-global'
export const restrictedVersionsSlug = 'restricted-versions' export const restrictedVersionsSlug = 'restricted-versions'
export const siblingDataSlug = 'sibling-data' export const siblingDataSlug = 'sibling-data'
export const relyOnRequestHeadersSlug = 'rely-on-request-headers' export const relyOnRequestHeadersSlug = 'rely-on-request-headers'

View File

@@ -206,7 +206,7 @@ describe('versions', () => {
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft') await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft')
}) })
test('bulk update - should publish changes', async () => { test('bulk update should publish changes', async () => {
const description = 'published document' const description = 'published document'
await page.goto(url.list) await page.goto(url.list)
@@ -233,7 +233,7 @@ describe('versions', () => {
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Published') await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Published')
}) })
test('bulk update - should draft changes', async () => { test('bulk update should draft changes', async () => {
const description = 'draft document' const description = 'draft document'
await page.goto(url.list) await page.goto(url.list)
@@ -259,7 +259,7 @@ describe('versions', () => {
await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft') await expect(findTableCell(page, '_status', 'Draft Title')).toContainText('Draft')
}) })
test('collection - has versions tab', async () => { test('collection has versions tab', async () => {
await page.goto(url.list) await page.goto(url.list)
await page.locator('tbody tr .cell-title a').first().click() await page.locator('tbody tr .cell-title a').first().click()
@@ -276,7 +276,7 @@ describe('versions', () => {
expect(href).toBe(`${pathname}/versions`) expect(href).toBe(`${pathname}/versions`)
}) })
test('collection - tab displays proper number of versions', async () => { test('collection tab displays proper number of versions', async () => {
await page.goto(url.list) await page.goto(url.list)
const linkToDoc = page const linkToDoc = page
@@ -297,12 +297,10 @@ describe('versions', () => {
expect(versionCount).toBe('11') expect(versionCount).toBe('11')
}) })
test('collection - has versions route', async () => { test('collection has versions route', async () => {
await page.goto(url.list) await page.goto(url.list)
await page.locator('tbody tr .cell-title a').first().click() await page.locator('tbody tr .cell-title a').first().click()
await page.waitForSelector('.doc-header__title', { state: 'visible' }) await page.waitForSelector('.doc-header__title', { state: 'visible' })
await page.goto(`${page.url()}/versions`) await page.goto(`${page.url()}/versions`)
expect(page.url()).toMatch(/\/versions/) expect(page.url()).toMatch(/\/versions/)
}) })
@@ -361,7 +359,7 @@ describe('versions', () => {
// TODO: Check versions/:version-id view for collections / globals // TODO: Check versions/:version-id view for collections / globals
test('global - has versions tab', async () => { test('global has versions tab', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug) const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
await page.goto(global.global(draftGlobalSlug)) await page.goto(global.global(draftGlobalSlug))
@@ -378,7 +376,7 @@ describe('versions', () => {
expect(href).toBe(`${pathname}/versions`) expect(href).toBe(`${pathname}/versions`)
}) })
test('global - has versions route', async () => { test('global has versions route', async () => {
const global = new AdminUrlUtil(serverURL, globalSlug) const global = new AdminUrlUtil(serverURL, globalSlug)
const versionsURL = `${global.global(globalSlug)}/versions` const versionsURL = `${global.global(globalSlug)}/versions`
await page.goto(versionsURL) await page.goto(versionsURL)
@@ -488,7 +486,7 @@ describe('versions', () => {
await expect(page.locator('#field-title')).toHaveValue(spanishTitle) await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
}) })
test('collection - autosave should only update the current document', async () => { test('collection autosave should only update the current document', async () => {
// create and save first doc // create and save first doc
await page.goto(autosaveURL.create) await page.goto(autosaveURL.create)
// Should redirect from /create to /[collectionslug]/[new id] due to auto-save // Should redirect from /create to /[collectionslug]/[new id] due to auto-save
@@ -547,20 +545,18 @@ describe('versions', () => {
await expect(page.locator('#field-title')).toHaveValue('title') await expect(page.locator('#field-title')).toHaveValue('title')
}) })
test('should hide publish when access control prevents updating on globals', async () => { test('globals — should hide publish button when access control prevents update', async () => {
const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug) const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug)
await page.goto(url.global(disablePublishGlobalSlug)) await page.goto(url.global(disablePublishGlobalSlug))
await expect(page.locator('#action-save')).not.toBeAttached() await expect(page.locator('#action-save')).not.toBeAttached()
}) })
test('should hide publish when access control prevents create operation', async () => { test('collections — should hide publish button when access control prevents create', async () => {
await page.goto(disablePublishURL.create) await page.goto(disablePublishURL.create)
await expect(page.locator('#action-save')).not.toBeAttached() await expect(page.locator('#action-save')).not.toBeAttached()
}) })
test('should hide publish when access control prevents update operation', async () => { test('collections — should hide publish button when access control prevents update', async () => {
const publishedDoc = await payload.create({ const publishedDoc = await payload.create({
collection: disablePublishSlug, collection: disablePublishSlug,
data: { data: {
@@ -571,7 +567,6 @@ describe('versions', () => {
}) })
await page.goto(disablePublishURL.edit(String(publishedDoc.id))) await page.goto(disablePublishURL.edit(String(publishedDoc.id)))
await expect(page.locator('#action-save')).not.toBeAttached() await expect(page.locator('#action-save')).not.toBeAttached()
}) })

View File

@@ -37,7 +37,7 @@
], ],
"paths": { "paths": {
"@payload-config": [ "@payload-config": [
"./test/_community/config.ts" "./test/access-control/config.ts"
], ],
"@payloadcms/live-preview": [ "@payloadcms/live-preview": [
"./packages/live-preview/src" "./packages/live-preview/src"