refactor: merge reject into trash as a single soft-trash concept

The Photo model previously had two near-identical "negative culling"
states: is_rejected (a flag) and is_trashed (a flag plus a file move).
Lightroom users typically use one or the other, never both, and the
file-move semantics of the old trash made it harder to undo. Merging
into a single soft is_trashed flag — file stays on disk, restore is a
flag flip, permanent deletion still happens via DELETE /trash/empty.

Backend
- Drop is_rejected from PhotoBase, PhotoResponse, PhotoUpdate, the
  list endpoint filter, and the bulk-action 'reject' branch.
- Add is_trashed to PhotoUpdate so the PATCH path can set it.
- Drop is_rejected Column declaration from the SQLAlchemy model. The
  legacy DB column may persist on existing installs but is no longer
  read or written; SQLAlchemy ignores extra columns.
- Rewrite DELETE /photos/{id} as a soft trash: just sets is_trashed=
  true and trashed_at=now, no shutil.move. Permanent deletion still
  goes through the trash router.

Frontend
- Photo TS type drops is_rejected, gains is_trashed.
- X keyboard shortcut now sets is_trashed=true (was is_rejected); U
  clears both is_picked and is_trashed.
- RightSidebar Reject button → Trash button (Trash2 icon).
- PhotoThumbnail flag overlay shows Trash2 icon for trashed photos
  instead of an X for rejected.
- KeyboardHints relabels X from "Reject" to "Trash".
- filterStore FlagFilter renames 'rejected' → 'trashed'; the params
  builder now sends is_trashed=true for the trashed filter (the list
  endpoint defaults to hiding trashed photos otherwise).
- FilterBar dropdown / URL sync allow-list updated accordingly.

No data migration: existing rejected photos remain as-is (flag stale)
and effectively become unflagged in the new model. Re-trash from the
UI to bring them into the new state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:16:10 +02:00
parent f7bf22db29
commit ce2cda0565
9 changed files with 36 additions and 53 deletions

View File

@@ -26,7 +26,7 @@ const COLOR_LABELS: { value: ColorLabel; className: string }[] = [
const FLAG_OPTIONS: { value: FlagFilter; label: string }[] = [
{ value: 'any', label: 'Any' },
{ value: 'picked', label: 'Picked' },
{ value: 'rejected', label: 'Rejected' },
{ value: 'trashed', label: 'Trashed' },
{ value: 'unflagged', label: 'Unflagged' },
]

View File

@@ -9,6 +9,7 @@ import {
ChevronDown,
ChevronRight,
Check,
Trash2,
} from 'lucide-react'
import clsx from 'clsx'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
@@ -26,7 +27,7 @@ interface PhotoDetails {
taken_at: string | null
rating: number
is_picked: boolean
is_rejected: boolean
is_trashed: boolean
exif_json: string | null
}
@@ -107,7 +108,7 @@ export function RightSidebar() {
mutationFn: (data: {
rating?: number
is_picked?: boolean
is_rejected?: boolean
is_trashed?: boolean
}) => photosApi.update(activePhotoId!, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['photo', activePhotoId] })
@@ -131,7 +132,7 @@ export function RightSidebar() {
const multipleSelected = selectedPhotos.length > 1
const rating = photo?.rating ?? 0
const isPicked = photo?.is_picked ?? false
const isRejected = photo?.is_rejected ?? false
const isTrashed = photo?.is_trashed ?? false
return (
<div className="flex h-full flex-col bg-surface">
@@ -186,7 +187,7 @@ export function RightSidebar() {
onClick={() =>
updateMutation.mutate({
is_picked: !isPicked,
is_rejected: false,
is_trashed: false,
})
}
className={clsx(
@@ -202,19 +203,19 @@ export function RightSidebar() {
<button
onClick={() =>
updateMutation.mutate({
is_rejected: !isRejected,
is_trashed: !isTrashed,
is_picked: false,
})
}
className={clsx(
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
isRejected
isTrashed
? 'bg-reject/20 text-reject'
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
)}
>
<X className="h-3 w-3" />
Reject
<Trash2 className="h-3 w-3" />
Trash
</button>
</div>
</div>