mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat: rename editable design systems from Settings + od CLI
Editable (user-created) design systems can already be renamed via
PATCH /api/design-systems/:id, but the capability was not surfaced
in the UI or CLI.
- Settings -> Design Systems: editable cards show a hover-reveal pencil
next to the name that opens a rename modal; built-in cards stay
read-only. Reuses common.rename/save/cancel (no new i18n keys).
- CLI: 'od design-systems rename <id> --title <new> [--json]', backed by
a unit-tested pure arg parser (design-system-rename-args.ts).
Both surfaces call the existing PATCH endpoint.
* Route od design-systems --help and -h to the rename-aware usage
The dispatcher only special-cased the `help` subcommand, so
`od design-systems --help` and `-h` fell through to the generic library
list, which advertises only `list` and `show`. That left `rename` off the
main discovery path even though this PR ships it.
Pulled the usage text and the help-arg check into a small pure module so
`help`, `--help`, and `-h` all render the same rename-aware usage, and added
a test that asserts the flag forms route to help and that the text lists
rename. The pure module keeps the assertion off process.exit / console.log.
* Reject --title flag-as-value and keep the rename modal open on failure
Two rename edge cases from review.
CLI: parseDesignSystemRenameArgs took the next token after --title
unconditionally, so `rename user:acme --title --json` parsed the title as
"--json" and could rename the system to a flag name instead of failing usage
validation. A separate --title value must now be a real token; a leading dash
means the user uses the --title=<value> form. Malformed inputs return null,
which the CLI surfaces as a usage error.
Web: commitRename closed the modal unconditionally, but updateDesignSystemDraft
returns null on any non-OK response or fetch failure, so a transient error
dropped the typed title with no feedback. The modal now stays open with the
title intact and shows an inline error on failure, matching the existing import
error pattern in this component. Added tests for the flag-as-value rejection
and for the failed-update modal state.
* Gate the rename completion on the active modal session
commitRename mutated the shared modal state after awaiting the PATCH, so a
slow rename for system A could resolve after the user cancelled and opened a
rename for system B, then close B's modal or show A's failure inside B's
dialog.
A monotonic session token (bumped whenever the modal opens or closes) is now
captured before the request and rechecked after it resolves. A stale
completion skips all modal-state updates. The list update for a successful
rename still applies, since that reflects a real server-side change regardless
of which modal is open. Added a regression test that opens a second rename
before the first PATCH settles and confirms the newer modal is untouched.
* Localize the rename-failed error instead of hardcoding English
The inline rename error was hardcoded English on a Settings surface that
otherwise runs through useT(), so non-English users saw English while the
rest of the panel was localized.
Added settings.designSystemRenameFailed to the typed dictionary and all 19
locale files, and the modal now reads it through t(). The translations are
adapted from each locale's existing settings.rescanFailed string ("X failed.
Check the daemon and try again."), swapping the verb to rename, so the daemon
and retry wording matches what those locales already ship.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { parseDesignSystemRenameArgs } from '../src/design-system-rename-args.js';
|
|
|
|
describe('parseDesignSystemRenameArgs', () => {
|
|
it('reads the id positional and the title from --title', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title', 'Acme v2'])).toEqual({
|
|
id: 'user:acme',
|
|
title: 'Acme v2',
|
|
});
|
|
});
|
|
|
|
it('accepts --title=<value>', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title=Acme v2'])).toEqual({
|
|
id: 'user:acme',
|
|
title: 'Acme v2',
|
|
});
|
|
});
|
|
|
|
it('falls back to trailing positionals as the title', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', 'Acme', 'v2'])).toEqual({
|
|
id: 'user:acme',
|
|
title: 'Acme v2',
|
|
});
|
|
});
|
|
|
|
it('does not mistake a --daemon-url value for the id or title', () => {
|
|
expect(
|
|
parseDesignSystemRenameArgs([
|
|
'--daemon-url',
|
|
'http://127.0.0.1:7456',
|
|
'user:acme',
|
|
'--title',
|
|
'Acme v2',
|
|
]),
|
|
).toEqual({ id: 'user:acme', title: 'Acme v2' });
|
|
});
|
|
|
|
it('returns null when the title is missing', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme'])).toBeNull();
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title', ' '])).toBeNull();
|
|
});
|
|
|
|
it('returns null when the id is missing', () => {
|
|
expect(parseDesignSystemRenameArgs(['--title', 'Acme v2'])).toBeNull();
|
|
expect(parseDesignSystemRenameArgs([])).toBeNull();
|
|
});
|
|
|
|
it('does not treat a following flag as the --title value', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title', '--json'])).toBeNull();
|
|
expect(
|
|
parseDesignSystemRenameArgs(['user:acme', '--title', '--daemon-url', 'http://127.0.0.1:7456']),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null when --title is the last token with no value', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title'])).toBeNull();
|
|
});
|
|
|
|
it('still accepts a dash-leading title via the --title=<value> form', () => {
|
|
expect(parseDesignSystemRenameArgs(['user:acme', '--title=-dash-brand'])).toEqual({
|
|
id: 'user:acme',
|
|
title: '-dash-brand',
|
|
});
|
|
});
|
|
});
|