repos: sidebar actions (sync/edit/remove) + edit modal
Sidebar: - Each connection header now has three inline icon buttons: Sync now (spins while in flight), Edit (opens the modal), Remove (opens an inline confirm strip). Removes cascade repos via ON DELETE CASCADE. - The connection's last_sync_error surfaces as a red inline banner under the header — no more 'error status with nowhere to see why'. - Sync is POST /api/repos/connections/:id/sync (already existed); after either sync or delete the sidebar re-fetches so state stays consistent. Edit modal (RepoConnectionEditModal): - Loads GET /api/repos/connections/:id, pre-fills owner/base_url/label - PATCHes only the fields that actually changed; empty string on a Some(&str) field sends explicit null so the backend clears it - Sync-now + Remove reachable from inside the modal too - Rotating the token is out of scope: the modal says as much and points the user at delete + re-create through the wizard (the broker doesn't expose an update path, and rotating in place would require duplicating the whole broker->store_secret flow here) Backend: - GET /api/repos/connections/:id — same ConnectionSummary shape - PATCH /api/repos/connections/:id — owner/base_url use Option<Option<T>> double-nesting so 'omit = leave alone' and 'null = clear' round-trip distinctly through serde - repo_connections::update with COALESCE-per-field so the SQL matches the double-Option semantics without an OR-chain per field
This commit is contained in:
@@ -121,6 +121,45 @@ pub async fn get(
|
||||
})
|
||||
}
|
||||
|
||||
/// Patch the mutable metadata of a connection. Any `None` field is left
|
||||
/// untouched. Rotating the underlying PAT is a separate flow (delete + re-
|
||||
/// create through the wizard) — the connection row doesn't own the credential.
|
||||
pub async fn update(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
owner: Option<Option<&str>>,
|
||||
base_url: Option<Option<&str>>,
|
||||
label: Option<&str>,
|
||||
) -> Result<bool, DbError> {
|
||||
// COALESCE lets us pass a sentinel per-field: NULL means "leave alone",
|
||||
// anything else means "set to this". `owner` and `base_url` need the
|
||||
// second-nested Option so we can distinguish clear-to-null from no-op.
|
||||
let owner_set = owner.is_some();
|
||||
let owner_val = owner.and_then(|v| v.map(str::to_owned));
|
||||
let base_url_set = base_url.is_some();
|
||||
let base_url_val = base_url.and_then(|v| v.map(str::to_owned));
|
||||
let label_val = label.map(str::to_owned);
|
||||
let res = sqlx::query!(
|
||||
"UPDATE repo_connections
|
||||
SET owner = CASE WHEN $3 THEN $4 ELSE owner END,
|
||||
base_url = CASE WHEN $5 THEN $6 ELSE base_url END,
|
||||
label = COALESCE($7, label),
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
owner_set,
|
||||
owner_val,
|
||||
base_url_set,
|
||||
base_url_val,
|
||||
label_val,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(res.rows_affected() == 1)
|
||||
}
|
||||
|
||||
pub async fn delete(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result<bool, DbError> {
|
||||
let res = sqlx::query!(
|
||||
"DELETE FROM repo_connections WHERE id = $1 AND workspace_id = $2",
|
||||
|
||||
Reference in New Issue
Block a user