feat(settings): edit a saved sync target (fix URL/username/options)

Each webdav target gets an Edit button that loads it into the form; save routes
to update_sync_target. Password left blank keeps the stored one; kind is locked
during edit. Fixes not being able to correct a mistyped Server URL.
This commit is contained in:
iamdoubz committed 2026-07-06 10:41:02 -05:00
1 parent 496a3372c4
commit fb790091fd
1 file changed
+62 -15
+62 -15
View File
@@ -8,7 +8,7 @@
import ConsentNotice from "../components/ConsentNotice.svelte"; import ConsentNotice from "../components/ConsentNotice.svelte";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { api, errorMessage, events } from "../api"; import { api, errorMessage, events } from "../api";
import type { BackendId, SyncKind, SyncTargetConfig } from "../api"; import type { BackendId, SyncKind, SyncTargetConfig, SyncTargetInfo } from "../api";
import { trapFocus } from "../actions/trapFocus"; import { trapFocus } from "../actions/trapFocus";
import { import {
X, X,
@@ -343,6 +343,8 @@
// ---- Add-target form ---- // ---- Add-target form ----
const PROVIDERS = ["nextcloud", "owncloud", "cloudreve", "seafile", "synology", "generic"]; const PROVIDERS = ["nextcloud", "owncloud", "cloudreve", "seafile", "synology", "generic"];
let form = $state<SyncTargetConfig>(blankForm("webdav")); let form = $state<SyncTargetConfig>(blankForm("webdav"));
// When set, the form is editing an existing target rather than adding one.
let editingId = $state<string | null>(null);
function blankForm(kind: SyncKind): SyncTargetConfig { function blankForm(kind: SyncKind): SyncTargetConfig {
return { return {
@@ -371,11 +373,46 @@
const r = await settings.test(form); const r = await settings.test(form);
testResult = { ok: r.ok, message: r.message }; testResult = { ok: r.ok, message: r.message };
} }
async function addTarget() { // Load an existing target into the form for editing. The secret is never
await settings.addTarget(form); // returned to the UI (FR-SYNC-6), so it stays blank — left blank on save it
form = blankForm(form.kind); // keeps the stored password; typed, it rotates. No `id` on the form itself, so
// "Test connection" checks the typed values (needs the password re-entered).
function startEdit(t: SyncTargetInfo) {
editingId = t.id;
form = {
name: t.name,
kind: t.kind,
provider_hint: t.provider_hint ?? undefined,
base_url: t.base_url ?? "",
remote_base_path: t.remote_base_path,
username: t.username ?? "",
secret: "",
enabled: t.enabled,
upload_transcript: t.upload_transcript,
upload_notes: t.upload_notes,
upload_summary: t.upload_summary,
upload_recording: t.upload_recording,
trigger_on_finalize: t.trigger_on_finalize,
allow_plaintext_lan: t.allow_plaintext_lan,
encrypt_before_upload: t.encrypt_before_upload,
};
testResult = null; testResult = null;
} }
function cancelEdit() {
editingId = null;
form = blankForm("webdav");
testResult = null;
}
async function saveTarget() {
if (editingId) {
await settings.updateTarget({ ...form, id: editingId });
cancelEdit();
} else {
await settings.addTarget(form);
form = blankForm(form.kind);
testResult = null;
}
}
function linkOAuth() { function linkOAuth() {
// begin_oauth_link → loopback PKCE flow; the target is created in the // begin_oauth_link → loopback PKCE flow; the target is created in the
// background on success (sync://linked), which the store handles. // background on success (sync://linked), which the store handles.
@@ -711,6 +748,9 @@
>{t.third_party ? "third-party" : "your server"}</span >{t.third_party ? "third-party" : "your server"}</span
> >
<span class="host">{t.host ?? t.kind}</span> <span class="host">{t.host ?? t.kind}</span>
{#if !t.third_party}
<button class="link" onclick={() => startEdit(t)}>Edit</button>
{/if}
<button class="link danger" onclick={() => settings.removeTarget(t.id)} <button class="link danger" onclick={() => settings.removeTarget(t.id)}
>Remove</button >Remove</button
> >
@@ -719,14 +759,16 @@
</ul> </ul>
{/if} {/if}
<h4>Add a target</h4> <h4>{editingId ? "Edit target" : "Add a target"}</h4>
<div class="kinds"> {#if !editingId}
{#each ["webdav", "onedrive", "dropbox", "box"] as k (k)} <div class="kinds">
<button class:active={form.kind === k} onclick={() => setKind(k as SyncKind)} {#each ["webdav", "onedrive", "dropbox", "box"] as k (k)}
>{k}</button <button class:active={form.kind === k} onclick={() => setKind(k as SyncKind)}
> >{k}</button
{/each} >
</div> {/each}
</div>
{/if}
{#if form.kind === "webdav"} {#if form.kind === "webdav"}
<p class="muted"> <p class="muted">
@@ -753,7 +795,9 @@
>App password<input >App password<input
type="password" type="password"
bind:value={form.secret} bind:value={form.secret}
placeholder="stored in OS credential store" placeholder={editingId
? "leave blank to keep current password"
: "stored in OS credential store"}
/></label /></label
> >
</div> </div>
@@ -782,9 +826,12 @@
</label> </label>
<div class="actions"> <div class="actions">
<button onclick={testConnection}>Test connection</button> <button onclick={testConnection}>Test connection</button>
<button class="primary" onclick={addTarget} disabled={!form.name || !form.base_url} <button class="primary" onclick={saveTarget} disabled={!form.name || !form.base_url}
>Add target</button >{editingId ? "Save changes" : "Add target"}</button
> >
{#if editingId}
<button class="link" onclick={cancelEdit}>Cancel</button>
{/if}
{#if testResult} {#if testResult}
<span class="test" class:ok={testResult.ok} class:fail={!testResult.ok}> <span class="test" class:ok={testResult.ok} class:fail={!testResult.ok}>
{#if testResult.ok}<Check size={14} aria-hidden="true" />{:else}<X {#if testResult.ok}<Check size={14} aria-hidden="true" />{:else}<X