Feature/260 email the upload link #292
@@ -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<UploadLink[]>([]);
|
||||
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<string | null>(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<boolean | null>(null);
|
||||
const [error, setError] = useState<string | null>(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 }}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Contributor email"
|
||||
placeholder="Where should the link be sent?"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
style={{ width: 260 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Max uses"
|
||||
aria-label="Maximum uses"
|
||||
@@ -117,13 +134,27 @@ export default function UploadLinks() {
|
||||
<Checkbox checked={unlimited} onChange={(e) => setUnlimited(e.target.checked)}>
|
||||
No limit
|
||||
</Checkbox>
|
||||
<Button type="primary" onClick={create} loading={creating} disabled={label.trim() === ''}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={create}
|
||||
loading={creating}
|
||||
disabled={label.trim() === '' || email.trim() === ''}
|
||||
>
|
||||
Create link
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{error && <Alert type="error" message={error} showIcon />}
|
||||
|
||||
{issued && mailed === false && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="The link was not emailed"
|
||||
description="Copy it below and send it yourself. This is normal where no mail is configured, and in QA, where delivery is restricted to a fixed list of addresses."
|
||||
/>
|
||||
)}
|
||||
|
||||
{issued && (
|
||||
<Alert
|
||||
type="success"
|
||||
@@ -151,6 +182,7 @@ export default function UploadLinks() {
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: 'Label', dataIndex: 'label' },
|
||||
{ title: 'Sent to', dataIndex: 'contact_email' },
|
||||
{
|
||||
title: 'Used',
|
||||
render: (_, row) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user