diff --git a/CHANGELOG.md b/CHANGELOG.md index 655b660..28b9f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ The latest release only. Every earlier version: - The inline picker's "more logins" footer is now a row that opens the Subclave popup, which lists the logins saved for other hosts of the domain. Where the browser cannot open the popup from the page, the footer names the Extensions menu and the fill shortcut instead. +### Fixed + +- The "Join a synced vault" screen scrolls as a whole and its form widens with the window, up to the Settings content width. No horizontal scrollbar appears. +- Join vault no longer stays disabled on a blank S3 Region, which Settings > Sync already saves. While the button is disabled, a list under it names every field that is still empty or invalid. + ## [0.1.1] - 06-10-2026 No change to the desktop app or to the extension's code. diff --git a/scripts/sync-verify.ts b/scripts/sync-verify.ts index 25a417f..97b79f5 100644 --- a/scripts/sync-verify.ts +++ b/scripts/sync-verify.ts @@ -31,9 +31,9 @@ import type { SyncStatus, } from "@/modules/sync/types"; import { - connectionFieldsReady, DEFAULT_SYNC_CONFIG, EMPTY_SYNC_STATUS, + missingConnectionFields, } from "@/modules/sync/types"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -486,9 +486,9 @@ check( !syncStore.includes("readDirty"), ); -// The join form's gate. Its whole value is that it is a checked predicate -// rather than a condition inside a component: a flipped one makes a provider -// permanently unsubmittable while the form still looks complete. +// The join form's connection gate. Its whole value is that it is a checked +// function rather than a condition inside a component: a field wrongly listed +// makes the join form refuse a connection Settings > Sync saves. { const base: SyncConfig = { enabled: true, @@ -500,19 +500,26 @@ check( cas: true, }; const webdav: SyncConfig = { ...base, provider: "webdav", region: "", bucket: "" }; - check("a webdav connection needs no region and no bucket", connectionFieldsReady(webdav, true)); + const missing = (config: SyncConfig): string => missingConnectionFields(config).join(", "); check( - "a webdav connection still needs its own credential pair", - !connectionFieldsReady(webdav, false), + "a webdav connection needs no region and no bucket", + missing(webdav) === "", + missing(webdav), ); - check("an s3 connection needs a region and a bucket", connectionFieldsReady(base, true)); + check("a complete s3 connection is ready", missing(base) === "", missing(base)); + const blankRegion = missing({ ...base, region: "" }); check( - "an s3 connection without a bucket is refused", - !connectionFieldsReady({ ...base, bucket: "" }, true), + "an s3 connection with a blank region is ready, as Settings > Sync saves one", + blankRegion === "", + blankRegion, ); + const blankBucket = missing({ ...base, bucket: " " }); + check("an s3 connection without a bucket names Bucket", blankBucket === "Bucket", blankBucket); + const blankEndpoint = missing({ ...webdav, endpoint: " " }); check( - "a connection with no endpoint is refused", - !connectionFieldsReady({ ...webdav, endpoint: " " }, true), + "a connection with no endpoint names Endpoint", + blankEndpoint === "Endpoint", + blankEndpoint, ); } diff --git a/src-tauri/src/modules/sync/engine/types.rs b/src-tauri/src/modules/sync/engine/types.rs index a28649e..fc5076f 100644 --- a/src-tauri/src/modules/sync/engine/types.rs +++ b/src-tauri/src/modules/sync/engine/types.rs @@ -135,9 +135,10 @@ pub struct SyncConfigArg { pub provider: String, pub endpoint: String, /// S3's alone. `#[serde(default)]` so a WebDAV caller does not have to send - /// an S3 field `provider_config` ignores. The S3 form still requires it - /// (`connectionFieldsReady` in `src/modules/sync/types.ts`) and - /// `provider_config` still puts it into the JSON `S3Config` deserializes. + /// an S3 field `provider_config` ignores. Neither form requires it: a server + /// that takes an empty region (Cloudflare R2 reads one as `auto`) works with + /// it blank, and `provider_config` puts it as typed into the JSON `S3Config` + /// deserializes. #[serde(default)] pub region: String, /// S3's alone, by the same default as the `region` field. diff --git a/src/modules/sync/types.ts b/src/modules/sync/types.ts index 34affb8..df93104 100644 --- a/src/modules/sync/types.ts +++ b/src/modules/sync/types.ts @@ -131,22 +131,26 @@ export function configSubset(config: SyncConfig): SyncConfigArg { } /** - * Whether the fields a provider actually needs are filled in. + * The connection fields a provider needs that are still blank, by the label the + * join form shows; empty when the connection is complete. * - * SHARED WITH `scripts/sync-verify.ts`, and that is the point: this is the one - * boolean in the join form that can make a whole provider unusable while the - * form still looks complete, so it lives where a check can drive it rather than - * inside a component. + * SHARED WITH `scripts/sync-verify.ts`, and that is the point: a field wrongly + * listed here makes a whole provider unusable from the join form while Settings + * > Sync, which has no such check, saves the same values. * - * REGION AND BUCKET ARE S3'S ALONE. A WebDAV configuration refuses unknown - * fields, so demanding them for a WebDAV server would make the form impossible - * to submit. + * REGION IS NEVER LISTED. `provider_config` in + * `src-tauri/src/modules/sync/engine/commands.rs` passes it through as typed, a + * server that takes an empty one (Cloudflare R2 reads it as `auto`) works with + * it blank, and Settings > Sync saves it blank. + * + * BUCKET IS S3'S ALONE. A WebDAV configuration refuses unknown fields, so + * demanding one for a WebDAV server would make the form impossible to submit. */ -export function connectionFieldsReady(config: SyncConfig, credentialsReady: boolean): boolean { - if (config.endpoint.trim().length === 0) return false; - if (!credentialsReady) return false; - if (config.provider === "webdav") return true; - return config.region.trim().length > 0 && config.bucket.trim().length > 0; +export function missingConnectionFields(config: SyncConfig): string[] { + const missing: string[] = []; + if (config.endpoint.trim().length === 0) missing.push("Endpoint"); + if (config.provider !== "webdav" && config.bucket.trim().length === 0) missing.push("Bucket"); + return missing; } /** A device with sync never configured. Off, and naming nothing. */ diff --git a/src/modules/vault/JoinSyncScreen.tsx b/src/modules/vault/JoinSyncScreen.tsx index 6ec178e..2bd1dd8 100644 --- a/src/modules/vault/JoinSyncScreen.tsx +++ b/src/modules/vault/JoinSyncScreen.tsx @@ -24,20 +24,21 @@ export function JoinSyncScreen({ onCreateInstead }: { onCreateInstead: () => voi const [passphrase, setPassphrase] = useState(""); const masterPassword = useMasterPassword(); - const { submit, busy, error, fresh, canSubmit } = useJoinSync({ + const { submit, busy, error, fresh, canSubmit, blockers } = useJoinSync({ config, credentials, passphrase, password: masterPassword.password, - masterPasswordReady: masterPassword.ready, + masterPasswordProblems: masterPassword.problems, }); + // The whole screen scrolls, not the form, so the wheel works anywhere on it. + // `m-auto` centres a short form and collapses to 0 on a tall one, which keeps + // its top reachable. `max-w-3xl` is the Settings window's content width, so + // the rows shared with Settings > Sync wrap the same in both. return ( -