feat(payload, ui): add admin.allowEdit relationship field (#8398)
This PR adds a new property `allowEdit` to the admin of the relationship field. It is very similar to the existing `allowCreate`, only in this case it hides the edit icon: <img width="796" alt="image" src="https://github.com/user-attachments/assets/bbe79bb2-db06-4ec4-b023-2f1c53330fcb">
This commit is contained in:
@@ -90,6 +90,7 @@ The Relationship Field inherits all of the default options from the base [Field
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). |
|
||||
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
|
||||
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
|
||||
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sortOptions) |
|
||||
|
||||
### Sort Options
|
||||
|
||||
@@ -1125,6 +1125,7 @@ type SharedRelationshipPropertiesClient = FieldBaseClient &
|
||||
|
||||
type RelationshipAdmin = {
|
||||
allowCreate?: boolean
|
||||
allowEdit?: boolean
|
||||
components?: {
|
||||
Error?: CustomComponent<
|
||||
RelationshipFieldErrorClientComponent | RelationshipFieldErrorServerComponent
|
||||
@@ -1142,7 +1143,7 @@ type RelationshipAdminClient = {
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<RelationshipAdmin, 'allowCreate' | 'isSortable'>
|
||||
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
|
||||
|
||||
export type PolymorphicRelationshipField = {
|
||||
admin?: {
|
||||
|
||||
@@ -63,7 +63,12 @@ export type ReactSelectAdapterProps = {
|
||||
disabled?: boolean
|
||||
filterOption?:
|
||||
| ((
|
||||
{ data, label, value }: { data: Option; label: string; value: string },
|
||||
{
|
||||
allowEdit,
|
||||
data,
|
||||
label,
|
||||
value,
|
||||
}: { allowEdit: boolean; data: Option; label: string; value: string },
|
||||
search: string,
|
||||
) => boolean)
|
||||
| undefined
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { Option } from '../../elements/ReactSelect/types.js'
|
||||
import type { OptionGroup, Value } from './types.js'
|
||||
|
||||
type Args = {
|
||||
allowEdit: boolean
|
||||
options: OptionGroup[]
|
||||
value: Value | Value[]
|
||||
}
|
||||
|
||||
export const findOptionsByValue = ({ options, value }: Args): Option | Option[] => {
|
||||
export const findOptionsByValue = ({ allowEdit, options, value }: Args): Option | Option[] => {
|
||||
if (value || typeof value === 'number') {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => {
|
||||
@@ -25,7 +26,7 @@ export const findOptionsByValue = ({ options, value }: Args): Option | Option[]
|
||||
}
|
||||
})
|
||||
|
||||
return matchedOption
|
||||
return matchedOption ? { allowEdit, ...matchedOption } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ export const findOptionsByValue = ({ options, value }: Args): Option | Option[]
|
||||
}
|
||||
})
|
||||
|
||||
return matchedOption
|
||||
return matchedOption ? { allowEdit, ...matchedOption } : undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -46,6 +46,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
_path: pathFromProps,
|
||||
admin: {
|
||||
allowCreate = true,
|
||||
allowEdit = true,
|
||||
className,
|
||||
description,
|
||||
isSortable = true,
|
||||
@@ -576,7 +577,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
}
|
||||
}, [openDrawer, currentlyOpenRelationship])
|
||||
|
||||
const valueToRender = findOptionsByValue({ options, value })
|
||||
const valueToRender = findOptionsByValue({ allowEdit, options, value })
|
||||
|
||||
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') {
|
||||
valueToRender.value = null
|
||||
|
||||
@@ -24,7 +24,7 @@ export const MultiValueLabel: React.FC<
|
||||
} & MultiValueProps<Option>
|
||||
> = (props) => {
|
||||
const {
|
||||
data: { label, relationTo, value },
|
||||
data: { allowEdit, label, relationTo, value },
|
||||
selectProps: { customProps: { draggableProps, onDocumentDrawerOpen } = {} } = {},
|
||||
} = props
|
||||
|
||||
@@ -44,7 +44,7 @@ export const MultiValueLabel: React.FC<
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{relationTo && hasReadPermission && (
|
||||
{relationTo && hasReadPermission && allowEdit !== false && (
|
||||
<Fragment>
|
||||
<button
|
||||
aria-label={`Edit ${label}`}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SingleValue: React.FC<
|
||||
> = (props) => {
|
||||
const {
|
||||
children,
|
||||
data: { label, relationTo, value },
|
||||
data: { allowEdit, label, relationTo, value },
|
||||
selectProps: { customProps: { onDocumentDrawerOpen } = {} } = {},
|
||||
} = props
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SingleValue: React.FC<
|
||||
<div className={`${baseClass}__label`}>
|
||||
<div className={`${baseClass}__label-text`}>
|
||||
<div className={`${baseClass}__text`}>{children}</div>
|
||||
{relationTo && hasReadPermission && (
|
||||
{relationTo && hasReadPermission && allowEdit !== false && (
|
||||
<Fragment>
|
||||
<button
|
||||
aria-label={t('general:editLabel', { label })}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientCollectionConfig, ClientConfig, FilterOptionsResult } from 'payload'
|
||||
|
||||
export type Option = {
|
||||
allowEdit: boolean
|
||||
label: string
|
||||
options?: Option[]
|
||||
relationTo?: string
|
||||
|
||||
@@ -159,11 +159,32 @@ describe('relationship', () => {
|
||||
|
||||
test('should hide relationship add new button', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.waitForURL(url.create)
|
||||
const locator1 = page.locator(
|
||||
'#relationWithAllowEditToFalse-add-new .relationship-add-new__add-button',
|
||||
)
|
||||
await expect(locator1).toHaveCount(1)
|
||||
// expect the button to not exist in the field
|
||||
const count = await page
|
||||
.locator('#relationToSelfSelectOnly-add-new .relationship-add-new__add-button')
|
||||
.count()
|
||||
expect(count).toEqual(0)
|
||||
const locator2 = page.locator(
|
||||
'#relationWithAllowCreateToFalse-add-new .relationship-add-new__add-button',
|
||||
)
|
||||
await expect(locator2).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should hide relationship edit button', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.waitForURL(url.create)
|
||||
const locator1 = page
|
||||
.locator('#field-relationWithAllowEditToFalse')
|
||||
.getByLabel('Edit dev@payloadcms.com')
|
||||
await expect(locator1).toHaveCount(0)
|
||||
const locator2 = page
|
||||
.locator('#field-relationWithAllowCreateToFalse')
|
||||
.getByLabel('Edit dev@payloadcms.com')
|
||||
await expect(locator2).toHaveCount(1)
|
||||
// The reason why I check for locator 1 again is that I've noticed that sometimes
|
||||
// the default value does not appear after the first locator is tested. IDK why.
|
||||
await expect(locator1).toHaveCount(0)
|
||||
})
|
||||
|
||||
// TODO: Flaky test in CI - fix this. https://github.com/payloadcms/payload/actions/runs/8910825395/job/24470963991
|
||||
|
||||
@@ -38,10 +38,25 @@ const RelationshipFields: CollectionConfig = {
|
||||
},
|
||||
{
|
||||
name: 'relationToSelfSelectOnly',
|
||||
relationTo: relationshipFieldsSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationWithAllowCreateToFalse',
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
},
|
||||
relationTo: relationshipFieldsSlug,
|
||||
defaultValue: ({ user }) => user?.id,
|
||||
relationTo: 'users',
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationWithAllowEditToFalse',
|
||||
admin: {
|
||||
allowEdit: false,
|
||||
},
|
||||
defaultValue: ({ user }) => user?.id,
|
||||
relationTo: 'users',
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1140,6 +1140,8 @@ export interface RelationshipField {
|
||||
| null;
|
||||
relationToSelf?: (string | null) | RelationshipField;
|
||||
relationToSelfSelectOnly?: (string | null) | RelationshipField;
|
||||
relationWithAllowCreateToFalse?: (string | null) | User;
|
||||
relationWithAllowEditToFalse?: (string | null) | User;
|
||||
relationWithDynamicDefault?: (string | null) | User;
|
||||
relationHasManyWithDynamicDefault?: {
|
||||
relationTo: 'users';
|
||||
|
||||
Reference in New Issue
Block a user