fix(richtext-lexical): link drawer has no fields if parent document create access control is false (#10954)
Previously, the lexical link drawer did not display any fields if the `create` permission was false, even though the `update` permission was true. The issue was a faulty permission check in `RenderFields` that did not check the top-level permission operation keys for truthiness. It only checked if the `permissions` variable itself was `true`, or if the sub-fields had `create` / `update` permissions set to `true`.
This commit is contained in:
@@ -250,8 +250,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
|||||||
setNotLink,
|
setNotLink,
|
||||||
config.routes.admin,
|
config.routes.admin,
|
||||||
config.routes.api,
|
config.routes.api,
|
||||||
config.collections,
|
|
||||||
config.serverURL,
|
config.serverURL,
|
||||||
|
getEntityConfig,
|
||||||
t,
|
t,
|
||||||
i18n,
|
i18n,
|
||||||
locale?.code,
|
locale?.code,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
|
|||||||
// This is different from `admin.readOnly` which is executed based on `operation`
|
// This is different from `admin.readOnly` which is executed based on `operation`
|
||||||
const hasReadPermission =
|
const hasReadPermission =
|
||||||
permissions === true ||
|
permissions === true ||
|
||||||
|
permissions?.read === true ||
|
||||||
permissions?.[parentName] === true ||
|
permissions?.[parentName] === true ||
|
||||||
('name' in field &&
|
('name' in field &&
|
||||||
typeof permissions === 'object' &&
|
typeof permissions === 'object' &&
|
||||||
@@ -79,6 +80,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
|
|||||||
// If the user does not have access control to begin with, force it to be read-only
|
// If the user does not have access control to begin with, force it to be read-only
|
||||||
const hasOperationPermission =
|
const hasOperationPermission =
|
||||||
permissions === true ||
|
permissions === true ||
|
||||||
|
permissions?.[operation] === true ||
|
||||||
permissions?.[parentName] === true ||
|
permissions?.[parentName] === true ||
|
||||||
('name' in field &&
|
('name' in field &&
|
||||||
typeof permissions === 'object' &&
|
typeof permissions === 'object' &&
|
||||||
|
|||||||
@@ -47,13 +47,10 @@ let serverURL: string
|
|||||||
*/
|
*/
|
||||||
async function navigateToLexicalFields(
|
async function navigateToLexicalFields(
|
||||||
navigateToListView: boolean = true,
|
navigateToListView: boolean = true,
|
||||||
localized: boolean = false,
|
collectionSlug: string = 'lexical-fields',
|
||||||
) {
|
) {
|
||||||
if (navigateToListView) {
|
if (navigateToListView) {
|
||||||
const url: AdminUrlUtil = new AdminUrlUtil(
|
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, collectionSlug)
|
||||||
serverURL,
|
|
||||||
localized ? 'lexical-localized-fields' : 'lexical-fields',
|
|
||||||
)
|
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,6 +929,41 @@ describe('lexicalMain', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ensure link drawer displays fields if document does not have `create` permission', async () => {
|
||||||
|
await navigateToLexicalFields(true, 'lexical-access-control')
|
||||||
|
const richTextField = page.locator('.rich-text-lexical').first()
|
||||||
|
await richTextField.scrollIntoViewIfNeeded()
|
||||||
|
await expect(richTextField).toBeVisible()
|
||||||
|
|
||||||
|
const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first()
|
||||||
|
await paragraph.scrollIntoViewIfNeeded()
|
||||||
|
await expect(paragraph).toBeVisible()
|
||||||
|
/**
|
||||||
|
* Type some text
|
||||||
|
*/
|
||||||
|
await paragraph.click()
|
||||||
|
await page.keyboard.type('Text')
|
||||||
|
|
||||||
|
// Select text
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await page.keyboard.press('Shift+ArrowLeft')
|
||||||
|
}
|
||||||
|
// Ensure inline toolbar appeared
|
||||||
|
const inlineToolbar = page.locator('.inline-toolbar-popup')
|
||||||
|
await expect(inlineToolbar).toBeVisible()
|
||||||
|
|
||||||
|
const linkButton = inlineToolbar.locator('.toolbar-popup__button-link')
|
||||||
|
await expect(linkButton).toBeVisible()
|
||||||
|
await linkButton.click()
|
||||||
|
|
||||||
|
const linkDrawer = page.locator('dialog[id^=drawer_1_lexical-rich-text-link-]').first() // IDs starting with drawer_1_lexical-rich-text-link- (there's some other symbol after the underscore)
|
||||||
|
await expect(linkDrawer).toBeVisible()
|
||||||
|
|
||||||
|
const urlInput = linkDrawer.locator('#field-url').first()
|
||||||
|
|
||||||
|
await expect(urlInput).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
test('lexical cursor / selection should be preserved when swapping upload field and clicking within with its list drawer', async () => {
|
test('lexical cursor / selection should be preserved when swapping upload field and clicking within with its list drawer', async () => {
|
||||||
await navigateToLexicalFields()
|
await navigateToLexicalFields()
|
||||||
const richTextField = page.locator('.rich-text-lexical').first()
|
const richTextField = page.locator('.rich-text-lexical').first()
|
||||||
@@ -1292,7 +1324,7 @@ describe('lexicalMain', () => {
|
|||||||
expect(htmlContent).toContain('Start typing, or press')
|
expect(htmlContent).toContain('Start typing, or press')
|
||||||
})
|
})
|
||||||
test.skip('ensure simple localized lexical field works', async () => {
|
test.skip('ensure simple localized lexical field works', async () => {
|
||||||
await navigateToLexicalFields(true, true)
|
await navigateToLexicalFields(true, 'lexical-localized-fields')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
29
test/fields/collections/LexicalAccessControl/index.ts
Normal file
29
test/fields/collections/LexicalAccessControl/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { defaultEditorFeatures, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
import { lexicalAccessControlSlug } from '../../slugs.js'
|
||||||
|
|
||||||
|
export const LexicalAccessControl: CollectionConfig = {
|
||||||
|
slug: lexicalAccessControlSlug,
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
create: () => false,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'richText',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: [...defaultEditorFeatures],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import GroupFields from './collections/Group/index.js'
|
|||||||
import IndexedFields from './collections/Indexed/index.js'
|
import IndexedFields from './collections/Indexed/index.js'
|
||||||
import JSONFields from './collections/JSON/index.js'
|
import JSONFields from './collections/JSON/index.js'
|
||||||
import { LexicalFields } from './collections/Lexical/index.js'
|
import { LexicalFields } from './collections/Lexical/index.js'
|
||||||
|
import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js'
|
||||||
import { LexicalInBlock } from './collections/LexicalInBlock/index.js'
|
import { LexicalInBlock } from './collections/LexicalInBlock/index.js'
|
||||||
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
|
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
|
||||||
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
||||||
@@ -68,6 +69,7 @@ export const collectionSlugs: CollectionConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
LexicalInBlock,
|
LexicalInBlock,
|
||||||
|
LexicalAccessControl,
|
||||||
SelectVersionsFields,
|
SelectVersionsFields,
|
||||||
ArrayFields,
|
ArrayFields,
|
||||||
BlockFields,
|
BlockFields,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface Config {
|
|||||||
lexicalObjectReferenceBug: LexicalObjectReferenceBug;
|
lexicalObjectReferenceBug: LexicalObjectReferenceBug;
|
||||||
users: User;
|
users: User;
|
||||||
LexicalInBlock: LexicalInBlock;
|
LexicalInBlock: LexicalInBlock;
|
||||||
|
'lexical-access-control': LexicalAccessControl;
|
||||||
'select-versions-fields': SelectVersionsField;
|
'select-versions-fields': SelectVersionsField;
|
||||||
'array-fields': ArrayField;
|
'array-fields': ArrayField;
|
||||||
'block-fields': BlockField;
|
'block-fields': BlockField;
|
||||||
@@ -80,6 +81,7 @@ export interface Config {
|
|||||||
lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>;
|
lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>;
|
LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>;
|
||||||
|
'lexical-access-control': LexicalAccessControlSelect<false> | LexicalAccessControlSelect<true>;
|
||||||
'select-versions-fields': SelectVersionsFieldsSelect<false> | SelectVersionsFieldsSelect<true>;
|
'select-versions-fields': SelectVersionsFieldsSelect<false> | SelectVersionsFieldsSelect<true>;
|
||||||
'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>;
|
'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>;
|
||||||
'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>;
|
'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>;
|
||||||
@@ -454,6 +456,31 @@ export interface LexicalInBlock {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "lexical-access-control".
|
||||||
|
*/
|
||||||
|
export interface LexicalAccessControl {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
richText?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "select-versions-fields".
|
* via the `definition` "select-versions-fields".
|
||||||
@@ -469,7 +496,7 @@ export interface SelectVersionsField {
|
|||||||
| null;
|
| null;
|
||||||
blocks?:
|
blocks?:
|
||||||
| {
|
| {
|
||||||
hasManyArr?: ('a' | 'b' | 'c')[] | null;
|
hasManyBlocks?: ('a' | 'b' | 'c')[] | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'block';
|
blockType: 'block';
|
||||||
@@ -1830,6 +1857,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'LexicalInBlock';
|
relationTo: 'LexicalInBlock';
|
||||||
value: string | LexicalInBlock;
|
value: string | LexicalInBlock;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'lexical-access-control';
|
||||||
|
value: string | LexicalAccessControl;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'select-versions-fields';
|
relationTo: 'select-versions-fields';
|
||||||
value: string | SelectVersionsField;
|
value: string | SelectVersionsField;
|
||||||
@@ -2104,6 +2135,16 @@ export interface LexicalInBlockSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "lexical-access-control_select".
|
||||||
|
*/
|
||||||
|
export interface LexicalAccessControlSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
richText?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "select-versions-fields_select".
|
* via the `definition` "select-versions-fields_select".
|
||||||
@@ -2122,7 +2163,7 @@ export interface SelectVersionsFieldsSelect<T extends boolean = true> {
|
|||||||
block?:
|
block?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
hasManyArr?: T;
|
hasManyBlocks?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -495,10 +495,12 @@ export const seed = async (_payload: Payload) => {
|
|||||||
data: {
|
data: {
|
||||||
text: 'text',
|
text: 'text',
|
||||||
},
|
},
|
||||||
|
depth: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
await _payload.create({
|
await _payload.create({
|
||||||
collection: 'LexicalInBlock',
|
collection: 'LexicalInBlock',
|
||||||
|
depth: 0,
|
||||||
data: {
|
data: {
|
||||||
content: {
|
content: {
|
||||||
root: {
|
root: {
|
||||||
@@ -537,24 +539,36 @@ export const seed = async (_payload: Payload) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await _payload.create({
|
||||||
|
collection: 'lexical-access-control',
|
||||||
|
data: {
|
||||||
|
richText: textToLexicalJSON({ text: 'text' }),
|
||||||
|
title: 'title',
|
||||||
|
},
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: customIDSlug,
|
collection: customIDSlug,
|
||||||
data: {
|
data: {
|
||||||
id: nonStandardID,
|
id: nonStandardID,
|
||||||
},
|
},
|
||||||
|
depth: 0,
|
||||||
}),
|
}),
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: customTabIDSlug,
|
collection: customTabIDSlug,
|
||||||
data: {
|
data: {
|
||||||
id: customTabID,
|
id: customTabID,
|
||||||
},
|
},
|
||||||
|
depth: 0,
|
||||||
}),
|
}),
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: customRowIDSlug,
|
collection: customRowIDSlug,
|
||||||
data: {
|
data: {
|
||||||
id: customRowID,
|
id: customRowID,
|
||||||
},
|
},
|
||||||
|
depth: 0,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export const lexicalFieldsSlug = 'lexical-fields'
|
|||||||
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
|
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
|
||||||
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
|
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
|
||||||
export const lexicalRelationshipFieldsSlug = 'lexical-relationship-fields'
|
export const lexicalRelationshipFieldsSlug = 'lexical-relationship-fields'
|
||||||
|
|
||||||
|
export const lexicalAccessControlSlug = 'lexical-access-control'
|
||||||
|
|
||||||
export const numberFieldsSlug = 'number-fields'
|
export const numberFieldsSlug = 'number-fields'
|
||||||
export const pointFieldsSlug = 'point-fields'
|
export const pointFieldsSlug = 'point-fields'
|
||||||
export const radioFieldsSlug = 'radio-fields'
|
export const radioFieldsSlug = 'radio-fields'
|
||||||
@@ -52,6 +55,7 @@ export const collectionSlugs = [
|
|||||||
lexicalFieldsSlug,
|
lexicalFieldsSlug,
|
||||||
lexicalMigrateFieldsSlug,
|
lexicalMigrateFieldsSlug,
|
||||||
lexicalRelationshipFieldsSlug,
|
lexicalRelationshipFieldsSlug,
|
||||||
|
lexicalAccessControlSlug,
|
||||||
numberFieldsSlug,
|
numberFieldsSlug,
|
||||||
pointFieldsSlug,
|
pointFieldsSlug,
|
||||||
radioFieldsSlug,
|
radioFieldsSlug,
|
||||||
|
|||||||
Reference in New Issue
Block a user