Files
nxdns/web/src/features/blocklists/BlocklistForm.tsx
T

147 lines
3.7 KiB
TypeScript

import { useState, type FormEvent } from "react";
import * as stylex from "@stylexjs/stylex";
import { ApiError } from "@/lib/api";
import InlineError from "@/lib/InlineError";
import type { Blocklist, BlocklistInput } from "@/lib/types";
import { styles as shared } from "@/ui/styles";
import { READ_ONLY_HINT } from "@/features/settings/authority";
const styles = stylex.create({
form: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
marginTop: "1rem",
maxWidth: "36rem",
},
heading: {
fontSize: "1.125rem",
lineHeight: "1.75rem",
fontWeight: 500,
},
fieldLabel: {
display: "block",
fontSize: "0.875rem",
lineHeight: "1.25rem",
fontWeight: 500,
},
checkboxLabel: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
fontWeight: 500,
},
buttonRow: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
cancel: {
fontWeight: 500,
},
});
/**
* Drops the rejection the page already renders inline below the form. Anything
* else is a bug in this component and must reach the console instead of dying
* silently in the submit handler.
*/
export function swallowMutationError(error: unknown): void {
if (error instanceof ApiError) return;
throw error;
}
interface BlocklistFormProps {
initial?: Blocklist;
busy: boolean;
/** File authority: the server answers 403, so the submit stays down. */
readOnly: boolean;
error: Error | null;
onSubmit: (input: BlocklistInput) => Promise<void>;
onCancel?: () => void;
}
export default function BlocklistForm({ initial, busy, readOnly, error, onSubmit, onCancel }: BlocklistFormProps) {
const [url, setUrl] = useState(initial?.url ?? "");
const [name, setName] = useState(initial?.name ?? "");
const [enabled, setEnabled] = useState(initial?.enabled ?? true);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
try {
await onSubmit({ url: url.trim(), name: name.trim(), enabled });
} catch (error) {
swallowMutationError(error);
return;
}
if (initial === undefined) {
setUrl("");
setName("");
setEnabled(true);
}
}
return (
<form onSubmit={handleSubmit} {...stylex.props(styles.form)}>
<h2 {...stylex.props(styles.heading)}>{initial === undefined ? "Add source" : `Edit ${initial.name}`}</h2>
<div>
<label htmlFor="blocklist-url" {...stylex.props(styles.fieldLabel)}>
URL
</label>
<input
id="blocklist-url"
type="url"
required
value={url}
onChange={(event) => setUrl(event.target.value)}
{...stylex.props(shared.input, shared.focusRing)}
/>
</div>
<div>
<label htmlFor="blocklist-name" {...stylex.props(styles.fieldLabel)}>
Name
</label>
<input
id="blocklist-name"
type="text"
required
value={name}
onChange={(event) => setName(event.target.value)}
{...stylex.props(shared.input, shared.focusRing)}
/>
</div>
<label {...stylex.props(styles.checkboxLabel)}>
<input
type="checkbox"
checked={enabled}
onChange={(event) => setEnabled(event.target.checked)}
{...stylex.props(shared.focusRing)}
/>
Enabled
</label>
<div {...stylex.props(styles.buttonRow)}>
<button
type="submit"
disabled={busy || readOnly}
title={readOnly ? READ_ONLY_HINT : undefined}
{...stylex.props(shared.primaryButton, shared.focusRing)}
>
{initial === undefined ? "Add source" : "Save changes"}
</button>
{onCancel !== undefined && (
<button
type="button"
onClick={onCancel}
{...stylex.props(shared.button, styles.cancel, shared.focusRing)}
>
Cancel
</button>
)}
</div>
<InlineError error={error} />
</form>
);
}