diff --git a/frontend/src/admin/UploadLinks.tsx b/frontend/src/admin/UploadLinks.tsx index 30ecd5d..06efb82 100644 --- a/frontend/src/admin/UploadLinks.tsx +++ b/frontend/src/admin/UploadLinks.tsx @@ -14,6 +14,7 @@ const { Paragraph, Text } = Typography; interface UploadLink { id: number; label: string; + contact_email: string | null; revoked_at: string | null; submission_count: number; max_submissions: number | null; @@ -36,11 +37,16 @@ const DEFAULT_CAP = '25'; export default function UploadLinks() { const [links, setLinks] = useState([]); const [label, setLabel] = useState(''); + const [email, setEmail] = useState(''); const [cap, setCap] = useState(DEFAULT_CAP); const [unlimited, setUnlimited] = useState(false); // Held only in component state and shown once. A refresh loses it, which is // the honest behaviour: the server genuinely cannot produce it again. const [issued, setIssued] = useState(null); + // Whether the link that is currently on screen was actually emailed. Separate + // from `issued` so the one-time display of the token keeps working exactly as + // it did; this only adds a note beside it. + const [mailed, setMailed] = useState(null); const [error, setError] = useState(null); const [creating, setCreating] = useState(false); @@ -57,6 +63,7 @@ export default function UploadLinks() { async function create() { setCreating(true); setError(null); + setMailed(null); const res = await fetch('/api/admin/upload-links', { method: 'POST', @@ -66,6 +73,7 @@ export default function UploadLinks() { // that are not this screen. body: JSON.stringify({ label, + email, maxSubmissions: unlimited ? null : Number(cap) }) }); @@ -80,7 +88,9 @@ export default function UploadLinks() { const created = await res.json(); setIssued(created.url); + setMailed(created.mail.sent); setLabel(''); + setEmail(''); setCap(DEFAULT_CAP); setUnlimited(false); await load(); @@ -106,6 +116,13 @@ export default function UploadLinks() { onChange={(e) => setLabel(e.target.value)} style={{ width: 260 }} /> + setEmail(e.target.value)} + style={{ width: 260 }} + /> setUnlimited(e.target.checked)}> No limit - {error && } + {issued && mailed === false && ( + + )} + {issued && ( diff --git a/frontend/tests/e2e/admin-draft-queue.spec.ts b/frontend/tests/e2e/admin-draft-queue.spec.ts index 5fe3720..6cf6c67 100644 --- a/frontend/tests/e2e/admin-draft-queue.spec.ts +++ b/frontend/tests/e2e/admin-draft-queue.spec.ts @@ -14,7 +14,7 @@ let token: string; test.beforeAll(async ({ playwright }) => { const api = await createAdminContext(playwright); const res = await api.post('/api/admin/upload-links', { - data: { label: `Review queue spec ${RUN}` } + data: { label: `Review queue spec ${RUN}`, email: `draft-queue-${RUN}@example.com` } }); expect(res.status(), 'creating the upload link').toBe(201); token = (await res.json()).token; diff --git a/frontend/tests/e2e/admin-upload-links.spec.ts b/frontend/tests/e2e/admin-upload-links.spec.ts index 3317821..e80b8d3 100644 --- a/frontend/tests/e2e/admin-upload-links.spec.ts +++ b/frontend/tests/e2e/admin-upload-links.spec.ts @@ -17,6 +17,7 @@ test.describe('Managing upload links', () => { await page.getByRole('tab', { name: 'Upload links' }).click(); await page.getByLabel('Link label').fill(label); + await page.getByLabel('Contributor email').fill(`${uniqueSuffix()}@example.com`); await page.getByRole('button', { name: 'Create link' }).click(); // Shown exactly once. The server keeps only a digest, so there is no @@ -38,6 +39,7 @@ test.describe('Managing upload links', () => { await admin.goto(); await page.getByRole('tab', { name: 'Upload links' }).click(); await page.getByLabel('Link label').fill(label); + await page.getByLabel('Contributor email').fill(`${uniqueSuffix()}@example.com`); await page.getByRole('button', { name: 'Create link' }).click(); const row = page.getByRole('row', { name: new RegExp(label) }); @@ -61,6 +63,7 @@ test.describe('Managing upload links', () => { await admin.goto(); await page.getByRole('tab', { name: 'Upload links' }).click(); await page.getByLabel('Link label').fill(label); + await page.getByLabel('Contributor email').fill(`${uniqueSuffix()}@example.com`); await page.getByText('No limit').click(); await page.getByRole('button', { name: 'Create link' }).click(); @@ -69,4 +72,14 @@ test.describe('Managing upload links', () => { // A bare count rather than "0 of N". await expect(row.getByText('0 of', { exact: false })).toHaveCount(0); }); + + // The address is the point of the change: a link nobody can be sent is the + // thing this replaced. + test('will not create a link without an address', async ({ page, admin }) => { + await admin.goto(); + await page.getByRole('tab', { name: 'Upload links' }).click(); + await page.getByLabel('Link label').fill(`Nameless ${uniqueSuffix()}`); + + await expect(page.getByRole('button', { name: 'Create link' })).toBeDisabled(); + }); }); diff --git a/frontend/tests/e2e/intake-submit.spec.ts b/frontend/tests/e2e/intake-submit.spec.ts index 507131f..9ab45a4 100644 --- a/frontend/tests/e2e/intake-submit.spec.ts +++ b/frontend/tests/e2e/intake-submit.spec.ts @@ -16,7 +16,7 @@ test.beforeAll(async ({ playwright }) => { const api = await createAdminContext(playwright); const res = await api.post('/api/admin/upload-links', { - data: { label: `Intake spec ${RUN}` } + data: { label: `Intake spec ${RUN}`, email: `intake-${RUN}@example.com` } }); expect(res.status(), 'creating the upload link').toBe(201); token = (await res.json()).token;