fix(ui): custom buttons and e2e refresh permissions test (#5458)
* moved refresh permissions test suite to access control * support for custom Save, SaveDraft and Publish buttons in admin config for collections and globals * moved navigation content to client side so that permissions can be refreshed from active state
This commit is contained in:
@@ -1,13 +1 @@
|
||||
export type CustomPublishButtonProps = React.ComponentType<
|
||||
DefaultPublishButtonProps & {
|
||||
DefaultButton: React.ComponentType<DefaultPublishButtonProps>
|
||||
}
|
||||
>
|
||||
|
||||
export type DefaultPublishButtonProps = {
|
||||
canPublish: boolean
|
||||
disabled: boolean
|
||||
id?: string
|
||||
label: string
|
||||
publish: () => void
|
||||
}
|
||||
export type CustomPublishButtonProps = React.ComponentType
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
export type CustomSaveButtonProps = React.ComponentType<
|
||||
DefaultSaveButtonProps & {
|
||||
DefaultButton: React.ComponentType<DefaultSaveButtonProps>
|
||||
}
|
||||
>
|
||||
|
||||
export type DefaultSaveButtonProps = {
|
||||
label: string
|
||||
save: () => void
|
||||
}
|
||||
export type CustomSaveButtonProps = React.ComponentType
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
export type CustomSaveDraftButtonProps = React.ComponentType<
|
||||
DefaultSaveDraftButtonProps & {
|
||||
DefaultButton: React.ComponentType<DefaultSaveDraftButtonProps>
|
||||
}
|
||||
>
|
||||
|
||||
export type DefaultSaveDraftButtonProps = {
|
||||
disabled: boolean
|
||||
label: string
|
||||
saveDraft: () => void
|
||||
}
|
||||
export type CustomSaveDraftButtonProps = React.ComponentType
|
||||
|
||||
@@ -4,11 +4,8 @@ export type { ConditionalDateProps } from './elements/DatePicker.js'
|
||||
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
|
||||
export type { DefaultPreviewButtonProps } from './elements/PreviewButton.js'
|
||||
export type { CustomPreviewButtonProps } from './elements/PreviewButton.js'
|
||||
export type { DefaultPublishButtonProps } from './elements/PublishButton.js'
|
||||
export type { CustomPublishButtonProps } from './elements/PublishButton.js'
|
||||
export type { DefaultSaveButtonProps } from './elements/SaveButton.js'
|
||||
export type { CustomSaveButtonProps } from './elements/SaveButton.js'
|
||||
export type { DefaultSaveDraftButtonProps } from './elements/SaveDraftButton.js'
|
||||
export type { CustomSaveDraftButtonProps } from './elements/SaveDraftButton.js'
|
||||
export type {
|
||||
DocumentTab,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { CollectionPermission, GlobalPermission } from 'payload/auth'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
@@ -47,10 +48,16 @@ export const DocumentControls: React.FC<{
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const config = useConfig()
|
||||
const { getComponentMap } = useComponentMap()
|
||||
|
||||
const collectionConfig = config.collections.find((coll) => coll.slug === slug)
|
||||
const globalConfig = config.globals.find((global) => global.slug === slug)
|
||||
|
||||
const componentMap = getComponentMap({
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
})
|
||||
|
||||
const {
|
||||
admin: { dateFormat },
|
||||
routes: { admin: adminRoute },
|
||||
@@ -164,27 +171,12 @@ export const DocumentControls: React.FC<{
|
||||
!collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts &&
|
||||
!globalConfig?.versions?.drafts?.autosave)) && (
|
||||
<SaveDraft
|
||||
CustomComponent={
|
||||
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveDraftButton
|
||||
}
|
||||
/>
|
||||
<SaveDraft CustomComponent={componentMap.SaveDraftButton} />
|
||||
)}
|
||||
<Publish
|
||||
CustomComponent={
|
||||
collectionConfig?.admin?.components?.edit?.PublishButton ||
|
||||
globalConfig?.admin?.components?.elements?.PublishButton
|
||||
}
|
||||
/>
|
||||
<Publish CustomComponent={componentMap.PublishButton} />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Save
|
||||
CustomComponent={
|
||||
collectionConfig?.admin?.components?.edit?.SaveButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveButton
|
||||
}
|
||||
/>
|
||||
<Save CustomComponent={componentMap.SaveButton} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
107
packages/ui/src/elements/Nav/index.client.tsx
Normal file
107
packages/ui/src/elements/Nav/index.client.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { isEntityHidden } from 'payload/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import type { EntityToGroup } from '../../utilities/groupNavItems.js'
|
||||
|
||||
import { Chevron } from '../../icons/Chevron/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { EntityType, groupNavItems } from '../../utilities/groupNavItems.js'
|
||||
import { NavGroup } from '../NavGroup/index.js'
|
||||
import { useNav } from './context.js'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
export const DefaultNavClient: React.FC = () => {
|
||||
const { permissions, user } = useAuth()
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin },
|
||||
} = useConfig()
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const { navOpen } = useNav()
|
||||
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
// @ts-expect-error todo: fix type error here
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
...globals
|
||||
// @ts-expect-error todo: fix type error here
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup key={key} label={label}>
|
||||
{entities.map(({ type, entity }, i) => {
|
||||
let entityLabel: string
|
||||
let href: string
|
||||
let id: string
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
||||
id = `nav-${entity.slug}`
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = `${admin}/globals/${entity.slug}`
|
||||
entityLabel = getTranslation(entity.label, i18n)
|
||||
id = `nav-global-${entity.slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
|
||||
return (
|
||||
<LinkElement
|
||||
// activeClassName="active"
|
||||
className={`${baseClass}__link`}
|
||||
href={href}
|
||||
id={id}
|
||||
key={i}
|
||||
tabIndex={!navOpen ? -1 : undefined}
|
||||
>
|
||||
<span className={`${baseClass}__link-icon`}>
|
||||
<Chevron direction="right" />
|
||||
</span>
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
})}
|
||||
</NavGroup>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,20 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Permissions, User } from 'payload/auth'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { isEntityHidden } from 'payload/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import type { EntityToGroup } from '../../utilities/groupNavItems.js'
|
||||
|
||||
import { Chevron } from '../../icons/Chevron/index.js'
|
||||
import { EntityType, groupNavItems } from '../../utilities/groupNavItems.js'
|
||||
import { Logout } from '../Logout/index.js'
|
||||
import { NavGroup } from '../NavGroup/index.js'
|
||||
import { NavHamburger } from './NavHamburger/index.js'
|
||||
import { NavWrapper } from './NavWrapper/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
import { DefaultNavClient } from './index.client.js'
|
||||
|
||||
export const DefaultNav: React.FC<{
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
permissions: Permissions
|
||||
user: User
|
||||
}> = (props) => {
|
||||
const { config, i18n, permissions, user } = props
|
||||
const { config } = props
|
||||
|
||||
if (!config) {
|
||||
return null
|
||||
@@ -35,87 +24,14 @@ export const DefaultNav: React.FC<{
|
||||
admin: {
|
||||
components: { afterNavLinks, beforeNavLinks },
|
||||
},
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
...globals
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<NavWrapper baseClass={baseClass}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(beforeNavLinks) &&
|
||||
beforeNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup key={key} label={label}>
|
||||
{entities.map(({ type, entity }, i) => {
|
||||
let entityLabel: string
|
||||
let href: string
|
||||
let id: string
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
||||
id = `nav-${entity.slug}`
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = `${admin}/globals/${entity.slug}`
|
||||
entityLabel = getTranslation(entity.label, i18n)
|
||||
id = `nav-global-${entity.slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
|
||||
return (
|
||||
<LinkElement
|
||||
// activeClassName="active"
|
||||
className={`${baseClass}__link`}
|
||||
href={href}
|
||||
id={id}
|
||||
// tabIndex={!navOpen ? -1 : undefined}
|
||||
key={i}
|
||||
>
|
||||
<span className={`${baseClass}__link-icon`}>
|
||||
<Chevron direction="right" />
|
||||
</span>
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
})}
|
||||
</NavGroup>
|
||||
)
|
||||
})}
|
||||
<DefaultNavClient />
|
||||
{Array.isArray(afterNavLinks) && afterNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Logout />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
'use client'
|
||||
import type { CustomPublishButtonProps, DefaultPublishButtonProps } from 'payload/types'
|
||||
|
||||
import qs from 'qs'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
import { useForm, useFormModified } from '../../forms/Form/context.js'
|
||||
import { FormSubmit } from '../../forms/Submit/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
@@ -12,27 +10,7 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
|
||||
const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
|
||||
id,
|
||||
canPublish,
|
||||
disabled,
|
||||
label,
|
||||
publish,
|
||||
}) => {
|
||||
if (!canPublish) return null
|
||||
|
||||
return (
|
||||
<FormSubmit buttonId={id} disabled={disabled} onClick={publish} size="small" type="button">
|
||||
{label}
|
||||
</FormSubmit>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
CustomComponent?: CustomPublishButtonProps
|
||||
}
|
||||
|
||||
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
const DefaultPublishButton: React.FC = () => {
|
||||
const { code } = useLocale()
|
||||
const { id, collectionSlug, globalSlug, publishedDoc, unpublishedVersions } = useDocumentInfo()
|
||||
const [hasPublishPermission, setHasPublishPermission] = React.useState(false)
|
||||
@@ -43,9 +21,10 @@ export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
const { t } = useTranslation()
|
||||
const label = t('version:publishChanges')
|
||||
|
||||
const hasNewerVersions = unpublishedVersions?.totalDocs > 0
|
||||
const canPublish = modified || hasNewerVersions || !publishedDoc
|
||||
const canPublish = hasPublishPermission && (modified || hasNewerVersions || !publishedDoc)
|
||||
|
||||
const publish = useCallback(() => {
|
||||
void submit({
|
||||
@@ -95,18 +74,27 @@ export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
void fetchPublishAccess()
|
||||
}, [api, code, collectionSlug, getData, globalSlug, id, serverURL])
|
||||
|
||||
if (!canPublish) return null
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomComponent}
|
||||
DefaultComponent={DefaultPublishButton}
|
||||
componentProps={{
|
||||
id: 'action-save',
|
||||
DefaultButton: DefaultPublishButton,
|
||||
canPublish: hasPublishPermission,
|
||||
disabled: !canPublish,
|
||||
label: t('version:publishChanges'),
|
||||
publish,
|
||||
}}
|
||||
/>
|
||||
<FormSubmit
|
||||
buttonId={id.toString()}
|
||||
disabled={!canPublish}
|
||||
onClick={publish}
|
||||
size="small"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
</FormSubmit>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
CustomComponent?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
|
||||
if (CustomComponent) return CustomComponent
|
||||
|
||||
return <DefaultPublishButton />
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
'use client'
|
||||
import type { CustomSaveButtonProps, DefaultSaveButtonProps } from 'payload/types'
|
||||
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
import { useForm } from '../../forms/Form/context.js'
|
||||
import { FormSubmit } from '../../forms/Submit/index.js'
|
||||
import { useHotkey } from '../../hooks/useHotkey.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
|
||||
const DefaultSaveButton: React.FC<DefaultSaveButtonProps> = ({ label, save }) => {
|
||||
const DefaultSaveButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { submit } = useForm()
|
||||
const label = t('general:save')
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
@@ -23,29 +24,24 @@ const DefaultSaveButton: React.FC<DefaultSaveButtonProps> = ({ label, save }) =>
|
||||
})
|
||||
|
||||
return (
|
||||
<FormSubmit buttonId="action-save" onClick={save} ref={ref} size="small" type="button">
|
||||
<FormSubmit
|
||||
buttonId="action-save"
|
||||
onClick={() => submit()}
|
||||
ref={ref}
|
||||
size="small"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
</FormSubmit>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
CustomComponent?: CustomSaveButtonProps
|
||||
CustomComponent?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Save: React.FC<Props> = ({ CustomComponent }) => {
|
||||
const { t } = useTranslation()
|
||||
const { submit } = useForm()
|
||||
if (CustomComponent) return CustomComponent
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomComponent}
|
||||
DefaultComponent={DefaultSaveButton}
|
||||
componentProps={{
|
||||
DefaultButton: DefaultSaveButton,
|
||||
label: t('general:save'),
|
||||
save: submit,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <DefaultSaveButton />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client'
|
||||
import type { CustomSaveDraftButtonProps, DefaultSaveDraftButtonProps } from 'payload/types'
|
||||
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
import { useForm, useFormModified } from '../../forms/Form/context.js'
|
||||
import { FormSubmit } from '../../forms/Submit/index.js'
|
||||
import { useHotkey } from '../../hooks/useHotkey.js'
|
||||
@@ -15,57 +13,19 @@ import { useTranslation } from '../../providers/Translation/index.js'
|
||||
|
||||
const baseClass = 'save-draft'
|
||||
|
||||
const DefaultSaveDraftButton: React.FC<DefaultSaveDraftButtonProps> = ({
|
||||
disabled,
|
||||
label,
|
||||
saveDraft,
|
||||
}) => {
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (ref?.current) {
|
||||
ref.current.click()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<FormSubmit
|
||||
buttonId="action-save-draft"
|
||||
buttonStyle="secondary"
|
||||
className={baseClass}
|
||||
disabled={disabled}
|
||||
onClick={saveDraft}
|
||||
ref={ref}
|
||||
size="small"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
</FormSubmit>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
CustomComponent?: CustomSaveDraftButtonProps
|
||||
}
|
||||
export const SaveDraft: React.FC<Props> = ({ CustomComponent }) => {
|
||||
const DefaultSaveDraftButton: React.FC = () => {
|
||||
const {
|
||||
routes: { api },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
const { submit } = useForm()
|
||||
const { id, collectionSlug, globalSlug } = useDocumentInfo()
|
||||
const modified = useFormModified()
|
||||
const { code: locale } = useLocale()
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
const editDepth = useEditDepth()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const canSaveDraft = modified
|
||||
const { submit } = useForm()
|
||||
const label = t('general:save')
|
||||
|
||||
const saveDraft = useCallback(async () => {
|
||||
const search = `?locale=${locale}&depth=0&fallback-locale=null&draft=true`
|
||||
@@ -91,16 +51,40 @@ export const SaveDraft: React.FC<Props> = ({ CustomComponent }) => {
|
||||
})
|
||||
}, [submit, collectionSlug, globalSlug, serverURL, api, locale, id])
|
||||
|
||||
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
|
||||
if (!modified) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (ref?.current) {
|
||||
ref.current.click()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomComponent}
|
||||
DefaultComponent={DefaultSaveDraftButton}
|
||||
componentProps={{
|
||||
DefaultButton: DefaultSaveDraftButton,
|
||||
disabled: !canSaveDraft,
|
||||
label: t('version:saveDraft'),
|
||||
saveDraft,
|
||||
}}
|
||||
/>
|
||||
<FormSubmit
|
||||
buttonId="action-save-draft"
|
||||
buttonStyle="secondary"
|
||||
className={baseClass}
|
||||
disabled={!modified}
|
||||
onClick={saveDraft}
|
||||
ref={ref}
|
||||
size="small"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
</FormSubmit>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
CustomComponent?: React.ReactNode
|
||||
}
|
||||
|
||||
export const SaveDraft: React.FC<Props> = ({ CustomComponent }) => {
|
||||
if (CustomComponent) return CustomComponent
|
||||
|
||||
return <DefaultSaveDraftButton />
|
||||
}
|
||||
|
||||
@@ -64,6 +64,18 @@ export const buildComponentMap = (args: {
|
||||
|
||||
const afterListTable = collectionConfig?.admin?.components?.AfterListTable
|
||||
|
||||
const SaveButton = collectionConfig?.admin?.components?.edit?.SaveButton
|
||||
const SaveButtonComponent = SaveButton ? <SaveButton /> : undefined
|
||||
|
||||
const SaveDraftButton = collectionConfig?.admin?.components?.edit?.SaveDraftButton
|
||||
const SaveDraftButtonComponent = SaveDraftButton ? <SaveDraftButton /> : undefined
|
||||
|
||||
/* const PreviewButton = collectionConfig?.admin?.components?.edit?.PreviewButton
|
||||
const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined */
|
||||
|
||||
const PublishButton = collectionConfig?.admin?.components?.edit?.PublishButton
|
||||
const PublishButtonComponent = PublishButton ? <PublishButton /> : undefined
|
||||
|
||||
const BeforeList =
|
||||
(beforeList && Array.isArray(beforeList) && beforeList?.map((Component) => <Component />)) ||
|
||||
null
|
||||
@@ -99,6 +111,10 @@ export const buildComponentMap = (args: {
|
||||
BeforeListTable,
|
||||
Edit: <Edit collectionSlug={collectionConfig.slug} />,
|
||||
List: <List collectionSlug={collectionConfig.slug} />,
|
||||
/* PreviewButton: PreviewButtonComponent, */
|
||||
PublishButton: PublishButtonComponent,
|
||||
SaveButton: SaveButtonComponent,
|
||||
SaveDraftButton: SaveDraftButtonComponent,
|
||||
actionsMap: mapActions({
|
||||
collectionConfig,
|
||||
}),
|
||||
@@ -121,6 +137,18 @@ export const buildComponentMap = (args: {
|
||||
|
||||
const editViewFromConfig = globalConfig?.admin?.components?.views?.Edit
|
||||
|
||||
const SaveButton = globalConfig?.admin?.components?.elements?.SaveButton
|
||||
const SaveButtonComponent = SaveButton ? <SaveButton /> : undefined
|
||||
|
||||
const SaveDraftButton = globalConfig?.admin?.components?.elements?.SaveDraftButton
|
||||
const SaveDraftButtonComponent = SaveDraftButton ? <SaveDraftButton /> : undefined
|
||||
|
||||
/* const PreviewButton = globalConfig?.admin?.components?.elements?.PreviewButton
|
||||
const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined */
|
||||
|
||||
const PublishButton = globalConfig?.admin?.components?.elements?.PublishButton
|
||||
const PublishButtonComponent = PublishButton ? <PublishButton /> : undefined
|
||||
|
||||
const CustomEditView =
|
||||
typeof editViewFromConfig === 'function'
|
||||
? editViewFromConfig
|
||||
@@ -136,6 +164,10 @@ export const buildComponentMap = (args: {
|
||||
|
||||
const componentMap: GlobalComponentMap = {
|
||||
Edit: <Edit globalSlug={globalConfig.slug} />,
|
||||
/* PreviewButton: PreviewButtonComponent, */
|
||||
PublishButton: PublishButtonComponent,
|
||||
SaveButton: SaveButtonComponent,
|
||||
SaveDraftButton: SaveDraftButtonComponent,
|
||||
actionsMap: mapActions({
|
||||
globalConfig,
|
||||
}),
|
||||
|
||||
@@ -103,6 +103,10 @@ export type GlobalComponentMap = ConfigComponentMapBase
|
||||
|
||||
export type ConfigComponentMapBase = {
|
||||
Edit: React.ReactNode
|
||||
/* PreviewButton: React.ReactNode */
|
||||
PublishButton: React.ReactNode
|
||||
SaveButton: React.ReactNode
|
||||
SaveDraftButton: React.ReactNode
|
||||
actionsMap: ActionMap
|
||||
fieldMap: FieldMap
|
||||
}
|
||||
|
||||
27
test/access-control/TestButton.tsx
Normal file
27
test/access-control/TestButton.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import { useForm } from '@payloadcms/ui/forms/Form'
|
||||
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React from 'react'
|
||||
|
||||
export const TestButton: React.FC = () => {
|
||||
const { refreshPermissions } = useAuth()
|
||||
const { submit } = useForm()
|
||||
const { t } = useTranslation()
|
||||
const label = t('general:save')
|
||||
|
||||
return (
|
||||
<button
|
||||
id="action-save"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
void refreshPermissions()
|
||||
void submit()
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Custom: {label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { FieldAccess } from 'payload/types'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { TestButton } from './TestButton.js'
|
||||
import {
|
||||
docLevelAccessSlug,
|
||||
firstArrayText,
|
||||
@@ -41,6 +42,35 @@ export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
user: 'users',
|
||||
},
|
||||
globals: [
|
||||
{
|
||||
slug: 'settings',
|
||||
fields: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'test',
|
||||
label: 'Allow access to test global',
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
components: {
|
||||
elements: {
|
||||
SaveButton: TestButton,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'test',
|
||||
fields: [],
|
||||
access: {
|
||||
read: async ({ req: { payload } }) => {
|
||||
const access = await payload.findGlobal({ slug: 'settings' })
|
||||
return Boolean(access.test)
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
|
||||
@@ -7,7 +7,13 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
import type { ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
|
||||
|
||||
import { exactText, initPageConsoleErrorCatch, openDocControls, openNav } from '../helpers.js'
|
||||
import {
|
||||
closeNav,
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocControls,
|
||||
openNav,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||
import config from './config.js'
|
||||
@@ -250,6 +256,30 @@ describe('access control', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('should show test global immediately after allowing access', async () => {
|
||||
await page.goto(`${serverURL}/admin/globals/settings`)
|
||||
|
||||
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 page.locator('#action-save').click()
|
||||
|
||||
await openNav(page)
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
await expect(page.locator('#nav-global-test')).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintain access control in document drawer', async () => {
|
||||
const unrestrictedDoc = await payload.create({
|
||||
collection: unrestrictedSlug,
|
||||
@@ -260,7 +290,7 @@ describe('access control', () => {
|
||||
|
||||
// navigate to the `unrestricted` document and open the drawers to test access
|
||||
const unrestrictedURL = new AdminUrlUtil(serverURL, unrestrictedSlug)
|
||||
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id))
|
||||
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id.toString()))
|
||||
|
||||
const addDocButton = page.locator(
|
||||
'#userRestrictedDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
|
||||
@@ -289,6 +319,7 @@ describe('access control', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async function createDoc(data: any): Promise<TypeWithID & Record<string, unknown>> {
|
||||
return payload.create({
|
||||
collection: slug,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
ignorePatterns: ['payload-types.ts'],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.eslint.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { EditViewProps } from 'payload/types'
|
||||
|
||||
import { EditView as DefaultEditView } from '@payloadcms/next/views'
|
||||
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
const GlobalView: React.FC<EditViewProps> = (props) => {
|
||||
const { onSave } = props
|
||||
const { refreshPermissions } = useAuth()
|
||||
const modifiedOnSave = useCallback(
|
||||
(...args) => {
|
||||
onSave.call(null, ...args)
|
||||
void refreshPermissions()
|
||||
},
|
||||
[onSave, refreshPermissions],
|
||||
)
|
||||
|
||||
return <DefaultEditView {...props} onSave={modifiedOnSave} />
|
||||
}
|
||||
|
||||
export default GlobalView
|
||||
@@ -1,53 +0,0 @@
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import GlobalViewWithRefresh from './GlobalViewWithRefresh.js'
|
||||
|
||||
export const pagesSlug = 'pages'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
globals: [
|
||||
{
|
||||
slug: 'settings',
|
||||
fields: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'test',
|
||||
label: 'Allow access to test global',
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: GlobalViewWithRefresh,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'test',
|
||||
fields: [],
|
||||
access: {
|
||||
read: async ({ req: { payload } }) => {
|
||||
const access = await payload.findGlobal({ slug: 'settings' })
|
||||
return access.test
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { closeNav, initPageConsoleErrorCatch, openNav } from '../helpers.js'
|
||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||
import config from './config.js'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
|
||||
describe('refresh-permissions', () => {
|
||||
let serverURL: string
|
||||
let page: Page
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
;({ serverURL } = await initPayloadE2E({ config, dirname }))
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('should show test global immediately after allowing access', async () => {
|
||||
await page.goto(`${serverURL}/admin/globals/settings`)
|
||||
|
||||
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 page.locator('#action-save').click()
|
||||
|
||||
await openNav(page)
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
await expect(page.locator('#nav-global-test')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user