diff --git a/frontend/src/admin/DraftQueue.tsx b/frontend/src/admin/DraftQueue.tsx index 07d65c5..9afc1e6 100644 --- a/frontend/src/admin/DraftQueue.tsx +++ b/frontend/src/admin/DraftQueue.tsx @@ -10,6 +10,7 @@ import Empty from 'antd/es/empty'; import Alert from 'antd/es/alert'; import Modal from 'antd/es/modal'; import message from 'antd/es/message'; +import { RotateLeftOutlined, RotateRightOutlined } from '@ant-design/icons'; import { Draft, DraftImage, @@ -19,6 +20,7 @@ import { publishDraft, setImageBackground } from './draftsApi'; +import { rotateImage, RotateDirection } from './imagesApi'; const { TextArea } = Input; @@ -52,6 +54,15 @@ function priceLabel(source: PriceSource): string { * already cut out — leaving a cut-out photo with no control and no way back to * the original short of hand-editing the database. That breaks the invariant * this whole feature rests on: the original is always restorable. + * + * The rotation buttons are not gated on `enabled`. That flag is about the + * background-removal sidecar, and rotation has nothing to do with it — turning + * a photo is a local file operation that works in every environment, including + * one where REMBG_URL was never set. + * + * Icon-only, with an aria-label rather than visible text: three labelled + * buttons under a 120px thumbnail is more furniture than the photo, and a + * button with no text has no accessible name at all without one. */ function DraftPhoto({ image, @@ -65,8 +76,19 @@ function DraftPhoto({ onChanged: () => void; }>) { const [busy, setBusy] = useState(false); + const [turning, setTurning] = useState(false); const cutOut = image.original_image_path !== null; + // Rotation does not change image_path, so after a successful turn the src is + // byte-for-byte the string the browser already holds a copy for, and the + // photo appears not to have moved. express.static is mounted with no maxAge + // and would serve the new bytes on a full page reload — but nothing in a + // session asks it to. This is what makes the button visibly do something. No + // column and no server change: the file's identity has not changed, only this + // page's need to see it again. + const [version, setVersion] = useState(0); + const src = version === 0 ? image.image_path : `${image.image_path}?v=${version}`; + const act = async () => { setBusy(true); try { @@ -79,9 +101,39 @@ function DraftPhoto({ } }; + // No onChanged(): rotation changes nothing in the queue payload, so + // refetching it would be a request that returns exactly what is on screen. + const turn = async (direction: RotateDirection) => { + setTurning(true); + try { + await rotateImage(itemId, image.id, direction); + setVersion(Date.now()); + } catch (err) { + message.error(err instanceof Error ? err.message : 'that did not work'); + } finally { + setTurning(false); + } + }; + return ( - + + +