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');
|
||||
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
|
||||
// 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.
|
||||
const needsUrl = state.isOwner !== false && !state.instanceConfigured;
|
||||
|
||||
const save = async () => {
|
||||
const save = async (ev?: React.FormEvent) => {
|
||||
ev?.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -92,7 +93,10 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
||||
};
|
||||
|
||||
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>
|
||||
<h2 className="text-base font-semibold">{state.unreadable ? 'Re-enter your token' : 'Connect Gitea'}</h2>
|
||||
<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">
|
||||
<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" />
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -157,26 +157,23 @@ export const GiteaConnection = ({ state, onSaved }: Props) => {
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={busy || (needsUrl ? !url.trim() : !state.tea?.hasToken && !token)}
|
||||
>
|
||||
<Button type="submit" disabled={busy || (needsUrl ? !url.trim() : !state.tea?.hasToken && !token)}>
|
||||
{busy ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : null}
|
||||
{needsUrl ? 'Continue' : state.configured ? 'Save' : 'Connect'}
|
||||
</Button>
|
||||
{/* Only offered when there is something to test. */}
|
||||
{state.tea?.hasToken ? (
|
||||
<Button variant="outline" onClick={test} disabled={busy}>
|
||||
<Button type="button" variant="outline" onClick={test} disabled={busy}>
|
||||
Test
|
||||
</Button>
|
||||
) : null}
|
||||
{/* Only once there is something to go back TO. */}
|
||||
{state.configured && !state.unreadable ? (
|
||||
<Button variant="outline" onClick={() => navigate('/gitea')} disabled={busy}>
|
||||
<Button type="button" variant="outline" onClick={() => navigate('/gitea')} disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user