a real form, and the URL step no longer demands a token
Two things reported from a production instance. The URL step was refused with "an access token is required the first time" — a check that predates splitting the screen into two steps, and which made the first step impossible to complete. Naming the instance is now its own step; a member still needs a token, since setting one is all they do. And the fields were a div, so pressing Enter did nothing and the button was the only way to submit. That is a keyboard trap wearing a layout. Now a <form> with onSubmit, with the other two buttons marked type="button" so they do not submit it. 19 panel files across the platform have the same problem. Recorded in the platform's TODO.md rather than swept blindly — a search box has no submit, and a multi-field editor wants the useForm hook. Also dropped the hint under the URL field, which repeated the sentence directly above it.
This commit is contained in:
+8
-1
@@ -198,7 +198,14 @@ async function handleConfig(req: Request, userId: number): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existing = await getServiceConnection(userId, 'gitea');
|
const existing = await getServiceConnection(userId, 'gitea');
|
||||||
if (!token && !existing?.hasSecret) return bad('an access token is required the first time');
|
// NOT required on the first save any more. Naming the instance is its own step — the owner says
|
||||||
|
// where Gitea is, and the token is asked for (or found in tea's config) afterwards. This check
|
||||||
|
// predates that split and refused the URL step outright.
|
||||||
|
//
|
||||||
|
// A member is different: they set nothing but a token, so an empty save from them means nothing.
|
||||||
|
if (!owner && !token && !(await readTeaLogin(userId, base))?.hasToken) {
|
||||||
|
return bad('an access token is required');
|
||||||
|
}
|
||||||
|
|
||||||
// Probe with the credentials as they WILL be, so a save either stores something that works or
|
// Probe with the credentials as they WILL be, so a save either stores something that works or
|
||||||
// fails with the reason. Storing first and discovering later makes every downstream screen fail
|
// fails with the reason. Storing first and discovering later makes every downstream screen fail
|
||||||
|
|||||||
+10
-13
@@ -75,7 +75,8 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
|||||||
// Members skip step one entirely: the instance is the owner's.
|
// Members skip step one entirely: the instance is the owner's.
|
||||||
const needsUrl = state.isOwner !== false && !state.instanceConfigured;
|
const needsUrl = state.isOwner !== false && !state.instanceConfigured;
|
||||||
|
|
||||||
const save = async () => {
|
const save = async (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -92,7 +93,10 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex h-full max-w-md flex-col justify-center gap-4 p-6">
|
// A real <form>, so Enter submits. Typing a URL and pressing return is what anyone does, and a div
|
||||||
|
// full of inputs silently swallows it — the button becomes the only way in, which is a keyboard trap
|
||||||
|
// wearing a layout.
|
||||||
|
<form onSubmit={save} className="mx-auto flex h-full max-w-md flex-col justify-center gap-4 p-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold">{state.unreadable ? 'Re-enter your token' : 'Connect Gitea'}</h2>
|
<h2 className="text-base font-semibold">{state.unreadable ? 'Re-enter your token' : 'Connect Gitea'}</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
@@ -110,10 +114,6 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
|||||||
<label className="space-y-1.5">
|
<label className="space-y-1.5">
|
||||||
<span className="text-xs font-medium text-muted-foreground">Instance URL</span>
|
<span className="text-xs font-medium text-muted-foreground">Instance URL</span>
|
||||||
<Input value={url} onChange={(ev) => setUrl(ev.target.value)} placeholder="http://localhost:3000" />
|
<Input value={url} onChange={(ev) => setUrl(ev.target.value)} placeholder="http://localhost:3000" />
|
||||||
<span className="block text-xs text-muted-foreground">
|
|
||||||
Officer reaches this from the server, not from your browser — so localhost means the machine
|
|
||||||
Officer runs on.
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -157,26 +157,23 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
|||||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button type="submit" disabled={busy || (needsUrl ? !url.trim() : !state.tea?.hasToken && !token)}>
|
||||||
onClick={save}
|
|
||||||
disabled={busy || (needsUrl ? !url.trim() : !state.tea?.hasToken && !token)}
|
|
||||||
>
|
|
||||||
{busy ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : null}
|
{busy ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : null}
|
||||||
{needsUrl ? 'Continue' : state.configured ? 'Save' : 'Connect'}
|
{needsUrl ? 'Continue' : state.configured ? 'Save' : 'Connect'}
|
||||||
</Button>
|
</Button>
|
||||||
{/* Only offered when there is something to test. */}
|
{/* Only offered when there is something to test. */}
|
||||||
{state.tea?.hasToken ? (
|
{state.tea?.hasToken ? (
|
||||||
<Button variant="outline" onClick={test} disabled={busy}>
|
<Button type="button" variant="outline" onClick={test} disabled={busy}>
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Only once there is something to go back TO. */}
|
{/* Only once there is something to go back TO. */}
|
||||||
{state.configured && !state.unreadable ? (
|
{state.configured && !state.unreadable ? (
|
||||||
<Button variant="outline" onClick={() => navigate('/gitea')} disabled={busy}>
|
<Button type="button" variant="outline" onClick={() => navigate('/gitea')} disabled={busy}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user