refactor: rename trash to discard end-to-end
User-facing labels and code now use "discard" (verb) and "Discarded"
(state/view label) instead of "trash" / "Trashed". The DB column names
stay (is_trashed / trashed_at) so no migration is required — only the
SQLAlchemy attribute names are renamed via Column('old_name', ...).
Backend
- Photo model: is_discarded / discarded_at attributes (DB columns
unchanged).
- PhotoBase / PhotoResponse / PhotoUpdate schemas use the new field
names.
- Photos list endpoint: is_discarded query param, filter logic.
- DELETE /photos/{id} now sets is_discarded; success message updated.
- Bulk action 'trash' renamed to 'discard'.
- backend/app/routers/trash.py renamed to discard.py with renamed
functions and route prefix /api/v1/discard.
- main.py imports and mounts the discard router.
- tasks/scan.py marks missing files as is_discarded.
Frontend
- Photo TS type: is_discarded.
- PhotoThumbnail: shows the trash-can icon when is_discarded.
- RightSidebar: button label "Discard"; mutation field name; local
variable rename.
- TopBar: discardPhotosMutation and "Discard" button; toast text
"Discarded".
- LeftSidebar: virtual node id 'discarded' / label "Discarded".
- FilterBar / filterStore / useFilterUrlSync: FlagFilter enum value
'trashed' → 'discarded'; backend param key is_discarded.
- KeyboardHints: X label "Discard".
- useKeyboardShortcuts: PhotoUpdate field rename, X handler.
- api.ts: /trash routes → /discard, trash export → discard,
bulkUpdate trash field → discard.
Out of scope (intentional): the docker-compose trash_data volume,
backend/Dockerfile mkdir /data/trash, config.py TrashSettings, and
the spec doc — all unused since soft-discard, and renaming them is
churn for no benefit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Trash API router
|
||||
Discard API router
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select, and_
|
||||
@@ -12,39 +12,39 @@ from app.models import Photo
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("")
|
||||
async def list_trashed(db: AsyncSession = Depends(get_db)):
|
||||
"""List trashed photos"""
|
||||
async def list_discarded(db: AsyncSession = Depends(get_db)):
|
||||
"""List discarded photos"""
|
||||
result = await db.execute(
|
||||
select(Photo).where(Photo.is_trashed == True)
|
||||
select(Photo).where(Photo.is_discarded == True)
|
||||
)
|
||||
photos = result.scalars().all()
|
||||
return photos
|
||||
|
||||
@router.post("/restore")
|
||||
async def restore_photos(photo_ids: list[str], db: AsyncSession = Depends(get_db)):
|
||||
"""Restore photos from trash"""
|
||||
"""Restore photos from the discard pile"""
|
||||
result = await db.execute(
|
||||
select(Photo).where(and_(Photo.id.in_(photo_ids), Photo.is_trashed == True))
|
||||
select(Photo).where(and_(Photo.id.in_(photo_ids), Photo.is_discarded == True))
|
||||
)
|
||||
photos = result.scalars().all()
|
||||
|
||||
|
||||
for photo in photos:
|
||||
photo.is_trashed = False
|
||||
photo.trashed_at = None
|
||||
|
||||
photo.is_discarded = False
|
||||
photo.discarded_at = None
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "restored": len(photos)}
|
||||
|
||||
@router.delete("/empty")
|
||||
async def empty_trash(db: AsyncSession = Depends(get_db)):
|
||||
"""Permanently delete all trashed photos"""
|
||||
async def empty_discard(db: AsyncSession = Depends(get_db)):
|
||||
"""Permanently delete all discarded photos"""
|
||||
result = await db.execute(
|
||||
select(Photo).where(Photo.is_trashed == True)
|
||||
select(Photo).where(Photo.is_discarded == True)
|
||||
)
|
||||
photos = result.scalars().all()
|
||||
|
||||
|
||||
for photo in photos:
|
||||
await db.delete(photo)
|
||||
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "deleted": len(photos)}
|
||||
return {"status": "success", "deleted": len(photos)}
|
||||
@@ -33,7 +33,7 @@ async def list_photos(
|
||||
rating_max: Optional[int] = Query(None, ge=0, le=5),
|
||||
color_label: Optional[str] = None,
|
||||
is_picked: Optional[bool] = None,
|
||||
is_trashed: Optional[bool] = False,
|
||||
is_discarded: Optional[bool] = False,
|
||||
heap_id: Optional[str] = None,
|
||||
sort: str = "taken_at",
|
||||
order: str = "desc",
|
||||
@@ -93,8 +93,8 @@ async def list_photos(
|
||||
if is_picked is not None:
|
||||
filters.append(Photo.is_picked == is_picked)
|
||||
|
||||
# Trash filter — defaults to hiding trashed photos
|
||||
filters.append(Photo.is_trashed == is_trashed)
|
||||
# Discard filter — defaults to hiding discarded photos
|
||||
filters.append(Photo.is_discarded == is_discarded)
|
||||
|
||||
# Apply all filters
|
||||
if filters:
|
||||
@@ -407,13 +407,13 @@ async def update_photo(
|
||||
return PhotoResponse.from_orm(photo)
|
||||
|
||||
@router.delete("/{photo_id}")
|
||||
async def trash_photo(
|
||||
async def discard_photo(
|
||||
photo_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Soft-trash a photo: sets is_trashed=true. The file stays on disk so
|
||||
"""Soft-discard a photo: sets is_discarded=true. The file stays on disk so
|
||||
restore is just a flag flip. Permanent deletion happens via DELETE
|
||||
/trash/{id} or DELETE /trash/empty.
|
||||
/discard/{id} or DELETE /discard/empty.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Photo).where(Photo.id == photo_id)
|
||||
@@ -423,11 +423,11 @@ async def trash_photo(
|
||||
if not photo:
|
||||
raise HTTPException(status_code=404, detail="Photo not found")
|
||||
|
||||
photo.is_trashed = True
|
||||
photo.trashed_at = datetime.utcnow()
|
||||
photo.is_discarded = True
|
||||
photo.discarded_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Photo moved to trash"}
|
||||
return {"status": "success", "message": "Photo discarded"}
|
||||
|
||||
@router.post("/bulk")
|
||||
async def bulk_action(
|
||||
@@ -445,14 +445,14 @@ async def bulk_action(
|
||||
raise HTTPException(status_code=404, detail="No photos found")
|
||||
|
||||
# Perform action based on type
|
||||
if action.action == 'trash':
|
||||
if action.action == 'discard':
|
||||
for photo in photos:
|
||||
photo.is_trashed = True
|
||||
photo.trashed_at = datetime.utcnow()
|
||||
photo.is_discarded = True
|
||||
photo.discarded_at = datetime.utcnow()
|
||||
elif action.action == 'restore':
|
||||
for photo in photos:
|
||||
photo.is_trashed = False
|
||||
photo.trashed_at = None
|
||||
photo.is_discarded = False
|
||||
photo.discarded_at = None
|
||||
elif action.action == 'set_rating':
|
||||
for photo in photos:
|
||||
photo.rating = action.value
|
||||
@@ -462,7 +462,7 @@ async def bulk_action(
|
||||
elif action.action == 'pick':
|
||||
for photo in photos:
|
||||
photo.is_picked = True
|
||||
photo.is_trashed = False
|
||||
photo.is_discarded = False
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid action")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user