Conversation
…din code Adds inline editing to the languages admin UI: language name, crowdin code, and a force-language-name checkbox can now be updated per language, and the create form exposes the new fields as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review (standard mode)
Verdict: APPROVED WITH SUGGESTIONS — no blockers. Risk: LOW (1/10).
5 specialized agents (Architecture, Testing, Data Integrity, UX, Standards) + dependency-impact analysis, with cross-examination and rebuttal rounds. 15 findings total: 6 medium-priority (posted as line comments below, dismissible via /dismiss: <reason> replies), 9 informational suggestions (listed here).
Verified correct (recorded as non-findings)
- Payload contract verified end-to-end against the Rails controller/serializer/schema: dashed attribute keys round-trip exactly;
codecorrectly omitted on update (server does not permit it); omittingdirectionis safe (Railspermitonly touches provided keys);'' || nullis the only mechanism for clearing a crowdin code and the backend nullifies correctly;undefined || falsematches the DB default forforce_language_name. - No breaking changes: model fields are additive;
updateLanguageis net-new with a single caller; no other consumer reads the new fields; UI-state fields (isEditing,canConfirmDelete) can never leak into payloads. - Failed save correctly retains the user's edits in the row for retry — any fix for the row-switch finding must preserve this.
Informational suggestions (severity < 5, do not require action or /dismiss)
directionfield added to the model but referenced nowhere (4.0) — wire it up or drop it.- Last-write-wins on concurrent admin edits; no optimistic locking (4.5) — acceptable for this tool.
createLanguage/updateLanguageduplicate the attributes block; siblings extract a privategetPayload()(4.0).- Save/error feedback renders at top of page, far from the edited row (UX 6.0, single-agent → suggestion tier; matches the component's pre-existing pattern).
- Blank crowdin-code cell could render
—(3.5); no Enter/Escape keyboard handling in edit row (3.5);cancelEdit()untested (3.5); update-success spec asserts only the alert, not the service call (3.0); 5-column layout cramped below ~768px (3.5).
Debate highlights
The top finding (dirty row-switch) was contested 6.0–7.5 across agents and settled at 6.5 unanimously after rebuttal: it is display-only, self-healing, and consistent with the repo's existing modal editors (whose cancel paths revert nothing) — but all 5 agents endorse fixing it via the edit-a-clone approach before or shortly after merge.
🤖 Generated with Claude Code
Edit a clone (editedLanguage) so row-switch/cancel never leak unsaved edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add language.service.spec.ts covering updateLanguage/createLanguage wire contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add aria-label to the view-mode force-language-name checkbox Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add update-failure spec: errorMessage set, saving reset, edits retained Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add aria-label to edit-mode checkbox; disable Save/Cancel while saving (re-applied after cherry-pick conflicts with the clone-edit commit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review — Round 2 (standard mode)
Verdict: APPROVED WITH SUGGESTIONS — 0 blockers. Risk: LOW (1/10).
Re-review after the 5 fix commits (97b62d2…85cb157). 5 specialized agents + dependency analysis, full cross-examination debate. The round-1 fixes were verified: the clone refactor is correct by construction (every path traced — failed saves retain edits, no route lets unsaved input display as saved or reach the wire), the new language.service.spec.ts correctly pins the wire contract, and no breaking changes exist.
/dismiss: the round-1 aria-label fix on the view-mode checkbox is non-functional (see line comment). One-token fix.
Suggestions (severity < 5, informational)
- [4.5] Delete/create while a row is being edited silently discards the draft via
loadLanguages(). Recommended remedy (matches the component's one-operation-at-a-time invariants): disable Edit/Delete/create-Save while any row is editing. - [4.0]
directionon the Language model is still dead code (converged from 5.5/3.5 in debate;customPagessets precedent for typed-but-unread fields, so this is scope hygiene, not a defect). - [3.5] Whitespace-only crowdin code is sent verbatim (
' 'is truthy) and would bypass thecrowdin_service.rb:17truthiness guard server-side, turning a clean error into an obscure Crowdin API failure. Cheap fix:(language['crowdin-code'] || '').trim() || nullin both payload builders. - [3.0] Create-form Save button has no
[disabled]="saving"(pre-existing line, shared-flag porosity). - [3.0] "Saving…"/"Success!" alerts render inside the Add-new-language card during edit-row saves (shared flag; screen readers still get
role="alert"). - [3.0]
isEditingandcanConfirmDeletecan coexist on one row (Delete-confirm then Edit); each entry method should clear both flags. - [2.5] Empty-name PUT: settled harmless — the API has
validates :name, presence: true(422 surfaced by the existing alert, whitespace included). - CI observation: Karma exits 0 despite browser-level
ERRORoutput (NG0303, page-reload). Consider enabling@angular-eslint/templateaccessibility rules in a follow-up so the aria-label bug class can't recur silently.
Debate highlights
Testing self-revised its top finding 7.5→6.5 (missing regression guard over working code shouldn't outrank a shipped defect). The aria-label finding was verified independently four ways (Karma NG0303 ×4 with exit 0, ɵɵpropertyInterpolate1 in the prod bundle, ariaLabel absent from Angular 13's DOM schema, and a red-first getAttribute test that fails on current code). Architecture self-revised the post-save window 6.0→5.0 after confirming the pattern is inherited verbatim from createLanguage on master.
🤖 Generated with Claude Code
| <div class="col"> | ||
| <input | ||
| type="checkbox" | ||
| aria-label="Force language name for {{ language.name }}" |
There was a problem hiding this comment.
In Angular 13 an interpolated aria-label compiles to a property binding (ɵɵpropertyInterpolate1 — confirmed in dist/main.js), and aria-label is not in the DOM schema. Dev mode logs NG0303: Can't bind to 'aria-label' since it isn't a known property of 'input' (4× in every Karma run, which still exits 0 — invisible to CI); prod mode sets a JS expando, not the attribute. Either way getAttribute('aria-label') is null and the checkbox has no accessible name — a red-first test asserting the attribute was run against this code and failed. Since commit 743d77f claims this fix, the broken-but-looks-applied state is worse than no fix.
One-token fix (apply to both checkboxes):
<!-- line 71 (view mode, mandatory) -->
attr.aria-label="Force language name for {{ language.name }}"
<!-- line 112 (edit mode, recommended for consistency — use language.name, not editedLanguage.name) -->
attr.aria-label="Force language name for {{ language.name }}"And lock it in: expect(checkbox.getAttribute('aria-label')).toContain('Force language name') — this assertion fails today and passes after the fix.
Note: findings with severity ≥ 7 cannot be dismissed via /dismiss.
|
|
||
| editLanguage(language: Language): void { | ||
| this.languages.forEach((l) => (l.isEditing = false)); | ||
| this.editedLanguage = { ...language }; |
There was a problem hiding this comment.
Mutation-verified twice: reverting this line to this.editedLanguage = language; keeps all 148 specs green — no spec references editedLanguage, and the edit row's bindings are never instantiated under test. The invariant this PR's core fix guarantees (list objects never mutated by typing; Cancel restores truth) has no automated guard, and this exact bug class already shipped once in round 1.
it('re-snapshots editedLanguage on row switch and does not mutate the original row', () => {
const languageOne = new Language();
languageOne.name = 'French';
const languageTwo = new Language();
languageTwo.name = 'German';
comp.languages = [languageOne, languageTwo];
comp.editLanguage(languageOne);
comp.editedLanguage.name = 'Français'; // simulate typing
expect(languageOne.name).toBe('French'); // clone isolation
comp.editLanguage(languageTwo);
expect(comp.editedLanguage.name).toBe('German'); // fresh snapshot
});| language.isEditing = true; | ||
| } | ||
|
|
||
| cancelEdit(language: Language): void { |
There was a problem hiding this comment.
| .updateLanguage(language) | ||
| .then(() => { | ||
| this.showSuccess(); | ||
| this.loadLanguages(); |
There was a problem hiding this comment.
During the reload round-trip the edit row is still rendered with Save/Cancel re-enabled: a double-click sends a duplicate (idempotent) PUT, keystrokes typed in the window are discarded on swap, and if the reload rejects the row is stranded open showing "Success!" next to an error (Cancel remains a one-click escape). The reload is also the only mechanism that closes the edit row, running unobserved.
Fix (one line each): make loadLanguages() return its promise and chain it:
private loadLanguages(): Promise<void> {
this.loading = true;
return this.languageService.getLanguages()
.then((languages) => (this.languages = languages))
.catch(this.handleError.bind(this))
.then(() => (this.loading = false));
}
// in updateLanguage's success handler:
.then(() => {
this.showSuccess();
return this.loadLanguages();
})This pattern is inherited from createLanguage on master, so aligning that flow too is optional but keeps them symmetric.
| comp = fixture.componentInstance; | ||
| }); | ||
|
|
||
| it('should show success alert after successfully updating a language', (done) => { |
There was a problem hiding this comment.
One deferred-stub spec covers this AND the post-save window finding together: stub getLanguages with a controllable promise, save, assert saving stays true until the reload resolves, then assert the refreshed list has no editing row. Recommended to land in the same commit as the loadLanguages() promise-chaining fix.
Jira: GT-2321
Summary
LanguageService.updateLanguage()(PUT/languages/:id) and extends the create payload; attribute keys use the dashed JSON:API names the API serializes (crowdin-code,force-language-name)codeon updateTest plan
yarn test --watch=false --browsers=ChromeHeadless— all 143 specs pass (2 new: update success alert, single-row editing)yarn lintandyarn prettier:checkpass;yarn buildsucceeds🤖 Generated with Claude Code