feat(admin): ask for the contributor's address when creating a link (#260)
The address is now a required field beside the label, the links table shows where each link was sent, and the admin is told plainly when the mail did not go — with the link still on screen to copy, which is the case that matters in QA and in local development where there is no mail at all. Two specs in unrelated features created links with only a label and the route now refuses that, so they are updated here rather than left to go red on somebody else's branch. That is the cost of making the address required, and it is a small one: the compiler and the suite find every call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user