Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: CodeSettingInput renders duplicate code text box #35273

Merged
merged 11 commits into from
Mar 21, 2025
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