Feature/281 background removal plan #284
@@ -8,6 +8,7 @@ import Input from 'antd/es/input';
|
|||||||
import Alert from 'antd/es/alert';
|
import Alert from 'antd/es/alert';
|
||||||
import Spin from 'antd/es/spin';
|
import Spin from 'antd/es/spin';
|
||||||
import Space from 'antd/es/space';
|
import Space from 'antd/es/space';
|
||||||
|
import Checkbox from 'antd/es/checkbox';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import { fetchIntakeLink, submitItem } from './intakeApi';
|
import { fetchIntakeLink, submitItem } from './intakeApi';
|
||||||
@@ -33,6 +34,10 @@ export default function Submit() {
|
|||||||
const [checking, setChecking] = useState(true);
|
const [checking, setChecking] = useState(true);
|
||||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||||
const [note, setNote] = useState('');
|
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 [sending, setSending] = useState(false);
|
||||||
const [sent, setSent] = useState(false);
|
const [sent, setSent] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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
|
// rather than map-then-filter because a type predicate cannot narrow to
|
||||||
// File here — antd's RcFile extends it, so the predicate would widen.
|
// File here — antd's RcFile extends it, so the predicate would widen.
|
||||||
files.flatMap((f) => (f.originFileObj ? [f.originFileObj] : [])),
|
files.flatMap((f) => (f.originFileObj ? [f.originFileObj] : [])),
|
||||||
note
|
note,
|
||||||
|
removeBackground
|
||||||
);
|
);
|
||||||
|
|
||||||
setSending(false);
|
setSending(false);
|
||||||
@@ -129,6 +135,7 @@ export default function Submit() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setNote('');
|
setNote('');
|
||||||
|
setRemoveBackground(true);
|
||||||
setSent(false);
|
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."
|
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 />}
|
{error && <Alert type="error" message={error} showIcon />}
|
||||||
|
|
||||||
<Button type="primary" onClick={send} loading={sending} disabled={files.length === 0}>
|
<Button type="primary" onClick={send} loading={sending} disabled={files.length === 0}>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export interface IntakeLink {
|
export interface IntakeLink {
|
||||||
label: string;
|
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 =
|
export type LinkState =
|
||||||
@@ -36,13 +42,18 @@ export type SubmitResult = { ok: true } | { ok: false; error: string };
|
|||||||
export async function submitItem(
|
export async function submitItem(
|
||||||
token: string,
|
token: string,
|
||||||
files: File[],
|
files: File[],
|
||||||
note: string
|
note: string,
|
||||||
|
removeBackground: boolean
|
||||||
): Promise<SubmitResult> {
|
): Promise<SubmitResult> {
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
// The field name the server's multer instance listens on. Sending several
|
// The field name the server's multer instance listens on. Sending several
|
||||||
// under one name is what makes req.files an array.
|
// under one name is what makes req.files an array.
|
||||||
for (const file of files) body.append('images', file);
|
for (const file of files) body.append('images', file);
|
||||||
body.append('note', note);
|
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)}`, {
|
const res = await fetch(`/api/intake/${encodeURIComponent(token)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -73,4 +73,32 @@ test.describe('Sending in an item through a link', () => {
|
|||||||
// is not on the site.
|
// is not on the site.
|
||||||
await expect(page.getByText(/Nothing is listed for sale until/)).toBeVisible();
|
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
|
# No PayPal credentials locally. DEMO_MODE lets the whole cart and checkout
|
||||||
# path run without them and with no way to reach live PayPal.
|
# path run without them and with no way to reach live PayPal.
|
||||||
$env:DEMO_MODE = 'true'
|
$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
|
New-Item -ItemType Directory -Force -Path $env:UPLOADS_DIR *>$null
|
||||||
|
|
||||||
Write-Step 'Running migrations'
|
Write-Step 'Running migrations'
|
||||||
|
|||||||
Reference in New Issue
Block a user