fix(playlists): what the first review of bulk actions found
An Esc during a pointer drag is spent on the drag and leaves the selection. A wheel over the rail re-reads the target each frame, so a drop lands on the row under the pointer. After a bulk removal the focus goes to the next row left, and to the list's menu when the list is emptied. remove-bulk names the videos that went, and its Undo brings back only those. An Undo of a move keeps each row (and when it was saved), leaves out a video that moved on from the target, closes up its place, and says how many came back. Delete with every tick hidden removes nothing and says why. A list gone before the lock answers 404, and a body without its videos 422. One removal path now: the single row goes through the bulk one, the same toast and undo, which also retires the add-at-position route. railDrop reuses the store and scrollParent, the drag image the overlay and the z-index token, and the sensors keep their number. The count is read out from a region that outlives the band. Tests fill the gaps the review named, each seen failing.
This commit is contained in:
@@ -6,7 +6,7 @@ out (`/{id}/push-plan`, `/{id}/revert-youtube`), behind the optional write scope
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic import BaseModel, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import quota
|
||||
@@ -386,24 +386,17 @@ def delete_playlist(
|
||||
return {"deleted": playlist_id}
|
||||
|
||||
|
||||
class AddItemIn(VideoRefIn):
|
||||
# Where the video lands among the list's items, in their stored order (0 = first). Omitted, it
|
||||
# is appended. The list page's "Undo" of a removal puts the row back where it was with this —
|
||||
# ONE request, because append-then-reorder leaves it at the end whenever the second call fails.
|
||||
position: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/items")
|
||||
def add_item(
|
||||
playlist_id: int,
|
||||
body: AddItemIn,
|
||||
body: VideoRefIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
pl = _own_playlist(db, user, playlist_id, lock=True)
|
||||
video_id = body.video_id
|
||||
_video_or_404(db, video_id)
|
||||
service.add_to_playlist(db, pl, video_id, body.position)
|
||||
service.add_item(db, pl, video_id, mark_dirty=True)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "video_id": video_id}
|
||||
|
||||
@@ -415,14 +408,15 @@ def remove_item(
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
pl = _own_playlist(db, user, playlist_id, lock=True)
|
||||
service.remove_item(db, pl, video_id, mark_dirty=True)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "video_id": video_id}
|
||||
|
||||
|
||||
class VideoIdsIn(BaseModel):
|
||||
video_ids: list[str] = []
|
||||
# Required: with a default, a misnamed key answered 200 having acted on no video.
|
||||
video_ids: list[str]
|
||||
|
||||
|
||||
def _paired(video_count: int, positions: list[int]) -> None:
|
||||
@@ -460,6 +454,11 @@ class UndoMoveIn(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
def _list_gone(e: service.ListGone) -> HTTPException:
|
||||
"""Each list of a two-list write under the reason the route's own read gives it."""
|
||||
return not_found("playlist") if e.source else not_found("move_target")
|
||||
|
||||
|
||||
def _other_list(db: Session, user: User, playlist_id: int) -> "Playlist":
|
||||
"""The other list of a move. Its own reason: "this playlist doesn't exist any more" about a list
|
||||
that is gone was read as the one on screen, which is still there."""
|
||||
@@ -476,8 +475,8 @@ def remove_items(
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Remove many videos from a playlist at once. Returns how many went."""
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
"""Remove many videos from a playlist at once. Returns the ones that went."""
|
||||
pl = _own_playlist(db, user, playlist_id, lock=True)
|
||||
removed = service.remove_items(db, pl, body.video_ids)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "removed": removed}
|
||||
@@ -491,7 +490,7 @@ def restore_items(
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""The "Undo" of a removal: the videos back at their places. Returns how many came back."""
|
||||
pl = _own_playlist(db, user, playlist_id)
|
||||
pl = _own_playlist(db, user, playlist_id, lock=True)
|
||||
added = service.restore_items(db, pl, body.video_ids, body.positions)
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": pl.id, "added": added}
|
||||
@@ -519,6 +518,8 @@ def move_items(
|
||||
raise HTTPException(
|
||||
status_code=400, detail={"code": "playlist_move", "reason": e.reason}
|
||||
) from None
|
||||
except service.ListGone as e:
|
||||
raise _list_gone(e) from None
|
||||
db.commit()
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -539,19 +540,18 @@ def undo_move(
|
||||
"""The "Undo" of a move, back into this playlist in one transaction. Returns how many came back."""
|
||||
src = _own_playlist(db, user, playlist_id)
|
||||
dst = _other_list(db, user, body.from_playlist_id)
|
||||
added = service.undo_move(db, src, dst, body.moved, body.already_there, body.positions)
|
||||
try:
|
||||
added = service.undo_move(db, src, dst, body.moved, body.already_there, body.positions)
|
||||
except service.ListGone as e:
|
||||
raise _list_gone(e) from None
|
||||
db.commit()
|
||||
return {"ok": True, "playlist_id": src.id, "added": added}
|
||||
|
||||
|
||||
class ReorderItemsIn(BaseModel):
|
||||
video_ids: list[str] = []
|
||||
|
||||
|
||||
@router.put("/{playlist_id}/order")
|
||||
def reorder_items(
|
||||
playlist_id: int,
|
||||
body: ReorderItemsIn,
|
||||
body: VideoIdsIn,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user