feat(suggestions): visible, reversible rejection in the modal rail
A red-✗ dismissal no longer makes the suggestion vanish. The rejected tag stays in the rail — dimmed, struck-through, with a "rejected" pill and a one-click undo (↶) in place of the ✗ — so a misclick is recoverable and the operator can see what they've said no to (operator-asked 2026-06-27). Backend: SuggestionService.for_image now KEEPS rejected tags, flagged rejected=True, sorted to the bottom of their category, instead of dropping them. New AllowlistService.undismiss + POST /suggestions/undismiss clears the TagSuggestionRejection. Rejected items are still excluded from bulk consensus (for_selection) and the type-to-add dropdown, whose jobs are unchanged. Frontend: store.dismiss flags in place (canonical tags) rather than dropping; new store.undismiss reverts. SuggestionItem renders the rejected state and swaps ✗→↶; ✓ still accepts (which clears the rejection server-side). Tests: rejected-surfaced-flagged-then-reversible (service) + undismiss endpoint idempotency (API). Completes #1134's reversible-rejection half. Heads-as-suggestion-source is the remaining piece. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -84,6 +84,19 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Flip the `rejected` flag on the matching suggestion in BOTH lists in place,
|
||||
// so a reject/un-reject shows immediately without dropping the row (visible,
|
||||
// reversible rejection — misclick recovery, operator-asked 2026-06-27).
|
||||
function _setRejectedEverywhere(suggestion, value) {
|
||||
const key = _keyOf(suggestion)
|
||||
const cat = suggestion.category
|
||||
for (const map of [byCategory.value, allByCategory.value]) {
|
||||
for (const s of map[cat] || []) {
|
||||
if (_keyOf(s) === key) s.rejected = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function accept(suggestion) {
|
||||
// Capture imageId so a mid-flight prev/next can't reroute the
|
||||
// accept POST to a different image AND push the tag to that
|
||||
@@ -168,21 +181,36 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
async function dismiss(suggestion) {
|
||||
const imageId = currentImageId
|
||||
if (imageId == null) return
|
||||
// Dismiss needs a tag_id; raw tags have none, so dismissing a raw
|
||||
// suggestion just hides it client-side (nothing to persist a rejection
|
||||
// against until the tag exists).
|
||||
if (suggestion.canonical_tag_id != null) {
|
||||
await api.post(`/api/images/${imageId}/suggestions/dismiss`, {
|
||||
body: { tag_id: suggestion.canonical_tag_id }
|
||||
})
|
||||
// Dismiss needs a tag_id. Raw tags (creates_new_tag) have none, so there's
|
||||
// nothing to persist a rejection against — drop them client-side as before.
|
||||
// Canonical tags persist a rejection and STAY in the list flagged rejected,
|
||||
// so the operator can see it and one-click un-reject (misclick recovery).
|
||||
if (suggestion.canonical_tag_id == null) {
|
||||
if (currentImageId === imageId) _dropEverywhere(suggestion)
|
||||
return
|
||||
}
|
||||
await api.post(`/api/images/${imageId}/suggestions/dismiss`, {
|
||||
body: { tag_id: suggestion.canonical_tag_id }
|
||||
})
|
||||
if (currentImageId === imageId) {
|
||||
_dropEverywhere(suggestion)
|
||||
_setRejectedEverywhere(suggestion, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Undo a per-image dismissal — the suggestion reverts to a live row.
|
||||
async function undismiss(suggestion) {
|
||||
const imageId = currentImageId
|
||||
if (imageId == null || suggestion.canonical_tag_id == null) return
|
||||
await api.post(`/api/images/${imageId}/suggestions/undismiss`, {
|
||||
body: { tag_id: suggestion.canonical_tag_id }
|
||||
})
|
||||
if (currentImageId === imageId) {
|
||||
_setRejectedEverywhere(suggestion, false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
byCategory, allByCategory, loading, error,
|
||||
load, loadAll, accept, aliasAccept, removeAlias, dismiss
|
||||
load, loadAll, accept, aliasAccept, removeAlias, dismiss, undismiss
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user