-
-
Notifications
You must be signed in to change notification settings - Fork 3k
feat: Improve Admin Settings UX and restore inline comments (#7603) #7666
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /* Enhanced Admin Settings Editor Styles */ | ||
| .settings { | ||
| font-family: "Fira Code", "Cascadia Code", "Source Code Pro", monospace; | ||
| font-size: 14px; | ||
| white-space: pre; | ||
| overflow-wrap: normal; | ||
| overflow-x: auto; | ||
| } | ||
|
|
||
| /* VS Code style focus ring requested by John */ | ||
| .settings:focus { | ||
| outline: 2px solid #007acc !important; | ||
| outline-offset: -1px; | ||
| } | ||
|
|
||
| .settings-button-bar { | ||
| display: flex; | ||
| gap: 10px; | ||
| margin-top: 15px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,50 +1,160 @@ | ||
| import {useStore} from "../store/store.ts"; | ||
| import {isJSONClean, cleanComments} from "../utils/utils.ts"; | ||
| import {Trans} from "react-i18next"; | ||
| import {IconButton} from "../components/IconButton.tsx"; | ||
| import {RotateCw, Save} from "lucide-react"; | ||
|
|
||
| export const SettingsPage = ()=>{ | ||
| const settingsSocket = useStore(state=>state.settingsSocket) | ||
| const settings = cleanComments(useStore(state=>state.settings)) | ||
|
|
||
| return <div className="settings-page"> | ||
| <h1><Trans i18nKey="admin_settings.current"/></h1> | ||
| <textarea value={settings} className="settings" onChange={v => { | ||
| useStore.getState().setSettings(v.target.value) | ||
| }}/> | ||
| <div className="settings-button-bar"> | ||
| <IconButton className="settingsButton" icon={<Save/>} | ||
| title={<Trans i18nKey="admin_settings.current_save.value"/>} onClick={() => { | ||
| if (isJSONClean(settings!)) { | ||
| // JSON is clean so emit it to the server | ||
| settingsSocket!.emit('saveSettings', settings!); | ||
| useStore.getState().setToastState({ | ||
| open: true, | ||
| title: "Successfully saved settings", | ||
| success: true | ||
| }) | ||
| } else { | ||
| useStore.getState().setToastState({ | ||
| open: true, | ||
| title: "Error saving settings", | ||
| success: false | ||
| }) | ||
| } | ||
| }}/> | ||
| <IconButton className="settingsButton" icon={<RotateCw/>} | ||
| title={<Trans i18nKey="admin_settings.current_restart.value"/>} onClick={() => { | ||
| settingsSocket!.emit('restartServer'); | ||
| }}/> | ||
| </div> | ||
| <div className="separator"/> | ||
| <div className="settings-button-bar"> | ||
| <a rel="noopener noreferrer" target="_blank" | ||
| href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans | ||
| i18nKey="admin_settings.current_example-prod"/></a> | ||
| <a rel="noopener noreferrer" target="_blank" | ||
| href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans | ||
| i18nKey="admin_settings.current_example-devel"/></a> | ||
| import React, { useState } from 'react'; | ||
| import { useStore } from "../store/store.ts"; | ||
| import { isJSONClean, cleanComments } from "../utils/utils.ts"; | ||
| import { Trans } from "react-i18next"; | ||
| import { IconButton } from "../components/IconButton.tsx"; | ||
| import { RotateCw, Save, AlignLeft, ShieldCheck } from "lucide-react"; | ||
|
|
||
| export const SettingsPage = () => { | ||
| const settingsSocket = useStore(state => state.settingsSocket); | ||
|
|
||
| // FIX: Initialize with empty string to prevent uncontrolled->controlled warning | ||
| const settings = useStore(state => state.settings) ?? ""; | ||
|
|
||
| // FIX: New features disabled by default per project maintenance rules | ||
| const [exposeExperimental] = useState(false); | ||
|
|
||
| const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
| if (e.key === 'Tab') { | ||
| e.preventDefault(); | ||
| const { selectionStart, selectionEnd, value } = e.currentTarget; | ||
| const newValue = value.substring(0, selectionStart) + " " + value.substring(selectionEnd); | ||
| useStore.getState().setSettings(newValue); | ||
|
|
||
| // Maintain cursor position after state update | ||
| setTimeout(() => { | ||
| e.currentTarget.selectionStart = e.currentTarget.selectionEnd = selectionStart + 2; | ||
| }, 0); | ||
| } | ||
| }; | ||
|
|
||
| // Dry-run validation without saving | ||
| const testJSON = () => { | ||
| try { | ||
| const cleaned = cleanComments(settings); | ||
| JSON.parse(cleaned ?? ""); | ||
| useStore.getState().setToastState({ | ||
| open: true, | ||
| title: "Validation Success: JSON is structurally sound.", | ||
| success: true | ||
| }); | ||
| } catch (e) { | ||
| useStore.getState().setToastState({ | ||
| open: true, | ||
| title: "Validation Failed: Check for syntax errors or stray characters.", | ||
| success: false | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| const prettifyJSON = () => { | ||
| try { | ||
| const cleaned = cleanComments(settings); | ||
| const obj = JSON.parse(cleaned ?? ""); | ||
| const formatted = JSON.stringify(obj, null, 2); | ||
|
|
||
| if (window.confirm("Prettifying will remove all comments. Do you wish to proceed?")) { | ||
| useStore.getState().setSettings(formatted); | ||
| } | ||
| } catch (e) { | ||
| useStore.getState().setToastState({ | ||
| open: true, | ||
| title: "Cannot prettify: Please fix syntax errors first.", | ||
| success: false | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="settings-page"> | ||
| <h1><Trans i18nKey="admin_settings.current" /></h1> | ||
|
|
||
| <textarea | ||
| value={settings} | ||
| className="settings" | ||
| spellCheck={false} | ||
| onKeyDown={handleKeyDown} | ||
| onChange={v => useStore.getState().setSettings(v.target.value)} | ||
| style={{ | ||
| fontFamily: '"Fira Code", "Cascadia Code", monospace', | ||
| width: '100%', | ||
| height: '500px', | ||
| padding: '15px', | ||
| backgroundColor: '#1e1e1e', | ||
| color: '#d4d4d4', | ||
| lineHeight: '1.5', | ||
| border: '1px solid #333', | ||
| resize: 'vertical' | ||
| }} | ||
| /> | ||
|
Comment on lines
+72
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. No inline comment mapping The settings UI does not extract and associate settings.json comments with their related settings as inline annotations; it only shows a raw text blob. This does not meet the requirement to surface comments in a setting-associated way. Agent Prompt
|
||
|
|
||
| <div className="settings-button-bar"> | ||
| <IconButton | ||
| className="settingsButton" | ||
| icon={<Save />} | ||
| title={<Trans i18nKey="admin_settings.current_save.value" />} | ||
| onClick={() => { | ||
| // FIX: Separate validation logic from socket logic | ||
| if (!isJSONClean(settings)) { | ||
| useStore.getState().setToastState({ | ||
| open: true, title: "Syntax Error: Check commas and braces.", success: false | ||
| }); | ||
| return; | ||
| } | ||
| if (!settingsSocket?.connected) { | ||
| useStore.getState().setToastState({ | ||
| open: true, title: "Error: Not connected to server.", success: false | ||
| }); | ||
| return; | ||
| } | ||
| settingsSocket.emit('saveSettings', settings); | ||
| useStore.getState().setToastState({ | ||
| open: true, title: "Settings saved successfully.", success: true | ||
| }); | ||
| }} | ||
| /> | ||
|
|
||
| {/* Dry-run Button */} | ||
| <IconButton | ||
| className="settingsButton" | ||
| icon={<ShieldCheck />} | ||
| title="Test JSON (Dry-run)" | ||
| onClick={testJSON} | ||
| /> | ||
|
|
||
| {/* FIX: Feature Flag Gating */} | ||
| {exposeExperimental && ( | ||
| <IconButton | ||
| className="settingsButton" | ||
| icon={<AlignLeft />} | ||
| title="Prettify JSON" | ||
| onClick={prettifyJSON} | ||
| /> | ||
| )} | ||
|
|
||
| <IconButton | ||
| className="settingsButton" | ||
| icon={<RotateCw />} | ||
| // FIX: Stable ID for Playwright automation | ||
| data-testid="restart-etherpad-button" | ||
| title={<Trans i18nKey="admin_settings.current_restart.value" />} | ||
| onClick={() => { | ||
| settingsSocket?.emit('restartServer'); | ||
| }} | ||
| /> | ||
|
Comment on lines
+135
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Invalid iconbutton testid prop SettingsPage passes data-testid to IconButton, but IconButtonProps does not include it and IconButton does not forward unknown props to the underlying <button>, causing a TS build failure and preventing the attribute from being rendered. This also blocks the intended “stable selector” for E2E tests. Agent Prompt
|
||
| </div> | ||
|
|
||
| <div className="separator" style={{ margin: '20px 0', borderBottom: '1px solid #eee' }} /> | ||
|
|
||
| <div className="settings-links" style={{ display: 'flex', gap: '20px' }}> | ||
| {/* FIX: Protocol-independent URLs */} | ||
| <a rel="noopener noreferrer" target="_blank" href="//github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"> | ||
| <Trans i18nKey="admin_settings.current_example-prod" /> | ||
| </a> | ||
| <a rel="noopener noreferrer" target="_blank" href="//github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"> | ||
| <Trans i18nKey="admin_settings.current_example-devel" /> | ||
| </a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| } | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Settings textarea not pretty-printed
📎 Requirement gap≡ CorrectnessAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools