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:
@@ -16,7 +16,7 @@ const ALLOWED_COLORS: ColorLabel[] = [
|
||||
'blue',
|
||||
'purple',
|
||||
]
|
||||
const ALLOWED_FLAGS: FlagFilter[] = ['any', 'picked', 'rejected', 'unflagged']
|
||||
const ALLOWED_FLAGS: FlagFilter[] = ['any', 'picked', 'trashed', 'unflagged']
|
||||
|
||||
function parseUrl(): Partial<FilterState> {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
|
||||
@@ -14,7 +14,7 @@ interface KeyboardShortcutsProps {
|
||||
interface PhotoUpdate {
|
||||
rating?: number
|
||||
is_picked?: boolean
|
||||
is_rejected?: boolean
|
||||
is_trashed?: boolean
|
||||
color_label?: string | null
|
||||
}
|
||||
|
||||
@@ -129,17 +129,19 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) {
|
||||
updateActive({ rating: 0 })
|
||||
})
|
||||
|
||||
// Pick / reject / unflag.
|
||||
// Pick / trash / unflag. Trash is the merged "rejected" concept — a soft
|
||||
// flag that hides the photo from the default timeline view; restore via
|
||||
// the trash view (or the U shortcut).
|
||||
useHotkeys('p', () => {
|
||||
updateActive({ is_picked: true, is_rejected: false })
|
||||
updateActive({ is_picked: true, is_trashed: false })
|
||||
})
|
||||
|
||||
useHotkeys('x', () => {
|
||||
updateActive({ is_rejected: true, is_picked: false })
|
||||
updateActive({ is_trashed: true, is_picked: false })
|
||||
})
|
||||
|
||||
useHotkeys('u', () => {
|
||||
updateActive({ is_picked: false, is_rejected: false })
|
||||
updateActive({ is_picked: false, is_trashed: false })
|
||||
})
|
||||
|
||||
// Color labels 6-9 (red/orange/yellow/green per spec §6.4).
|
||||
|
||||
Reference in New Issue
Block a user