[NRT-591] Surface duplicate anki_id suggestion error + redesign bulk suggestion summary#1306
Open
RisingOrange wants to merge 12 commits into
Open
[NRT-591] Surface duplicate anki_id suggestion error + redesign bulk suggestion summary#1306RisingOrange wants to merge 12 commits into
RisingOrange wants to merge 12 commits into
Conversation
…summary Single suggestion: when a new-note suggestion is rejected because the note already exists in the deck on AnkiHub, show a 'Note already exists in this deck' dialog offering to resubmit the edits as a change suggestion (converting the already-built NewNoteSuggestion, no remote fetch / AnkiHub-DB write). Soft-deleted conflicts route to the existing deleted-on-AnkiHub handling. Bulk suggestion: replace the plain-text showText summary with a real dialog shown for all outcomes - categorized results, a new 'Other errors' bucket with a support link, and an interactive 'Notes already in this deck' action (batched resubmit / ignore) with Close gating and 'Updated' badges. Degrades gracefully when the backend doesn't return the conflicting id. Adds analytics events (error_dialog_shown, resubmit_clicked, bulk_error_category_shown, bulk_resubmit_clicked) and tests.
- Gate X/Escape (reject()/closeEvent()) on the bulk summary dialog so an unresolved action or in-flight resubmit can't be dismissed - previously only the Close button was disabled. Adds a regression test. - Replace the stringly-typed action state + separate hard-error flag with a single _ActionState enum (a hard resubmit failure is now FAILED: retry buttons stay, Close works). - Reuse gui.utils.clear_layout instead of a local duplicate; add missing type annotations.
…icate-anki-id # Conflicts: # ankihub/gui/suggestion_dialog.py # ankihub/main/suggestions.py
- Move the bulk summary dialog, its action-state machine, categories, color helpers, and the bulk-submit callback to gui/bulk_suggestion_summary_dialog.py. - Move the shared panel color helpers to gui/utils.py (avoids a circular import between the two dialog modules). - Drop the change_type plumbing from the bulk callback/dialog - converted resubmits are always 'Updated content', matching the single-note path. - Flatten the nested resubmit task/on_done callbacks in _show_note_already_exists_dialog. - Rename module constants to the no-underscore convention.
Match the finalized design: flat layout with section headers (Summary / Submission
issues / Action required), colored summary bullets, and 'no changes' surfaced as a
'skipped' summary line excluded from the failure count. Each category gets a
'Copy note IDs' button (copies an 'nid:' Browse search) and a one-line guidance.
Renamed categories ('Notes deleted on AnkiHub', 'First field empty'); deleted-on-
AnkiHub links to the docs article. Action-required band has a distinct background;
Resubmit is the default button.
…tion - _handle_suggestion_error: a 400 response without 'non_field_errors' iterated None in the no-change check (latent TypeError). Normalize to [] and guard the all() so it degrades to the generic error dialog. Adds a regression test. - Annotate BulkSuggestionSummaryDialog.closeEvent's event param (QCloseEvent).
…cs rename - Full-bleed "Action required" band; content inset at 28px, 16px top margin - Fix resize squashing summary on re-render (defer adjustSize one tick) - Section spacing: 20px between sections, 8px header→band, 16px band→footer - Typography: Summary 18 / Submission issues 16 / titles+badge tiers - macOS: ApplicationModal (no sheet), scope transparent bg so buttons keep their border, top-align the "Updated" badge so it stays pill-height - Narrow min width 520→440 - Rename analytics events to spec: duplicate_note_error_dialog_shown, duplicate_note_resubmitted_as_update_suggestion, bulk_* variants
The band description's last line was clipped at the 440px width: adjustSize() computes height from the label's sizeHint, which under-reports a word-wrap label's wrapped height. Pin each word-wrap label to its real heightForWidth at the current width before resizing.
ddevdan
requested changes
Jun 16, 2026
Contributor
There was a problem hiding this comment.
There's this medium bug that claude flagged where if you submit a note with image, then change the image and try to submit again, the image breaks
Single-note resubmit submits stale media filenames
`resubmit_new_note_as_change_suggestion` rebuilds the suggestion from the **in-memory** `note`, but the failed `suggest_new_note` renamed media to hashed names via raw SQL on the DB only (`_update_media_names_on_notes` → `media_utils.py:9`) — the
in-memory note is stale. So if new media is added during the edit, the change suggestion references the **original** filename, which was never uploaded under that name → broken image for everyone who syncs. Silent, and auto-accept (the maintainer
trigger) skips review.
Bulk path is safe — it reuses the post-rename DTO. Not hit by the canonical NRT-591 repro (text-only edit, no rename), which is why tests stayed green.
**Fix** (one line in the single path):
```python
# Failed new-note submit renamed media in the DB only, not the in-memory note. Reload
# so the change suggestion carries the uploaded (hashed) names, not the stale originals.
note = aqt.mw.col.get_note(note.id)
screen-capture.10.webm
Solution:
++ b/ankihub/main/suggestions.py
@@ -547,6 +547,11 @@ def resubmit_new_note_as_change_suggestion(
"""Resubmit a single note (whose new-note suggestion hit the duplicate-anki_id
error) as a change suggestion for the existing note. Media was already renamed and
uploaded by the failed new-note submit, so it is not re-uploaded here."""
+ # The failed new-note submit renamed media to hashed names directly in the DB (raw
+ # SQL, see `_update_media_names_on_notes`) without touching the in-memory note the
+ # caller is holding. Reload so the change suggestion carries the uploaded (hashed)
+ # media names rather than the stale originals (which were never uploaded).
+ note = aqt.mw.col.get_note(note.id)
new_note_suggestion = _new_note_suggestion(note, ah_did, comment, filters=filters)
if new_note_suggestion is None:
return ChangeSuggestionResult.NO_CHANGESThe failed new-note submit renames media to hashed names directly in the DB via raw SQL (_update_media_names_on_notes), without touching the caller's in-memory note. The single resubmit path rebuilt the suggestion from that stale note, so a note whose media changed during the edit would reference the original (never uploaded) filename — a broken image for everyone who syncs, silent under auto-accept. Reload the note from the collection before building the suggestion. The bulk path is unaffected (it reuses the post-rename DTOs). Adds a regression test (the canonical text-only repro didn't exercise the rename).
Collaborator
Author
|
@ddevdan Good catch! |
Top-align each category title and its "Copy note IDs" button (the button is taller) and put 12px between that row and the guidance text below, matching the Figma spacing. Apply the same to the "Notes already in this deck" band so it's consistent with the Submission issues blocks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issues
Proposed changes
When a user submits a new-note suggestion for a note whose
anki_idalready exists in the deck on AnkiHub (note created + auto-accepted, then edited and re-suggested before a sync), the server rejects it with"A deck can't contain multiple notes with the same anki_id.". Previously this failure was effectively hidden (single → rawshowInfo; bulk → an uncategorized blob in the plain-text summary). This PR surfaces it and lets the user recover.Detection (
main/suggestions.py)ANKIHUB_DUPLICATE_ANKI_ID_ERRORconstant +parse_duplicate_anki_id_error()— tolerates the backend's DRF-normalized payload (list-wrapped/stringified: readsconflicting_ankihub_id[0], comparesconflicting_note_deleted[0] == "True", not truthiness)._new_note_to_change_suggestion()converts the already-builtNewNoteSuggestioninto aChangeNoteSuggestionkeyed by the conflictingah_nidand lets the server diff — no remote fetch, no AnkiHub-DB write.resubmit_new_note_as_change_suggestion()(single) andresubmit_new_notes_as_change_suggestions_in_bulk()(one batched call).suggest_notes_in_bulknow returnsalready_in_deck_by_nidon the result; soft-deleted conflicts are routed to the existing deleted-on-AnkiHub category instead (a change suggestion would 404 a tombstoned note).Scenario 1 — single suggestion (
gui/suggestion_dialog.py)NoteAlreadyExistsDialog(info icon, Cancel / Send as change suggestion). The suggestion dialog closes and the modal shows over the editor; on success → toast; soft-deleted conflict → existing "deleted from AnkiHub" dialog; missing conflicting id (older server) → falls back to today's generic error.Scenario 2 — bulk suggestion summary (
gui/bulk_suggestion_summary_dialog.py)showTextsummary with a realBulkSuggestionSummaryDialog, shown for all outcomes (full redesign): colored counts, bold(N)category titles, a new "Other errors" bucket with acommunity.ankihub.net/c/supportlink, and the redundant "All notes with failed suggestions" block removed.create_suggestions_in_bulkcall) / Ignore;Close/Xdisabled until resolved; "Updated" badges on changed categories; a hard resubmit failure re-enablesClose(no dead-end).Analytics (structlog → Datadog) — single:
duplicate_note_error_dialog_shown,duplicate_note_resubmitted_as_update_suggestion; bulk:bulk_duplicate_note_error_dialog_shown,bulk_duplicate_note_resubmitted_as_update_suggestion.How to reproduce
As a maintainer on staging:
Tests:
pytest tests/addon -k "BulkSuggestionSummaryDialog or MaybeHandleNoteAlreadyExists or ParseDuplicate or NewNoteToChange or OnSuggestNotesInBulkDone or already_in_deck".Screenshots and videos
Redesigned Bulk Suggestion Summary dialog.
Default (dark) — Summary, categorized submission issues, and the full-bleed "Action required" band:

After resubmitting — the band is removed and the Summary updates with an "Updated" badge (failed count drops accordingly):

Ignored — user dismissed the action; the band collapses to a muted note and Close is enabled:

Light mode:
