Skip to content

Commit 136c90c

Browse files
authored
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`.
1 parent 6353cf8 commit 136c90c

File tree

8 files changed

+133
-9
lines changed

8 files changed

+133
-9
lines changed

packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
250250
setNotLink,
251251
config.routes.admin,
252252
config.routes.api,
253-
config.collections,
254253
config.serverURL,
254+
getEntityConfig,
255255
t,
256256
i18n,
257257
locale?.code,

packages/ui/src/forms/RenderFields/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
5757
// This is different from `admin.readOnly` which is executed based on `operation`
5858
const hasReadPermission =
5959
permissions === true ||
60+
permissions?.read === true ||
6061
permissions?.[parentName] === true ||
6162
('name' in field &&
6263
typeof permissions === 'object' &&
@@ -79,6 +80,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
7980
// If the user does not have access control to begin with, force it to be read-only
8081
const hasOperationPermission =
8182
permissions === true ||
83+
permissions?.[operation] === true ||
8284
permissions?.[parentName] === true ||
8385
('name' in field &&
8486
typeof permissions === 'object' &&

test/fields/collections/Lexical/e2e/main/e2e.spec.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,10 @@ let serverURL: string
4747
*/
4848
async function navigateToLexicalFields(
4949
navigateToListView: boolean = true,
50-
localized: boolean = false,
50+
collectionSlug: string = 'lexical-fields',
5151
) {
5252
if (navigateToListView) {
53-
const url: AdminUrlUtil = new AdminUrlUtil(
54-
serverURL,
55-
localized ? 'lexical-localized-fields' : 'lexical-fields',
56-
)
53+
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, collectionSlug)
5754
await page.goto(url.list)
5855
}
5956

@@ -932,6 +929,41 @@ describe('lexicalMain', () => {
932929
})
933930
})
934931

932+
test('ensure link drawer displays fields if document does not have `create` permission', async () => {
933+
await navigateToLexicalFields(true, 'lexical-access-control')
934+
const richTextField = page.locator('.rich-text-lexical').first()
935+
await richTextField.scrollIntoViewIfNeeded()
936+
await expect(richTextField).toBeVisible()
937+
938+
const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first()
939+
await paragraph.scrollIntoViewIfNeeded()
940+
await expect(paragraph).toBeVisible()
941+
/**
942+
* Type some text
943+
*/
944+
await paragraph.click()
945+
await page.keyboard.type('Text')
946+
947+
// Select text
948+
for (let i = 0; i < 4; i++) {
949+
await page.keyboard.press('Shift+ArrowLeft')
950+
}
951+
// Ensure inline toolbar appeared
952+
const inlineToolbar = page.locator('.inline-toolbar-popup')
953+
await expect(inlineToolbar).toBeVisible()
954+
955+
const linkButton = inlineToolbar.locator('.toolbar-popup__button-link')
956+
await expect(linkButton).toBeVisible()
957+
await linkButton.click()
958+
959+
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)
960+
await expect(linkDrawer).toBeVisible()
961+
962+
const urlInput = linkDrawer.locator('#field-url').first()
963+
964+
await expect(urlInput).toBeVisible()
965+
})
966+
935967
test('lexical cursor / selection should be preserved when swapping upload field and clicking within with its list drawer', async () => {
936968
await navigateToLexicalFields()
937969
const richTextField = page.locator('.rich-text-lexical').first()
@@ -1292,7 +1324,7 @@ describe('lexicalMain', () => {
12921324
expect(htmlContent).toContain('Start typing, or press')
12931325
})
12941326
test.skip('ensure simple localized lexical field works', async () => {
1295-
await navigateToLexicalFields(true, true)
1327+
await navigateToLexicalFields(true, 'lexical-localized-fields')
12961328
})
12971329
})
12981330

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { defaultEditorFeatures, lexicalEditor } from '@payloadcms/richtext-lexical'
4+
5+
import { lexicalAccessControlSlug } from '../../slugs.js'
6+
7+
export const LexicalAccessControl: CollectionConfig = {
8+
slug: lexicalAccessControlSlug,
9+
access: {
10+
read: () => true,
11+
create: () => false,
12+
},
13+
admin: {
14+
useAsTitle: 'title',
15+
},
16+
fields: [
17+
{
18+
name: 'title',
19+
type: 'text',
20+
},
21+
{
22+
name: 'richText',
23+
type: 'richText',
24+
editor: lexicalEditor({
25+
features: [...defaultEditorFeatures],
26+
}),
27+
},
28+
],
29+
}

test/fields/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import GroupFields from './collections/Group/index.js'
2121
import IndexedFields from './collections/Indexed/index.js'
2222
import JSONFields from './collections/JSON/index.js'
2323
import { LexicalFields } from './collections/Lexical/index.js'
24+
import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js'
2425
import { LexicalInBlock } from './collections/LexicalInBlock/index.js'
2526
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
2627
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
@@ -68,6 +69,7 @@ export const collectionSlugs: CollectionConfig[] = [
6869
],
6970
},
7071
LexicalInBlock,
72+
LexicalAccessControl,
7173
SelectVersionsFields,
7274
ArrayFields,
7375
BlockFields,

test/fields/payload-types.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface Config {
3434
lexicalObjectReferenceBug: LexicalObjectReferenceBug;
3535
users: User;
3636
LexicalInBlock: LexicalInBlock;
37+
'lexical-access-control': LexicalAccessControl;
3738
'select-versions-fields': SelectVersionsField;
3839
'array-fields': ArrayField;
3940
'block-fields': BlockField;
@@ -80,6 +81,7 @@ export interface Config {
8081
lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>;
8182
users: UsersSelect<false> | UsersSelect<true>;
8283
LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>;
84+
'lexical-access-control': LexicalAccessControlSelect<false> | LexicalAccessControlSelect<true>;
8385
'select-versions-fields': SelectVersionsFieldsSelect<false> | SelectVersionsFieldsSelect<true>;
8486
'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>;
8587
'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>;
@@ -454,6 +456,31 @@ export interface LexicalInBlock {
454456
updatedAt: string;
455457
createdAt: string;
456458
}
459+
/**
460+
* This interface was referenced by `Config`'s JSON-Schema
461+
* via the `definition` "lexical-access-control".
462+
*/
463+
export interface LexicalAccessControl {
464+
id: string;
465+
title?: string | null;
466+
richText?: {
467+
root: {
468+
type: string;
469+
children: {
470+
type: string;
471+
version: number;
472+
[k: string]: unknown;
473+
}[];
474+
direction: ('ltr' | 'rtl') | null;
475+
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
476+
indent: number;
477+
version: number;
478+
};
479+
[k: string]: unknown;
480+
} | null;
481+
updatedAt: string;
482+
createdAt: string;
483+
}
457484
/**
458485
* This interface was referenced by `Config`'s JSON-Schema
459486
* via the `definition` "select-versions-fields".
@@ -469,7 +496,7 @@ export interface SelectVersionsField {
469496
| null;
470497
blocks?:
471498
| {
472-
hasManyArr?: ('a' | 'b' | 'c')[] | null;
499+
hasManyBlocks?: ('a' | 'b' | 'c')[] | null;
473500
id?: string | null;
474501
blockName?: string | null;
475502
blockType: 'block';
@@ -1830,6 +1857,10 @@ export interface PayloadLockedDocument {
18301857
relationTo: 'LexicalInBlock';
18311858
value: string | LexicalInBlock;
18321859
} | null)
1860+
| ({
1861+
relationTo: 'lexical-access-control';
1862+
value: string | LexicalAccessControl;
1863+
} | null)
18331864
| ({
18341865
relationTo: 'select-versions-fields';
18351866
value: string | SelectVersionsField;
@@ -2104,6 +2135,16 @@ export interface LexicalInBlockSelect<T extends boolean = true> {
21042135
updatedAt?: T;
21052136
createdAt?: T;
21062137
}
2138+
/**
2139+
* This interface was referenced by `Config`'s JSON-Schema
2140+
* via the `definition` "lexical-access-control_select".
2141+
*/
2142+
export interface LexicalAccessControlSelect<T extends boolean = true> {
2143+
title?: T;
2144+
richText?: T;
2145+
updatedAt?: T;
2146+
createdAt?: T;
2147+
}
21072148
/**
21082149
* This interface was referenced by `Config`'s JSON-Schema
21092150
* via the `definition` "select-versions-fields_select".
@@ -2122,7 +2163,7 @@ export interface SelectVersionsFieldsSelect<T extends boolean = true> {
21222163
block?:
21232164
| T
21242165
| {
2125-
hasManyArr?: T;
2166+
hasManyBlocks?: T;
21262167
id?: T;
21272168
blockName?: T;
21282169
};

test/fields/seed.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,12 @@ export const seed = async (_payload: Payload) => {
495495
data: {
496496
text: 'text',
497497
},
498+
depth: 0,
498499
})
499500

500501
await _payload.create({
501502
collection: 'LexicalInBlock',
503+
depth: 0,
502504
data: {
503505
content: {
504506
root: {
@@ -537,24 +539,36 @@ export const seed = async (_payload: Payload) => {
537539
},
538540
})
539541

542+
await _payload.create({
543+
collection: 'lexical-access-control',
544+
data: {
545+
richText: textToLexicalJSON({ text: 'text' }),
546+
title: 'title',
547+
},
548+
depth: 0,
549+
})
550+
540551
await Promise.all([
541552
_payload.create({
542553
collection: customIDSlug,
543554
data: {
544555
id: nonStandardID,
545556
},
557+
depth: 0,
546558
}),
547559
_payload.create({
548560
collection: customTabIDSlug,
549561
data: {
550562
id: customTabID,
551563
},
564+
depth: 0,
552565
}),
553566
_payload.create({
554567
collection: customRowIDSlug,
555568
data: {
556569
id: customRowID,
557570
},
571+
depth: 0,
558572
}),
559573
])
560574
}

test/fields/slugs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const lexicalFieldsSlug = 'lexical-fields'
1717
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
1818
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
1919
export const lexicalRelationshipFieldsSlug = 'lexical-relationship-fields'
20+
21+
export const lexicalAccessControlSlug = 'lexical-access-control'
22+
2023
export const numberFieldsSlug = 'number-fields'
2124
export const pointFieldsSlug = 'point-fields'
2225
export const radioFieldsSlug = 'radio-fields'
@@ -52,6 +55,7 @@ export const collectionSlugs = [
5255
lexicalFieldsSlug,
5356
lexicalMigrateFieldsSlug,
5457
lexicalRelationshipFieldsSlug,
58+
lexicalAccessControlSlug,
5559
numberFieldsSlug,
5660
pointFieldsSlug,
5761
radioFieldsSlug,

0 commit comments

Comments
 (0)