mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 00:57:55 +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>
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
// admin/src/components/settings/__tests__/comments.test.ts
|
|
//
|
|
// Regression coverage for https://github.com/ether/etherpad/issues/7740.
|
|
// A previous version of findLeading treated any line ending in `*/` as a
|
|
// comment continuation; a JSON line like
|
|
// "altF9": true, /* focus on the File Menu and/or editbar */
|
|
// then leaked into the next sibling's "leading comment", which the form
|
|
// view rendered as the row label.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { extractAdjacentComments } from '../comments.ts';
|
|
import { humanize, labelAndHelp } from '../labels.ts';
|
|
|
|
const padShortcutText = `{
|
|
"padShortcutEnabled" : {
|
|
"altF9": true, /* focus on the File Menu and/or editbar */
|
|
"altC": true, /* focus on the Chat window */
|
|
"cmdShift2": true, /* shows a gritter popup showing a line author */
|
|
"delete": true,
|
|
"return": true,
|
|
"esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */
|
|
"cmdS": true /* save a revision */
|
|
}
|
|
}`;
|
|
|
|
const offsetsFor = (text: string, key: string) => {
|
|
const keyOffset = text.indexOf(`"${key}"`);
|
|
const valOffset = text.indexOf('true', keyOffset);
|
|
return { keyOffset, valOffset, valLength: 4 };
|
|
};
|
|
|
|
test('does not absorb prior JSON line with trailing comment as leading', () => {
|
|
const { keyOffset, valOffset, valLength } = offsetsFor(padShortcutText, 'altC');
|
|
const { leading, trailing } =
|
|
extractAdjacentComments(padShortcutText, keyOffset, valOffset, valLength);
|
|
assert.equal(leading, '');
|
|
assert.equal(trailing, 'focus on the Chat window');
|
|
});
|
|
|
|
test('does not accumulate multiple prior trailing-comment lines', () => {
|
|
const { keyOffset, valOffset, valLength } = offsetsFor(padShortcutText, 'cmdShift2');
|
|
const { leading } =
|
|
extractAdjacentComments(padShortcutText, keyOffset, valOffset, valLength);
|
|
assert.equal(leading, '');
|
|
});
|
|
|
|
test('leading is empty when prior line is plain code (no trailing comment)', () => {
|
|
const { keyOffset, valOffset, valLength } = offsetsFor(padShortcutText, 'return');
|
|
const { leading } =
|
|
extractAdjacentComments(padShortcutText, keyOffset, valOffset, valLength);
|
|
assert.equal(leading, '');
|
|
});
|
|
|
|
test('still recognises JSDoc-style leading block comments', () => {
|
|
const text = `{
|
|
/*
|
|
* Pad Shortcut Keys
|
|
*/
|
|
"padShortcutEnabled" : {}
|
|
}`;
|
|
const keyOffset = text.indexOf('"padShortcutEnabled"');
|
|
const valOffset = text.indexOf('{}');
|
|
const { leading, trailing } = extractAdjacentComments(text, keyOffset, valOffset, 2);
|
|
assert.equal(leading, 'Pad Shortcut Keys');
|
|
assert.equal(trailing, '');
|
|
});
|
|
|
|
test('still recognises single-line // leading comments', () => {
|
|
const text = `{
|
|
// Whether to enable the thing.
|
|
"thing": true
|
|
}`;
|
|
const keyOffset = text.indexOf('"thing"');
|
|
const valOffset = text.indexOf('true');
|
|
const { leading } = extractAdjacentComments(text, keyOffset, valOffset, 4);
|
|
assert.equal(leading, 'Whether to enable the thing.');
|
|
});
|
|
|
|
test('humanize spaces camelCase and capitalises only the first word', () => {
|
|
assert.equal(humanize('requireAuthentication'), 'Require authentication');
|
|
assert.equal(humanize('altF9'), 'Alt f9');
|
|
});
|
|
|
|
test('labelAndHelp splits a leading block at the first sentence boundary', () => {
|
|
const { label, help } = labelAndHelp(
|
|
'Name your instance! Optional context follows.',
|
|
'title',
|
|
);
|
|
assert.equal(label, 'Name your instance!');
|
|
assert.equal(help, 'Optional context follows.');
|
|
});
|