Files
webstudio/scripts/fetch-backed-form-request.ts
Oleg Isonen 9a8ae2dc8f feat: Customize Form progress feedback (#6606)
Closes #6605

New Forms show editable feedback while Resource actions run. The
`formProgress` variable starts with `status: "idle"`, `loaded: 0`,
`total: 0`, and `files: []`. It becomes pending during submission, then
returns to idle. The existing `formState` still reports only initial,
success, or error.

The Progress container says “Submitting…” and adds measured
whole-request bytes when the total is known. Files include ID, field
name, name, size, and type. The browser cannot measure reliable per-file
transfer bytes in the single multipart request. The binding editor
suggests one example file to reveal those fields; the example never
enters the actual Form value. Saved Forms retain their existing bindings
and submission message.

## Checklist

- [x] Keep the existing Form outcome variable and add upload activity,
byte counts, and file details.
- [x] Show zero byte defaults on insertion, new submissions,
cancellation, and Preview reset without fake runtime files.
- [x] Suggest file fields in Form and Resource bindings while
evaluation, reset, rendering, and stored values use real data.
- [x] Preserve Builder Preview headers, authentication, response checks,
and cancellation.
- [x] Keep existing success/error feedback and saved-Form fallback.
- [x] Reset external progress when an active published Form unmounts.
- [x] Update Form, Variables, and MCP/CLI guides; clarify the older
screenshot and final-only simulation. Generated MCP docs check passes.
- [x] Audit Form and Variables docs against final progress behavior;
clarify the older Variables screenshot.
- [x] Cover lifecycle, transport, repeated submission, cancellation,
unmount, Preview, template, MCP binding/style, and editor completion
with focused tests.
- [x] Run relevant typechecks, lint, formatting, fixture generation, and
independent review.
- [x] Required CI checks pass on the latest commit.

## Verification

Local focused Form browser tests (25), Form template tests (12), Builder
Preview and Form context tests (14), and mounted editor example/reset
tests (6) pass. Builder, SDK React, SDK registry, and project-build
typechecks pass, as do lint, formatting, and fixture generation. The
fixture sync regenerated two unrelated linked-project site paths; those
outputs were restored before commit. Independent review found no
remaining issues. All latest-commit checks passed; E2E shard 9 passed on
a targeted rerun after its two Form tests also passed locally with 11
related tests.

This follow-up adds 162 net non-test lines, more than the initial
estimate. The extra code identifies the actual callback-bound variable
by ID, even when renamed, and gives autocomplete a separate example
scope so it cannot affect actual values. One provider covers Form fields
and Resource editors. No schema or package was added.

The existing Variables screenshot predates `formProgress`; its
explanatory text is current. The React Router fixture's built browser
client grew by 4 KiB on Linux CI (376 → 380 KiB), and its checked limit
matches that measured output. Multipart transfer remains one whole
request.

Browser submission supplies live progress. MCP can discover
`formProgress`, bind the Progress container, and style it; server
simulation reports final outcomes and does not measure browser uploads.
2026-10-26 14:38:12 +01:00

59 lines
1.4 KiB
TypeScript

/** Lets router and Remix tests keep their existing fetch-backed endpoint fakes. */
export class FetchBackedFormRequest {
upload: { onload?: (event: ProgressEvent) => void } = {};
onload?: () => void;
onerror?: () => void;
onabort?: () => void;
status = 0;
responseText = "";
private url = "";
private headers = new Headers();
private response?: Response;
private controller = new AbortController();
open(_method: string, url: string) {
this.url = url;
}
setRequestHeader(name: string, value: string) {
this.headers.set(name, value);
}
getResponseHeader(name: string) {
return this.response?.headers.get(name) ?? null;
}
send(body: FormData) {
fetch(this.url, {
method: "POST",
body,
headers: this.headers,
signal: this.controller.signal,
})
.then(async (response) => {
if (this.controller.signal.aborted) {
return;
}
this.upload.onload?.({
loaded: 0,
total: 0,
lengthComputable: false,
} as ProgressEvent);
this.response = response;
this.status = response.status;
this.responseText = await response.text();
this.onload?.();
})
.catch(() => {
if (!this.controller.signal.aborted) {
this.onerror?.();
}
});
}
abort() {
this.controller.abort();
this.onabort?.();
}
}