mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* admin: parsed JSONC settings editor with form view (#7603, #7666) Takes over #7666 / closes #7603. Squashed rebase of 32 commits onto current develop (which has since absorbed admin design rework #7716 and admin i18n fixes #7736 — granular history preserved on takeover/7666-admin-settings-editor before this squash, see PR description for the original commit log). Highlights: - New parsed JSONC settings editor under admin/src/components/settings/ — FormView, ModeToggle, ParseErrorBanner, JsoncNode dispatcher, leaf widgets (string, number, bool, null, env pill), and pure helpers (comments, envPill, jsoncEdit, labels, templateComments). - ${VAR:default} env placeholders render as editable inline inputs that round-trip through the raw textarea (env-pill spec asserts this; docker-template spec protects against form-view degradation on env-heavy configs). - Schema-driven help text sourced from settings.json.template, inlined at build time via vite (drops the runtime fs.allow widening that earlier iterations needed). - ModeToggle switches between FormView and raw textarea on /admin/settings; parse errors surface in a non-blocking banner. - jsonc-parser dep added; pure helpers wrap modify() for stable edits that preserve key order and trailing comments (stops at end-of-line so trailing-comment trains don't bleed into the next property). - i18n keys added for form mode, parse error, env pill, default_label, and input aria. - Playwright specs cover form view, env pill, parse error banner, raw round-trip, and form-mode regressions called out in #7666 review (stable React keys from AST offsets, save-toast on server ack only, NumberInput draft sync, parse-error flash during initial load, .settings CSS conflict resolution, focus retention via rAF, IconButton type defaulting to 'button'). Co-authored-by: Ayushi Gupta <ayushigupta36881@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): stabilise React keys to prevent focus loss in settings editor Switch React keys in JsoncNode and FormView from byte offsets to stable JSON paths (`getNodePath(...).join('.')`). Byte offsets shift on every keystroke because the edit changes the surrounding character count, which forces React to remount inputs and lose focus mid-typing. - Object children key on the property path. - Array elements key on their JSON path index. - Add a Playwright regression test pinning focus stability for array element edits. Co-authored-by: John McLear <john@mclear.co.uk> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): stop trailing /* */ comments from bleeding into next key's label (#7740) In the parsed settings form view, each key's row was rendering its label as the previous keys' source lines concatenated together. Root cause: findLeading() in admin/src/components/settings/comments.ts treated any line ending in `*/` as a comment continuation, so a JSON line like "altF9": true, /* focus on the File Menu and/or editbar */ was absorbed into the next sibling's leading comment block, and then each subsequent key picked up an even longer accumulation. - Tighten findLeading's isComment check to only match structural comment lines (`//`, `/*`, or a `*`-prefixed continuation/close), so JSON code with a trailing block comment no longer matches. - Surface leading and trailing comments separately from the template map. Leaf rows with only a trailing same-line comment now render the humanized key as the row label and the comment as the help text below the control, matching settings.json.template's convention (and #7740's recommendation that "helper text should be below"). - Add unit tests pinning the regression and the JSDoc/`//` leading styles, plus a Playwright spec that asserts altC's row carries a clean label and the "focus on the Chat window" help text. Closes #7740. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ayushi Gupta <ayushigupta36881@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d537f3deb
commit
ab60ed33c1
31 changed files with 3554 additions and 128 deletions
1353
docs/superpowers/plans/2026-05-09-admin-settings-parsed-view.md
Normal file
1353
docs/superpowers/plans/2026-05-09-admin-settings-parsed-view.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,237 @@
|
|||
# Admin /settings parsed view — design
|
||||
|
||||
Date: 2026-05-09
|
||||
Status: draft, awaiting review
|
||||
Related: closes #7603, supersedes parts of #7666 / #7709
|
||||
|
||||
## Problem
|
||||
|
||||
`/admin/settings` renders `settings.json` as a single textarea. Admins
|
||||
asked (#7603) for a parsed view that surfaces each key with the JSONC
|
||||
comment that documents it, so the file stops looking like an opaque blob.
|
||||
|
||||
The current PR (#7709) only restyles the textarea and adds Validate /
|
||||
Prettify / Restart buttons. The actual "parsed per-key with inline
|
||||
comments" feature is still missing.
|
||||
|
||||
## Goals
|
||||
|
||||
- Render `settings.json` as a tree of typed widgets, one per key.
|
||||
- Show each key's leading `/* */` or `//` comment as inline help text.
|
||||
- Round-trip back to `settings.json` preserving comments, key order,
|
||||
whitespace, and `${ENV:default}` placeholders byte-identically for
|
||||
keys the admin did not edit.
|
||||
- Keep a raw textarea fallback for power users and structural edits.
|
||||
|
||||
## Non-goals (deferred)
|
||||
|
||||
- Adding or removing keys from the form view.
|
||||
- Editing `${ENV:default}` placeholders inline. They render as
|
||||
read-only pills; structural changes go through raw mode.
|
||||
- Curated, schema-driven help text. Comments in `settings.json` are
|
||||
the help text; we don't ship a parallel schema.
|
||||
- Search / filter / collapse-all controls.
|
||||
|
||||
## Constraints
|
||||
|
||||
- `settings.json` is JSONC: `/* */` and `//` comments, trailing commas
|
||||
tolerated by the loader.
|
||||
- Values can contain `${VAR:default}` placeholders that the server
|
||||
resolves at boot. The form must not rewrite or normalise these.
|
||||
- Server already ships the raw file text down `settingsSocket` and
|
||||
accepts a full-text `saveSettings` event back. We don't change the
|
||||
server contract.
|
||||
|
||||
## Architecture
|
||||
|
||||
All parsing and editing run in the browser. The server-side path is
|
||||
unchanged.
|
||||
|
||||
```
|
||||
load server reads settings.json --raw text--> settingsSocket --> store.settings
|
||||
edit widget --modify(text, path, value)--> new text --> store.settings
|
||||
save store.settings --emit('saveSettings', text)--> server writes file
|
||||
```
|
||||
|
||||
The store keeps a single source of truth: the file text. The AST is
|
||||
re-parsed from the text on each render of the form view; we don't keep
|
||||
a separate model and try to keep them in sync.
|
||||
|
||||
### Library
|
||||
|
||||
Add `jsonc-parser` (MS, MIT) to `admin/`. We use four entry points:
|
||||
|
||||
- `parseTree(text)` → AST `Node` with `{ type, offset, length, children, value }`.
|
||||
- `getNodePath(node)` → `(string|number)[]` JSON pointer-ish path.
|
||||
- `modify(text, path, value, options)` → array of `Edit { offset, length, content }`.
|
||||
- `applyEdits(text, edits)` → new text.
|
||||
|
||||
Comments are recoverable from the source `text` using `node.offset`;
|
||||
`parseTree` skips them but their byte ranges are deterministic.
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
SettingsPage
|
||||
├── ModeToggle Form | Raw (segmented control)
|
||||
│
|
||||
├── FormView visible when mode === 'form'
|
||||
│ ├── ParseErrorBanner shown when parseTree fails
|
||||
│ └── JsoncNode (recursive) one per AST node
|
||||
│ ├── CommentLabel leading /* */ or // text
|
||||
│ ├── KeyLabel
|
||||
│ ├── ValueWidget dispatches on node.type
|
||||
│ │ ├── StringInput
|
||||
│ │ ├── NumberInput
|
||||
│ │ ├── BooleanToggle
|
||||
│ │ ├── NullChip
|
||||
│ │ ├── EnvPill read-only, when raw matches ${VAR:default}
|
||||
│ │ ├── ObjectGroup collapsible, recurses
|
||||
│ │ └── ArrayGroup collapsible, recurses
|
||||
│ └── TrailingCommentBadge `// inline` after the value
|
||||
│
|
||||
├── RawView visible when mode === 'raw'
|
||||
│ └── <textarea className="settings"> (the existing editor)
|
||||
│
|
||||
└── ButtonBar
|
||||
├── Save
|
||||
├── Validate dry-run JSON parse, toast result
|
||||
└── Restart unchanged, keeps data-testid
|
||||
```
|
||||
|
||||
`exposeExperimental` (Prettify) stays gated off, as in #7709.
|
||||
|
||||
### Comment binding
|
||||
|
||||
For each property node `"key": value`:
|
||||
|
||||
- "Leading comment" = the longest run of `/* */` and `//` lines whose
|
||||
byte range ends at the line break immediately before `node.offset`,
|
||||
with at most one blank line allowed between the comment block and
|
||||
the key. Rendered as the help text under `KeyLabel`.
|
||||
- "Trailing comment" = a single `//` or `/* */` on the same line as
|
||||
the value, after the trailing comma if any. Rendered as a small
|
||||
badge with tooltip.
|
||||
- Comments not adjacent to a key (file header, orphan blocks) render
|
||||
in raw mode only.
|
||||
|
||||
### Env placeholder detection
|
||||
|
||||
A string node whose raw text slice (`text.slice(offset, offset+length)`)
|
||||
matches `/^"\$\{[^}]+\}"$/` renders as `EnvPill` instead of
|
||||
`StringInput`. The pill shows the variable name and (if present) the
|
||||
default after `:`. It is read-only; tooltip explains "edit in raw mode".
|
||||
|
||||
## Data flow
|
||||
|
||||
1. `settingsSocket` emits the raw file text. Store sets
|
||||
`state.settings = text` (this already happens today).
|
||||
2. `FormView` calls `parseTree(state.settings)`. On any thrown
|
||||
`SyntaxError` it renders `ParseErrorBanner` and a "Switch to raw to
|
||||
fix" button instead of the tree.
|
||||
3. Each leaf widget receives `(node, path)` and an `onChange(value)`
|
||||
callback. `onChange` runs:
|
||||
```ts
|
||||
const edits = modify(state.settings, path, value, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true }
|
||||
});
|
||||
useStore.getState().setSettings(applyEdits(state.settings, edits));
|
||||
```
|
||||
4. Re-render uses the new text. Successive edits stack naturally.
|
||||
5. Save: `isJSONClean(text)` → `socket.emit('saveSettings', text)`.
|
||||
6. Mode toggle does not touch `state.settings`; both views share it.
|
||||
|
||||
### Why no AST round-trip
|
||||
|
||||
We tried (in design) the alternative "parse → model → reserialize" and
|
||||
rejected it: even with `jsonc-parser`'s `Edit` API at the bottom, an
|
||||
intermediate model loses information about whitespace runs and
|
||||
comment whitespace prefixes. Patching the original text with
|
||||
`modify()` is the only path that gives byte-identical output for
|
||||
untouched regions, which is the explicit requirement (admins watch
|
||||
`settings.json` in git).
|
||||
|
||||
## Save semantics
|
||||
|
||||
- Form mode: each widget edit produces a `modify()` patch against the
|
||||
current text. Untouched bytes — including comments, ordering,
|
||||
trailing commas, env placeholders — are preserved.
|
||||
- Raw mode: textarea writes the whole text back wholesale. New keys
|
||||
and structural reshuffles only happen here.
|
||||
- The save button itself is mode-agnostic: it sends `state.settings`
|
||||
as it stands. The existing `isJSONClean` validation gates it.
|
||||
- Toggling Form → Raw is always safe: text is unchanged.
|
||||
- Toggling Raw → Form may surface a parse error; banner explains and
|
||||
offers to switch back.
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | UX |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Server hasn't sent settings yet | Spinner (existing behavior preserved) |
|
||||
| Socket disconnected during save | Failure toast `admin_settings.toast.disconnected` |
|
||||
| Invalid JSON at save time | Failure toast `admin_settings.toast.json_invalid`, save blocked |
|
||||
| Invalid JSON when toggling Raw → Form | `ParseErrorBanner` with line/col, "Switch back to Raw" button |
|
||||
| `modify()` returns no edits (no-op) | Treat as successful; widget value reflects current text |
|
||||
| Number widget non-finite/non-numeric | Field-level inline error; file text is *not* updated until input parses; save uses last-valid value |
|
||||
|
||||
## i18n
|
||||
|
||||
All new strings go through `react-i18next`. New keys:
|
||||
|
||||
```
|
||||
admin_settings.mode.form "Form"
|
||||
admin_settings.mode.raw "Raw"
|
||||
admin_settings.parse_error.title "Cannot parse settings.json"
|
||||
admin_settings.parse_error.cta "Switch to raw to edit"
|
||||
admin_settings.env_pill.tooltip "Environment variable. Edit in raw mode."
|
||||
admin_settings.add_key.disabled "Add new keys in raw mode"
|
||||
```
|
||||
|
||||
Plus the toast keys already added in #7709.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Mode toggle is a `role=tablist` with arrow-key navigation.
|
||||
- Each form group has an `aria-labelledby` pointing at its key label.
|
||||
- `EnvPill` is `role=note` with `aria-label="environment variable …"`.
|
||||
- Collapsible groups use `<details>`/`<summary>` so keyboard and
|
||||
screen-reader behavior is native.
|
||||
|
||||
## Test plan (Playwright, `admin-spec/adminsettings.spec.ts`)
|
||||
|
||||
Add specs:
|
||||
|
||||
- `form view renders comment as help text` — assert that a `dbType`
|
||||
row's `aria-describedby` resolves to text containing the leading
|
||||
comment in `settings.json`.
|
||||
- `editing string preserves comments` — change `title` via the form
|
||||
input, save, reload; assert raw text contains both the new title and
|
||||
the original comment block above it.
|
||||
- `boolean toggle round-trips` — toggle `requireAuthentication`, save,
|
||||
reload; assert raw text shows `true`/`false` literal and surrounding
|
||||
comments unchanged.
|
||||
- `env pill is read-only` — locate the SSO `issuer` row, assert pill
|
||||
is rendered, assert no `<input>` accepts text in form mode.
|
||||
- `raw toggle round-trip is lossless` — Form → Raw → Form returns
|
||||
identical bytes when nothing was edited.
|
||||
- `invalid raw JSON shows banner on toggle to form` — paste broken
|
||||
JSON in raw, toggle to form, assert `ParseErrorBanner`, assert
|
||||
"Switch to raw" button works.
|
||||
- Existing `comments preserved after save round-trip`,
|
||||
`validate button toasts`, `restart works` specs continue to pass
|
||||
through the new helper testids.
|
||||
|
||||
## Out of scope (future PRs)
|
||||
|
||||
- Add-key / delete-key form UI.
|
||||
- Env-var inline editing widget.
|
||||
- Schema-driven help text overlay.
|
||||
- Sectioning / search / filter.
|
||||
|
||||
## Rollout
|
||||
|
||||
Single PR replacing the current `SettingsPage.tsx` from #7709. The
|
||||
takeover branch (`takeover/7666-admin-settings-editor` on
|
||||
johnmclear/etherpad-lite) gets new commits on top; #7709 stays open
|
||||
and gets force-updated. No server-side migration.
|
||||
Loading…
Add table
Add a link
Reference in a new issue