Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/honest-toys-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes an issue where the code input type in settings renders duplicate code text boxes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { Editor, EditorFromTextArea } from 'codemirror';
import type { ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

const defaultGutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'];

Expand Down Expand Up @@ -44,77 +44,69 @@ function CodeMirror({
...props
}: CodeMirrorProps): ReactElement {
const [value, setValue] = useState(valueProp || defaultValue);

const textAreaRef = useRef<HTMLTextAreaElement>(null);
const editorRef = useRef<EditorFromTextArea | null>(null);
const handleChange = useEffectEvent(onChange);

useEffect(() => {
if (editorRef.current) {
return;
}

const setupCodeMirror = async (): Promise<void> => {
const { default: CodeMirror } = await import('codemirror');
await Promise.all([
import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'),
import('codemirror/addon/edit/matchbrackets'),
import('codemirror/addon/edit/closebrackets'),
import('codemirror/addon/edit/matchtags'),
import('codemirror/addon/edit/trailingspace'),
import('codemirror/addon/search/match-highlighter'),
import('codemirror/lib/codemirror.css'),
]);

if (!textAreaRef.current) {
return;
}

editorRef.current = CodeMirror.fromTextArea(textAreaRef.current, {
lineNumbers,
lineWrapping,
mode,
gutters,
foldGutter,
matchBrackets,
autoCloseBrackets,
matchTags,
showTrailingSpace,
highlightSelectionMatches,
readOnly,
});

editorRef.current.on('change', (doc: Editor) => {
const value = doc.getValue();
setValue(value);
handleChange(value);
});
};

setupCodeMirror();

return (): void => {
if (!editorRef.current) {
return;
const editorRef = useRef<EditorFromTextArea | null>(null);
const textAreaRef = useCallback(
async (node: HTMLTextAreaElement | null) => {
if (!node) return;

try {
const { default: CodeMirror } = await import('codemirror');
await Promise.all([
import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'),
import('codemirror/addon/edit/matchbrackets'),
import('codemirror/addon/edit/closebrackets'),
import('codemirror/addon/edit/matchtags'),
import('codemirror/addon/edit/trailingspace'),
import('codemirror/addon/search/match-highlighter'),
import('codemirror/lib/codemirror.css'),
]);

editorRef.current = CodeMirror.fromTextArea(node, {
lineNumbers,
lineWrapping,
mode,
gutters,
foldGutter,
matchBrackets,
autoCloseBrackets,
matchTags,
showTrailingSpace,
highlightSelectionMatches,
readOnly,
});

editorRef.current.on('change', (doc: Editor) => {
const newValue = doc.getValue();
setValue(newValue);
handleChange(newValue);
});

return () => {
if (node.parentNode) {
editorRef.current?.toTextArea();
}
};
} catch (error) {
console.error('CodeMirror initialization failed:', error);
}

editorRef.current.toTextArea();
};
}, [
autoCloseBrackets,
foldGutter,
gutters,
highlightSelectionMatches,
lineNumbers,
lineWrapping,
matchBrackets,
matchTags,
mode,
handleChange,
readOnly,
textAreaRef,
showTrailingSpace,
]);
},
[
autoCloseBrackets,
foldGutter,
gutters,
highlightSelectionMatches,
lineNumbers,
lineWrapping,
matchBrackets,
matchTags,
mode,
handleChange,
readOnly,
showTrailingSpace,
],
);

useEffect(() => {
setValue(valueProp);
Expand Down
26 changes: 22 additions & 4 deletions apps/meteor/tests/e2e/administration-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Users } from './fixtures/userStates';
import { Admin } from './page-objects';
import { getSettingValueById } from './utils';
import { getSettingValueById, setSettingValueById } from './utils';
import { test, expect } from './utils/test';

test.use({ storageState: Users.admin.state });
Expand Down Expand Up @@ -42,11 +42,29 @@ test.describe.parallel('administration-settings', () => {
await page.goto('/admin/settings/Layout');
});

test('should code mirror full screen be displayed correctly', async ({ page }) => {
test.afterAll(async ({ api }) => setSettingValueById(api, 'theme-custom-css', ''));

test('should display the code mirror correctly', async ({ page, api }) => {
await poAdmin.getAccordionBtnByName('Custom CSS').click();
await poAdmin.btnFullScreen.click();

await expect(page.getByRole('code')).toHaveCSS('width', '920px');
await test.step('should render only one code mirror element', async () => {
const codeMirrorParent = page.getByRole('code');
await expect(codeMirrorParent.locator('.CodeMirror')).toHaveCount(1);
});

await test.step('should display full screen properly', async () => {
await poAdmin.btnFullScreen.click();
await expect(page.getByRole('code')).toHaveCSS('width', '920px');
await poAdmin.btnExitFullScreen.click();
});

await test.step('should reflect updated value when valueProp changes after server update', async () => {
const codeValue = `.test-class-${Date.now()} { background-color: red; }`;
await setSettingValueById(api, 'theme-custom-css', codeValue);

const codeMirrorParent = page.getByRole('code');
await expect(codeMirrorParent.locator('.CodeMirror-line')).toHaveText(codeValue);
});
});
});
});
4 changes: 4 additions & 0 deletions apps/meteor/tests/e2e/page-objects/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ export class Admin {
return this.page.getByRole('button', { name: 'Full Screen', exact: true });
}

get btnExitFullScreen(): Locator {
return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true });
}

async dropdownFilterRoomType(text = 'All rooms'): Promise<Locator> {
return this.page.locator(`div[role="button"]:has-text("${text}")`);
}
Expand Down
Loading