Feature/281 background removal plan #284
@@ -8,6 +8,7 @@ import Input from 'antd/es/input';
|
||||
import Alert from 'antd/es/alert';
|
||||
import Spin from 'antd/es/spin';
|
||||
import Space from 'antd/es/space';
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { fetchIntakeLink, submitItem } from './intakeApi';
|
||||
@@ -33,6 +34,10 @@ export default function Submit() {
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||
const [note, setNote] = useState('');
|
||||
// Ticked by default. Most items look better cut out, and a submitter who
|
||||
// wants their kitchen table in the photograph can say so — the reverse
|
||||
// default would mean almost nobody got it.
|
||||
const [removeBackground, setRemoveBackground] = useState(true);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -62,7 +67,8 @@ export default function Submit() {
|
||||
// rather than map-then-filter because a type predicate cannot narrow to
|
||||
// File here — antd's RcFile extends it, so the predicate would widen.
|
||||
files.flatMap((f) => (f.originFileObj ? [f.originFileObj] : [])),
|
||||
note
|
||||
note,
|
||||
removeBackground
|
||||
);
|
||||
|
||||
setSending(false);
|
||||
@@ -129,6 +135,7 @@ export default function Submit() {
|
||||
onClick={() => {
|
||||
setFiles([]);
|
||||
setNote('');
|
||||
setRemoveBackground(true);
|
||||
setSent(false);
|
||||
}}
|
||||
>
|
||||
@@ -171,6 +178,17 @@ export default function Submit() {
|
||||
placeholder="What is it, what is it made of, how big, what condition, where did it come from? Anything you know helps — a photo cannot show any of it."
|
||||
/>
|
||||
|
||||
{state.kind === 'usable' && state.link.backgroundRemoval && (
|
||||
<Checkbox
|
||||
checked={removeBackground}
|
||||
onChange={(e) => setRemoveBackground(e.target.checked)}
|
||||
>
|
||||
{/* Described by what it does, not by how. Nobody sending in a
|
||||
vase knows what a cut-out or an alpha channel is. */}
|
||||
Remove the background from my photos
|
||||
</Checkbox>
|
||||
)}
|
||||
|
||||
{error && <Alert type="error" message={error} showIcon />}
|
||||
|
||||
<Button type="primary" onClick={send} loading={sending} disabled={files.length === 0}>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export interface IntakeLink {
|
||||
label: string;
|
||||
/**
|
||||
* Whether there is a background-removal sidecar behind the checkbox. False
|
||||
* hides it entirely rather than showing a control that would do nothing —
|
||||
* an unconfigured environment is a working one, not a broken one.
|
||||
*/
|
||||
backgroundRemoval: boolean;
|
||||
}
|
||||
|
||||
export type LinkState =
|
||||
@@ -36,13 +42,18 @@ export type SubmitResult = { ok: true } | { ok: false; error: string };
|
||||
export async function submitItem(
|
||||
token: string,
|
||||
files: File[],
|
||||
note: string
|
||||
note: string,
|
||||
removeBackground: boolean
|
||||
): Promise<SubmitResult> {
|
||||
const body = new FormData();
|
||||
// The field name the server's multer instance listens on. Sending several
|
||||
// under one name is what makes req.files an array.
|
||||
for (const file of files) body.append('images', file);
|
||||
body.append('note', note);
|
||||
// A string, because that is all a multipart field can be. The server treats
|
||||
// only the exact 'false' as an opt-out, so this is the one value that has to
|
||||
// be got right.
|
||||
body.append('removeBackground', removeBackground ? 'true' : 'false');
|
||||
|
||||
const res = await fetch(`/api/intake/${encodeURIComponent(token)}`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -73,4 +73,32 @@ test.describe('Sending in an item through a link', () => {
|
||||
// is not on the site.
|
||||
await expect(page.getByText(/Nothing is listed for sale until/)).toBeVisible();
|
||||
});
|
||||
|
||||
// Ticked by default, because that is the decision: most items look better
|
||||
// cut out, and the reverse default would mean almost nobody got it.
|
||||
test('offers to remove the background, already ticked', async ({ page }) => {
|
||||
await page.goto(`/submit/${token}`);
|
||||
|
||||
const control = page.getByRole('checkbox', { name: /remove the background/i });
|
||||
await expect(control).toBeVisible();
|
||||
await expect(control).toBeChecked();
|
||||
});
|
||||
|
||||
test('lets a sender turn it off and still send', async ({ page }) => {
|
||||
await page.goto(`/submit/${token}`);
|
||||
|
||||
const png = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64'
|
||||
);
|
||||
await page.setInputFiles('input[type="file"]', {
|
||||
name: `${RUN}-nobg.png`,
|
||||
mimeType: 'image/png',
|
||||
buffer: png
|
||||
});
|
||||
await page.getByRole('checkbox', { name: /remove the background/i }).uncheck();
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Thank you — it arrived' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,6 +270,11 @@ function Start-Backend {
|
||||
# No PayPal credentials locally. DEMO_MODE lets the whole cart and checkout
|
||||
# path run without them and with no way to reach live PayPal.
|
||||
$env:DEMO_MODE = 'true'
|
||||
# Needs to be set for the submission page's checkbox to appear at all, but
|
||||
# not to resolve: no e2e submission reaches the sidecar, because the
|
||||
# worker only cuts out after a draft and drafting is not configured
|
||||
# locally.
|
||||
$env:REMBG_URL = 'http://127.0.0.1:7000'
|
||||
New-Item -ItemType Directory -Force -Path $env:UPLOADS_DIR *>$null
|
||||
|
||||
Write-Step 'Running migrations'
|
||||
|
||||
Reference in New Issue
Block a user