Add live duplicate check to the new-entry dialog#2411
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a duplicate-entry detection feature to the entry editor: a new duplicate-check utility module classifies candidate entries by match kind, a ChangesDuplicate Check Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
frontend/viewer/tests/new-entry-duplicates.test.ts (3)
21-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSelectors depend on internal inline
styleattributes.
lexemeInput/glossInputlocate fields via[style*="grid-area: lexemeForm"]and[style*="grid-area: gloss"]. These are implementation details of the component's CSS grid layout, not stable testing contracts — any layout refactor (e.g. switching to a class-based grid or renaming grid areas) will silently break these locators.♻️ Suggested approach
Add
data-testid(or rely on accessible labels) on the lexeme/gloss form fields in the dialog component, then select viadialog.getByTestId('lexeme-form-field')etc. This decouples tests from layout implementation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/viewer/tests/new-entry-duplicates.test.ts` around lines 21 - 27, The current test helpers lexemeInput and glossInput are coupled to internal inline style selectors, which makes the Playwright locators brittle. Update the dialog component to expose stable selectors such as data-testid or accessible labels for the lexeme and gloss fields, then change these helpers to use those stable hooks instead of querying [style*="grid-area: ..."] so the new-entry-duplicates test no longer depends on CSS layout details.
103-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTest hardcodes fixture-specific counts ("3", "10+") tightly coupled to demo dataset.
This test's assertions (
toHaveCount(3), checking for the literal text'10+') depend on the exact number of demo entries whose headword starts with'ba'. Any change todemo-entry-data.ts(adding/removing entries starting with "ba") will silently break this test without an obvious link to the root cause.Consider deriving the expected counts from the demo dataset itself (e.g., counting matching entries programmatically) or adding an inline comment enumerating which demo entries are expected to match, to make the coupling more discoverable when it breaks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/viewer/tests/new-entry-duplicates.test.ts` around lines 103 - 119, The duplicate-list test is hardcoded to fixture-specific counts, so update the assertions in new-entry-duplicates.test.ts to avoid depending on a fixed "3" or literal "10+" tied to the demo dataset. Use the existing test helpers around openNewEntryDialog, lexemeInput, duplicateRows, and duplicatesSummary to derive the expected number of matches programmatically from the demo entries or document the exact expected "ba" matches inline so changes in demo-entry-data.ts are easier to track.
82-82: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueNon-null assertion on
entryIdcould produce a confusing failure if absent.
new URL(page.url()).searchParams.get('entryId')!would silently coercenullto satisfy the type checker if the param is missing, leading to an unclear downstream assertion failure viaentryHasGlossValuerather than a clear "entryId missing" error.🧪 Suggested tweak
- const entryId = new URL(page.url()).searchParams.get('entryId')!; + const entryId = new URL(page.url()).searchParams.get('entryId'); + expect(entryId).toBeTruthy(); await expect(async () => { - expect(await browsePage.api.entryHasGlossValue(entryId, newGloss)).toBe(true); + expect(await browsePage.api.entryHasGlossValue(entryId!, newGloss)).toBe(true); }).toPass({timeout: 5000});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/viewer/tests/new-entry-duplicates.test.ts` at line 82, The `entryId` lookup in `new-entry-duplicates.test.ts` is using a non-null assertion that can hide a missing query param and cause a misleading failure later. Update the test around `page.url()` and `searchParams.get('entryId')` to explicitly check for a missing value and fail immediately with a clear message before calling `entryHasGlossValue`, so the failure points directly to the absent `entryId`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/viewer/src/lib/entry-editor/DuplicateCheck.svelte`:
- Around line 113-128: The pending save in addSenseToEntry can overwrite a newer
user navigation because openEntry(target) always runs after createSense
resolves. Update the DuplicateCheck.svelte flow so a “go to entry” click during
an in-flight add-sense either waits/blocks, or the post-save navigation is
skipped if the user has already navigated elsewhere; use the existing
addingSense state and the entry-opening logic around addSenseToEntry/openEntry
to keep the final navigation aligned with the user’s last click.
---
Nitpick comments:
In `@frontend/viewer/tests/new-entry-duplicates.test.ts`:
- Around line 21-27: The current test helpers lexemeInput and glossInput are
coupled to internal inline style selectors, which makes the Playwright locators
brittle. Update the dialog component to expose stable selectors such as
data-testid or accessible labels for the lexeme and gloss fields, then change
these helpers to use those stable hooks instead of querying [style*="grid-area:
..."] so the new-entry-duplicates test no longer depends on CSS layout details.
- Around line 103-119: The duplicate-list test is hardcoded to fixture-specific
counts, so update the assertions in new-entry-duplicates.test.ts to avoid
depending on a fixed "3" or literal "10+" tied to the demo dataset. Use the
existing test helpers around openNewEntryDialog, lexemeInput, duplicateRows, and
duplicatesSummary to derive the expected number of matches programmatically from
the demo entries or document the exact expected "ba" matches inline so changes
in demo-entry-data.ts are easier to track.
- Line 82: The `entryId` lookup in `new-entry-duplicates.test.ts` is using a
non-null assertion that can hide a missing query param and cause a misleading
failure later. Update the test around `page.url()` and
`searchParams.get('entryId')` to explicitly check for a missing value and fail
immediately with a clear message before calling `entryHasGlossValue`, so the
failure points directly to the absent `entryId`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 74e64c61-0469-474a-9d9b-58ef682e3de4
📒 Files selected for processing (13)
frontend/viewer/src/lib/entry-editor/DuplicateCheck.sveltefrontend/viewer/src/lib/entry-editor/NewEntryDialog.sveltefrontend/viewer/src/lib/entry-editor/duplicate-check.test.tsfrontend/viewer/src/lib/entry-editor/duplicate-check.tsfrontend/viewer/src/locales/en.pofrontend/viewer/src/locales/es.pofrontend/viewer/src/locales/fr.pofrontend/viewer/src/locales/id.pofrontend/viewer/src/locales/ko.pofrontend/viewer/src/locales/ms.pofrontend/viewer/src/locales/sw.pofrontend/viewer/src/locales/vi.pofrontend/viewer/tests/new-entry-duplicates.test.ts
| async function addSenseToEntry(target: IEntry): Promise<void> { | ||
| if (!sense || addingSense) return; | ||
| addingSense = true; | ||
| try { | ||
| const senseSnapshot = {...$state.snapshot(sense), entryId: target.id}; | ||
| await saveHandler.handleSave(() => lexboxApi.createSense(target.id, senseSnapshot)); | ||
| } finally { | ||
| addingSense = false; | ||
| } | ||
| AppNotification.display( | ||
| pt($t`Sense added to "${writingSystemService.headword(target)}"`, | ||
| $t`Meaning added to "${writingSystemService.headword(target)}"`, | ||
| viewService.currentView), | ||
| {type: 'success', timeout: 'short'}); | ||
| openEntry(target); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Race: clicking "go to entry" on another row while an add-sense save is pending can trigger a surprise second navigation.
addingSense only disables the add-sense icon buttons; the "go to entry" buttons stay clickable. If a user clicks a different match's "go to entry" while addSenseToEntry is still awaiting createSense, the dialog closes and navigation happens immediately — but once the pending save resolves, addSenseToEntry still calls openEntry(target) again, silently re-navigating the user to a different entry than the one they just chose.
🛠️ Suggested fix
<button
type="button"
class="grow min-w-0 flex items-center gap-2 rounded bg-background/80 hover:bg-accent px-2.5 py-2 text-start"
title={goToLabel}
aria-label={headword ? `${goToLabel}: ${headword}` : goToLabel}
+ disabled={addingSense}
onkeydown={trapEnter}
onclick={() => openEntry(match.entry)}>Also applies to: 176-193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/viewer/src/lib/entry-editor/DuplicateCheck.svelte` around lines 113
- 128, The pending save in addSenseToEntry can overwrite a newer user navigation
because openEntry(target) always runs after createSense resolves. Update the
DuplicateCheck.svelte flow so a “go to entry” click during an in-flight
add-sense either waits/blocks, or the post-save navigation is skipped if the
user has already navigated elsewhere; use the existing addingSense state and the
entry-opening logic around addSenseToEntry/openEntry to keep the final
navigation aligned with the user’s last click.
|
I like it! |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spec-cited review against EntrySearchService/SqlHelpers found the client fold diverged: host-locale lowercasing, unconditional accent-stripping (backend keeps diacritics significant when the query has them), and no morph-token handling (typed "-aji" vs suffix entry "aji" missed exact). Also ranks each search by the writing system it was typed in, fixes the mount-time "Checking" flash, singularizes the one-match banner, and closes a pre-existing double-Enter double-create in the dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- match rows expand to reveal Add sense / Go to entry; DictionaryEntry gains an inline mode for the compact collapsed rows - classification: attribute same-word matches to the field that hit (Same headword vs Same lexeme form), rank similar words closest-in-length first, drop cross-field coincidences instead of a vague 'related' kind - classify in a $derived so the lazy morph-types resource warms at mount and re-classifies without re-searching - dedupe queries per field kind: the same text typed as lexeme and gloss keeps its gloss query (same-meaning matches were lost) - share the banner message/tint/Enter-trap between the strip and the extracted DuplicateSummaryPill - scope backend-parity doc claims to the CRDT FTS path; drop unverified FLEx attribution - adapt the Playwright suite to the new interaction model and cover the jump pill (7 tests); pin length-delta boundary, gloss containment direction, and closest-form ranking in unit tests (33; 8/8 targeted mutants killed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…icates # Conflicts: # frontend/viewer/src/locales/en.po # frontend/viewer/src/locales/es.po # frontend/viewer/src/locales/fr.po # frontend/viewer/src/locales/id.po # frontend/viewer/src/locales/ko.po # frontend/viewer/src/locales/ms.po # frontend/viewer/src/locales/sw.po # frontend/viewer/src/locales/vi.po
|
Superseded by #2433 — same feature rebased onto latest Generated by Claude Code |
Searches existing entries as you type in the new-entry dialog and surfaces likely duplicates before you save.
Resolves #1752
duplicate-check.ts— debounced multi-field search (headword/citation/gloss) via the existing FTSsearchEntries; results classified as same word / similar word / similar meaning and ranked strongest-first (exact match, then closest length, then gloss overlap). Cross-field coincidences (a typed vernacular value that only hit a gloss, or vice versa) are dropped as noise. Classification approximates the backend match semantics (invariant case fold; diacritics significant only when the typed text has them; morph tokens stripped likeEntrySearchService.StripMorphTokens— CRDT FTS path only; FwData and the <3-char fallback don't strip); collation equivalences (ss/eszett, ligatures, ICU-ignorables) are not replicated — a backend folding change should re-check this fileDuplicateCheck.svelte— collapsible strip inNewEntryDialog.svelte(amber when the word already exists, muted otherwise); auto-expands on an exact word match; collapsed header previews the matched headwords; rows expand in place to reveal "Add sense" / "Go to entry" actions; "Show N more" for long lists; quiet green "Looks like a new word" line when nothing matchesDuplicateSummaryPill.svelte— sticky pill in the dialog once the strip scrolls out of view (IntersectionObserver), so a match found while typing lower fields isn't missed; click jumps back and expands the strip; dismissibleDictionaryEntry.svelte— newinlineprop (senses flow on one line) for the compact collapsed rowssearchEntriesfetching 20 rows (over-fetched because cross-field hits are filtered client-side) — typically 1-3 queries per pause; in-process on desktop, SignalR round-trips on server-hosted sessions (abort is client-side discard only; the signal can't cross JSInterop)Note: the
.poregen also picks up stale activity-view#:refs from an earlier refactor (benign, identical across locales).Screenshots
Exact match:

Clicking scrolls to: (and auto-expands)

Note the "Same lexeme form" badge which explains why the displayed headword "gasa" does not exactly match the entered text "gas":
Rows are expandable, because:
Not expanded automatically if only "similar" entries: (but clicking the sticky indicator always triggers an expand)
This:


expands to this:
(from an earlier iteration — rows now expand in place for their actions instead of navigating on click)


Test plan
Considered and rejected
🤖 Generated with Claude Code