Merge pull request #1168 from payloadcms/fix/read-only
Fix: read only field styles
This commit is contained in:
@@ -70,6 +70,7 @@ const RichText: React.FC<Props> = (props) => {
|
|||||||
const [enabledLeaves, setEnabledLeaves] = useState({});
|
const [enabledLeaves, setEnabledLeaves] = useState({});
|
||||||
const [initialValueKey, setInitialValueKey] = useState('');
|
const [initialValueKey, setInitialValueKey] = useState('');
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
const toolbarRef = useRef(null);
|
||||||
|
|
||||||
const renderElement = useCallback(({ attributes, children, element }) => {
|
const renderElement = useCallback(({ attributes, children, element }) => {
|
||||||
const matchedElement = enabledElements[element?.type];
|
const matchedElement = enabledElements[element?.type];
|
||||||
@@ -176,6 +177,31 @@ const RichText: React.FC<Props> = (props) => {
|
|||||||
setInitialValueKey(JSON.stringify(initialValue || ''));
|
setInitialValueKey(JSON.stringify(initialValue || ''));
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function setClickableState(clickState: 'disabled' | 'enabled') {
|
||||||
|
const selectors = 'button, a, [role="button"]';
|
||||||
|
const toolbarButtons: (HTMLButtonElement | HTMLAnchorElement)[] = toolbarRef.current.querySelectorAll(selectors);
|
||||||
|
const editorButtons: (HTMLButtonElement | HTMLAnchorElement)[] = editorRef.current.querySelectorAll(selectors);
|
||||||
|
|
||||||
|
[...(toolbarButtons || []), ...(editorButtons || [])].forEach((child) => {
|
||||||
|
const isButton = child.tagName === 'BUTTON';
|
||||||
|
const isDisabling = clickState === 'disabled';
|
||||||
|
child.setAttribute('tabIndex', isDisabling ? '-1' : '0');
|
||||||
|
if (isButton) child.setAttribute('disabled', isDisabling ? 'disabled' : null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded && readOnly) {
|
||||||
|
setClickableState('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (loaded && readOnly) {
|
||||||
|
setClickableState('enabled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loaded, readOnly]);
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -215,13 +241,16 @@ const RichText: React.FC<Props> = (props) => {
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
value={valueToRender as any[]}
|
value={valueToRender as any[]}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val !== defaultValue && val !== value) {
|
if (!readOnly && val !== defaultValue && val !== value) {
|
||||||
setValue(val);
|
setValue(val);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__wrapper`}>
|
<div className={`${baseClass}__wrapper`}>
|
||||||
<div className={`${baseClass}__toolbar`}>
|
<div
|
||||||
|
className={`${baseClass}__toolbar`}
|
||||||
|
ref={toolbarRef}
|
||||||
|
>
|
||||||
<div className={`${baseClass}__toolbar-wrap`}>
|
<div className={`${baseClass}__toolbar-wrap`}>
|
||||||
{elements.map((element, i) => {
|
{elements.map((element, i) => {
|
||||||
let elementName: string;
|
let elementName: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
.rich-text {
|
.rich-text {
|
||||||
margin-bottom: base(2);
|
margin-bottom: base(2);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
&__toolbar {
|
&__toolbar {
|
||||||
@include blur-bg(var(--theme-elevation-0));
|
@include blur-bg(var(--theme-elevation-0));
|
||||||
@@ -103,6 +104,29 @@
|
|||||||
.rich-text__editor {
|
.rich-text__editor {
|
||||||
background-color: var(--theme-elevation-150);
|
background-color: var(--theme-elevation-150);
|
||||||
padding: base(.5);
|
padding: base(.5);
|
||||||
|
|
||||||
|
.popup button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text__toolbar {
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--theme-elevation-150);
|
||||||
|
opacity: .85;
|
||||||
|
z-index: 2;
|
||||||
|
backdrop-filter: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ const Select: React.FC<Props> = (props) => {
|
|||||||
showError={showError}
|
showError={showError}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
required={required}
|
required={required}
|
||||||
|
readOnly={readOnly}
|
||||||
description={description}
|
description={description}
|
||||||
style={style}
|
style={style}
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@@ -73,12 +73,46 @@ const RichTextFields: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'richTextReadOnly',
|
||||||
|
type: 'richText',
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
link: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
label: 'Rel Attribute',
|
||||||
|
type: 'select',
|
||||||
|
hasMany: true,
|
||||||
|
options: [
|
||||||
|
'noopener', 'noreferrer', 'nofollow',
|
||||||
|
],
|
||||||
|
admin: {
|
||||||
|
description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
collections: {
|
||||||
|
uploads: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'caption',
|
||||||
|
type: 'richText',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const richTextDoc = {
|
function generateRichText() {
|
||||||
selectHasMany: ['one', 'five'],
|
return [
|
||||||
richText: [
|
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -220,7 +254,13 @@ export const richTextDoc = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
],
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const richTextDoc = {
|
||||||
|
selectHasMany: ['one', 'five'],
|
||||||
|
richText: generateRichText(),
|
||||||
|
richTextReadOnly: generateRichText(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RichTextFields;
|
export default RichTextFields;
|
||||||
|
|||||||
@@ -24,6 +24,27 @@ const SelectFields: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'selectReadOnly',
|
||||||
|
type: 'select',
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Value One',
|
||||||
|
value: 'one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value Two',
|
||||||
|
value: 'two',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value Three',
|
||||||
|
value: 'three',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'selectHasMany',
|
name: 'selectHasMany',
|
||||||
hasMany: true,
|
hasMany: true,
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export default buildConfig({
|
|||||||
|
|
||||||
const richTextUploadIndex = richTextDocWithRelationship.richText.findIndex(({ type }) => type === 'upload');
|
const richTextUploadIndex = richTextDocWithRelationship.richText.findIndex(({ type }) => type === 'upload');
|
||||||
richTextDocWithRelationship.richText[richTextUploadIndex].value = { id: createdUploadDoc.id };
|
richTextDocWithRelationship.richText[richTextUploadIndex].value = { id: createdUploadDoc.id };
|
||||||
|
richTextDocWithRelationship.richTextReadOnly[richTextUploadIndex].value = { id: createdUploadDoc.id };
|
||||||
|
|
||||||
await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship });
|
await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship });
|
||||||
|
|
||||||
|
|||||||
@@ -143,73 +143,84 @@ describe('fields', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('fields - richText', () => {
|
describe('fields - richText', () => {
|
||||||
test('should create new url link', async () => {
|
async function navigateToRichTextFields() {
|
||||||
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields');
|
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields');
|
||||||
await page.goto(url.list);
|
await page.goto(url.list);
|
||||||
await page.locator('.row-1 .cell-id').click();
|
await page.locator('.row-1 .cell-id').click();
|
||||||
|
}
|
||||||
|
|
||||||
// Open link popup
|
describe('toolbar', () => {
|
||||||
await page.locator('.rich-text__toolbar .link').click();
|
test('should create new url link', async () => {
|
||||||
|
await navigateToRichTextFields();
|
||||||
|
|
||||||
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
// Open link popup
|
||||||
await expect(editLinkModal).toBeVisible();
|
await page.locator('.rich-text__toolbar button:not([disabled]) .link').click();
|
||||||
|
|
||||||
// Fill values and click Confirm
|
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
||||||
await editLinkModal.locator('#field-text').fill('link text');
|
await expect(editLinkModal).toBeVisible();
|
||||||
await editLinkModal.locator('label[for="field-linkType-custom"]').click();
|
|
||||||
await editLinkModal.locator('#field-url').fill('https://payloadcms.com');
|
|
||||||
await wait(200);
|
|
||||||
await editLinkModal.locator('button[type="submit"]').click();
|
|
||||||
|
|
||||||
// Remove link
|
// Fill values and click Confirm
|
||||||
await page.locator('span >> text="link text"').click();
|
await editLinkModal.locator('#field-text').fill('link text');
|
||||||
const popup = page.locator('.popup--active .rich-text-link__popup');
|
await editLinkModal.locator('label[for="field-linkType-custom"]').click();
|
||||||
await expect(popup.locator('.rich-text-link__link-label')).toBeVisible();
|
await editLinkModal.locator('#field-url').fill('https://payloadcms.com');
|
||||||
await popup.locator('.rich-text-link__link-close').click();
|
await wait(200);
|
||||||
await expect(page.locator('span >> text="link text"')).toHaveCount(0);
|
await editLinkModal.locator('button[type="submit"]').click();
|
||||||
|
|
||||||
|
// Remove link from editor body
|
||||||
|
await page.locator('span >> text="link text"').click();
|
||||||
|
const popup = page.locator('.popup--active .rich-text-link__popup');
|
||||||
|
await expect(popup.locator('.rich-text-link__link-label')).toBeVisible();
|
||||||
|
await popup.locator('.rich-text-link__link-close').click();
|
||||||
|
await expect(page.locator('span >> text="link text"')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not create new url link when read only', async () => {
|
||||||
|
await navigateToRichTextFields();
|
||||||
|
|
||||||
|
// Attempt to open link popup
|
||||||
|
const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link');
|
||||||
|
await expect(modalTrigger).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should populate url link', async () => {
|
describe('editor', () => {
|
||||||
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields');
|
test('should populate url link', async () => {
|
||||||
await page.goto(url.list);
|
navigateToRichTextFields();
|
||||||
await page.locator('.row-1 .cell-id').click();
|
|
||||||
|
|
||||||
// Open link popup
|
// Open link popup
|
||||||
await page.locator('span >> text="render links"').click();
|
await page.locator('#field-richText span >> text="render links"').click();
|
||||||
const popup = page.locator('.popup--active .rich-text-link__popup');
|
const popup = page.locator('.popup--active .rich-text-link__popup');
|
||||||
await expect(popup).toBeVisible();
|
await expect(popup).toBeVisible();
|
||||||
await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com');
|
await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com');
|
||||||
|
|
||||||
// Open link edit modal
|
// Open link edit modal
|
||||||
await popup.locator('.rich-text-link__link-edit').click();
|
await popup.locator('.rich-text-link__link-edit').click();
|
||||||
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
||||||
await expect(editLinkModal).toBeVisible();
|
await expect(editLinkModal).toBeVisible();
|
||||||
|
|
||||||
// Close link edit modal
|
// Close link edit modal
|
||||||
await editLinkModal.locator('button[type="submit"]').click();
|
await editLinkModal.locator('button[type="submit"]').click();
|
||||||
await expect(editLinkModal).not.toBeVisible();
|
await expect(editLinkModal).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should populate relationship link', async () => {
|
test('should populate relationship link', async () => {
|
||||||
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields');
|
navigateToRichTextFields();
|
||||||
await page.goto(url.list);
|
|
||||||
await page.locator('.row-1 .cell-id').click();
|
|
||||||
|
|
||||||
// Open link popup
|
// Open link popup
|
||||||
await page.locator('span >> text="link to relationships"').click();
|
await page.locator('#field-richText span >> text="link to relationships"').click();
|
||||||
const popup = page.locator('.popup--active .rich-text-link__popup');
|
const popup = page.locator('.popup--active .rich-text-link__popup');
|
||||||
await expect(popup).toBeVisible();
|
await expect(popup).toBeVisible();
|
||||||
await expect(popup.locator('a')).toHaveAttribute('href', /\/admin\/collections\/array-fields\/.*/);
|
await expect(popup.locator('a')).toHaveAttribute('href', /\/admin\/collections\/array-fields\/.*/);
|
||||||
|
|
||||||
// Open link edit modal
|
// Open link edit modal
|
||||||
await popup.locator('.rich-text-link__link-edit').click();
|
await popup.locator('.rich-text-link__link-edit').click();
|
||||||
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
||||||
await expect(editLinkModal).toBeVisible();
|
await expect(editLinkModal).toBeVisible();
|
||||||
|
|
||||||
// Close link edit modal
|
// Close link edit modal
|
||||||
await editLinkModal.locator('button[type="submit"]').click();
|
await editLinkModal.locator('button[type="submit"]').click();
|
||||||
await expect(editLinkModal).not.toBeVisible();
|
await expect(editLinkModal).not.toBeVisible();
|
||||||
// await page.locator('span >> text="render links"').click();
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user