Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
04c44ba
admin: improve settings editor UX and restore comments (#7603)
AyushiGupta160604 May 9, 2026
b5d81b5
admin(settings): address review of #7666 — types, focus, i18n, tests
JohnMcLear May 9, 2026
b0c668b
docs: design spec for admin /settings parsed view
JohnMcLear May 9, 2026
f46864c
docs: implementation plan for admin /settings parsed view
JohnMcLear May 9, 2026
5700e93
admin(settings): add jsonc-parser dep
JohnMcLear May 9, 2026
d358c12
admin(settings): pure helpers for env pills and comment extraction
JohnMcLear May 9, 2026
15c461e
admin(settings): editJsonc wrapper around jsonc-parser modify
JohnMcLear May 9, 2026
e5ea0c3
admin(settings): leaf widgets (string, number, bool, null, env pill)
JohnMcLear May 9, 2026
0345b26
admin(settings): group widgets, JsoncNode dispatcher, CommentLabel
JohnMcLear May 9, 2026
36240f0
admin(settings): FormView, ParseErrorBanner, ModeToggle
JohnMcLear May 9, 2026
778754f
admin(settings): toggle FormView and raw textarea from SettingsPage
JohnMcLear May 9, 2026
313b134
admin(settings): styles for tree, widgets, env pill, parse error
JohnMcLear May 9, 2026
442aab4
admin(settings): i18n keys for form mode, parse error, env pill
JohnMcLear May 9, 2026
4d61291
admin(settings): playwright specs for form view, env pill, parse erro…
JohnMcLear May 9, 2026
74b312c
admin(settings): don't read trailing comments past end-of-line
JohnMcLear May 9, 2026
0e98161
admin(settings): schema-driven help text from settings.json.template
JohnMcLear May 9, 2026
586d20c
admin(settings): redesign form view as light two-column form
JohnMcLear May 9, 2026
81f7407
admin(settings): update specs for label/help split + template fallback
JohnMcLear May 9, 2026
5bcd3e3
fix(#2): use stable React keys derived from AST node offsets/property…
JohnMcLear May 9, 2026
8f94f71
fix(#3): read latest settings text from store in onEdit to prevent st…
JohnMcLear May 9, 2026
3971e68
fix(#6): resolve .settings CSS conflict between App.css and index.css
JohnMcLear May 9, 2026
e6e92ab
fix(#8): suppress parse-error flash while settings are still loading
JohnMcLear May 9, 2026
4bbac8b
fix(#9): sync NumberInput draft state with value prop when not focused
JohnMcLear May 9, 2026
a57fd2e
docs(#10): aria-describedby already resolved during form redesign
JohnMcLear May 9, 2026
2a76a20
docs(#11): add comment explaining handleKeyDown Tab/rAF behaviour
JohnMcLear May 9, 2026
c1d360b
fix(#12): show save toast on server ack only, not optimistically
JohnMcLear May 9, 2026
05ad93f
fix(#13): strengthen saveSettings helper to wait for genuine new toas…
JohnMcLear May 9, 2026
4dd16c2
fix(#14): internationalise hard-coded UI strings in FormView and Mode…
JohnMcLear May 9, 2026
237200d
fix(#15): default IconButton type to 'button' to prevent accidental f…
JohnMcLear May 9, 2026
bd84291
fix(#7): change protocol-relative GitHub wiki links to explicit https://
JohnMcLear May 9, 2026
bf4c250
admin(settings): inline template at build time, drop fs.allow widening
JohnMcLear May 9, 2026
41dece9
Merge remote-tracking branch 'origin/develop' into takeover/7666-admi…
JohnMcLear May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-switch": "^1.2.6",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-query-devtools": "^5.100.9",
"jsonc-parser": "^3.3.1",
"openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4"
},
Expand Down
275 changes: 275 additions & 0 deletions admin/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/* Raw textarea (kept dark to signal "this is code") */
textarea.settings {
font-family: "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
font-size: 14px;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
width: 100%;
height: 500px;
padding: 15px;
background-color: #1e1e1e;
color: #d4d4d4;
line-height: 1.5;
border: 1px solid #333;
resize: vertical;
}
textarea.settings:focus {
outline: 2px solid #007acc;
outline-offset: -1px;
}

.settings-button-bar {
display: flex;
flex-shrink: 0;
gap: 10px;
margin-top: 15px;
}

.settings-links {
display: flex;
gap: 20px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
}

/* --- mode toggle --- */
.settings-mode-toggle {
display: inline-flex;
flex-shrink: 0;
border: 1px solid #ccc;
border-radius: 6px;
overflow: hidden;
margin-bottom: 16px;
background: #fff;
}
.settings-mode-toggle button {
padding: 6px 14px;
border: 0;
background: transparent;
color: #555;
font: inherit;
cursor: pointer;
}
.settings-mode-toggle button.active {
background: var(--etherpad-color, #0f775b);
color: #fff;
}

/* --- form (light, two-column) --- */
.settings-form {
font-family: inherit;
font-size: 14px;
color: #333;
}

.settings-section {
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 18px;
overflow: hidden;
}
.settings-section-header {
padding: 14px 18px;
border-bottom: 1px solid #eee;
background: #fafafa;
}
.settings-section-header h2 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #222;
letter-spacing: 0.01em;
text-transform: uppercase;
}
.settings-section-header p {
margin: 4px 0 0;
font-size: 13px;
color: #666;
white-space: pre-wrap;
}
.settings-section-body {
padding: 4px 0;
}

/* Two-column row: label | control, with help below spanning column 2.
* Single-column on narrow widths. */
.settings-row {
display: grid;
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
gap: 6px 18px;
padding: 10px 18px;
align-items: center;
border-top: 1px solid #f4f4f4;
}
.settings-row:first-child {
border-top: 0;
}
.settings-row-label {
font-weight: 600;
color: #333;
word-break: break-word;
}
.settings-row-control {
min-width: 0;
}
.settings-row-help {
grid-column: 2;
margin: 2px 0 0;
font-size: 12.5px;
color: #666;
white-space: pre-wrap;
line-height: 1.4;
}

@media (max-width: 600px) {
.settings-row {
grid-template-columns: 1fr;
}
.settings-row-help {
grid-column: 1;
}
}

/* --- nested subsections (objects/arrays inside a section) --- */
.settings-subsection {
grid-column: 1 / -1;
margin: 8px 18px;
border-left: 3px solid #e2e2e2;
padding-left: 14px;
}
.settings-subsection-header {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 0;
}
.settings-subsection-title {
font-weight: 600;
color: #444;
font-size: 13.5px;
text-transform: uppercase;
letter-spacing: 0.01em;
}
.settings-subsection-help {
color: #777;
font-size: 12.5px;
white-space: pre-wrap;
}
.settings-subsection-body .settings-row {
padding-left: 0;
padding-right: 0;
}

/* --- leaf widgets (light) --- */
.settings-widget-string,
.settings-widget-number {
width: 100%;
background: #fff;
color: #222;
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px 10px;
font-family: inherit;
font-size: inherit;
}
.settings-widget-string:focus,
.settings-widget-number:focus {
outline: none;
border-color: var(--etherpad-color, #0f775b);
box-shadow: 0 0 0 3px rgba(15, 119, 91, 0.15);
}
.settings-widget-number.invalid {
border-color: #ce5050;
}
.settings-widget-null {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
background: #f0f0f0;
color: #888;
font-style: italic;
font-size: 12.5px;
}
.settings-widget-env {
display: inline-flex;
align-items: center;
gap: 6px;
background: #f4f8ff;
color: #335;
border: 1px dashed #88a;
border-radius: 12px;
padding: 2px 10px;
font-size: 13px;
cursor: help;
}
.settings-widget-env-icon {
font-style: normal;
color: #557;
}
.settings-widget-env-name {
font-family: "Fira Code", monospace;
font-weight: 600;
}
.settings-widget-env code {
background: transparent;
color: #804;
font-family: "Fira Code", monospace;
}

/* Radix switch (boolean) */
.settings-widget-boolean {
appearance: none;
width: 36px;
height: 20px;
border-radius: 999px;
background: #ccc;
border: 0;
position: relative;
cursor: pointer;
transition: background 120ms ease;
padding: 0;
}
.settings-widget-boolean[data-state="checked"] {
background: var(--etherpad-color, #0f775b);
}
.settings-widget-boolean-thumb {
display: block;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transform: translateX(2px);
transition: transform 120ms ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.settings-widget-boolean[data-state="checked"] .settings-widget-boolean-thumb {
transform: translateX(18px);
}

/* --- parse error --- */
.settings-parse-error {
border: 1px solid #d99;
background: #fff5f5;
color: #842;
padding: 14px 18px;
border-radius: 6px;
}
.settings-parse-error-detail {
margin: 8px 0;
white-space: pre-wrap;
font-family: "Fira Code", monospace;
font-size: 12.5px;
}
.settings-parse-error button {
margin-top: 4px;
background: var(--etherpad-color, #0f775b);
color: #fff;
border: 0;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
font: inherit;
}
10 changes: 8 additions & 2 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,14 @@ export const App = () => {
useStore.getState().setShowLoading(false);
});

settingSocket.on('saveprogress', (status) => {
console.log(status)
settingSocket.on('saveprogress', (status: string, payload?: {message?: string}) => {
const {setToastState} = useStore.getState();
if (status === 'saved') {
setToastState({open: true, title: t('admin_settings.toast.saved'), success: true});
} else {
const detail = payload?.message ?? '';
setToastState({open: true, title: t('admin_settings.toast.save_failed') + (detail ? ` (${detail})` : ''), success: false});
}
})

return () => {
Expand Down
15 changes: 6 additions & 9 deletions admin/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import {FC, JSX, ReactElement} from "react";
import {ButtonHTMLAttributes, FC, JSX, ReactElement} from "react";

export type IconButtonProps = {
style?: React.CSSProperties,
export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title' | 'onClick'> & {
icon: JSX.Element,
title: string|ReactElement,
onClick: ()=>void,
className?: string,
disabled?: boolean
}

export const IconButton:FC<IconButtonProps> = ({icon,className,onClick,title, disabled, style})=>{
return <button style={style} onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
export const IconButton: FC<IconButtonProps> = ({icon, className, onClick, title, type = 'button', ...rest}) => (
<button {...rest} type={type} onClick={onClick} className={"icon-button " + (className ?? "")}>
{icon}
<span>{title}</span>
</button>
}
</button>
);
Loading
Loading