Rename workspace and remote folders with confirmation
Add WebDAV MOVE support and update workspace rename flow to handle both local and WebDAV-backed workspaces. The Tauri rename_workspace command is made async and now performs filesystem rename for local workspaces and issues a WebDAV MOVE (via a new WebDavClient::move_resource) for remote workspaces, updating stored paths and credentials accordingly. A confirmation dialog is added to SettingsScreen to prompt users before renaming, and minor UI/default tweaks are included (SetupScreen default name). This ensures renames update both local folders and remote WebDAV folders reliably and with user confirmation.
This commit is contained in:
parent
50d859ef80
commit
4c57851e15
|
|
@ -160,15 +160,84 @@ fn remove_workspace(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn rename_workspace(
|
||||
async fn rename_workspace(
|
||||
id: String,
|
||||
new_name: String,
|
||||
state: State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let mut s = lock_state(&state)?;
|
||||
s.config.rename_workspace(&id, new_name).map_err(|e| e.to_string())?;
|
||||
s.repo = None;
|
||||
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())
|
||||
// Extract workspace info while holding the lock briefly
|
||||
let (mode, old_path, webdav_url, webdav_path) = {
|
||||
let s = lock_state(&state)?;
|
||||
let ws = s.config.workspaces.get(&id).ok_or("Workspace not found")?;
|
||||
(
|
||||
ws.mode.clone(),
|
||||
ws.path.clone(),
|
||||
ws.webdav_url.clone(),
|
||||
ws.webdav_path.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
match mode {
|
||||
WorkspaceMode::Local => {
|
||||
// Rename the local folder
|
||||
let parent = old_path.parent().ok_or("Workspace has no parent directory")?;
|
||||
let new_path = parent.join(&new_name);
|
||||
if new_path != old_path {
|
||||
if new_path.exists() {
|
||||
return Err(format!("A folder named '{}' already exists at that location", new_name));
|
||||
}
|
||||
std::fs::rename(&old_path, &new_path).map_err(|e| format!("Failed to rename folder: {}", e))?;
|
||||
}
|
||||
let mut s = lock_state(&state)?;
|
||||
s.config.rename_workspace(&id, new_name).map_err(|e| e.to_string())?;
|
||||
if let Some(ws) = s.config.workspaces.get_mut(&id) {
|
||||
ws.path = new_path;
|
||||
}
|
||||
s.repo = None;
|
||||
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
WorkspaceMode::Webdav => {
|
||||
// Rename the remote folder via WebDAV MOVE
|
||||
let base_url = webdav_url.as_deref().ok_or("No WebDAV URL configured")?;
|
||||
let remote_path = webdav_path.as_deref().unwrap_or("");
|
||||
|
||||
let domain = base_url
|
||||
.split("://").nth(1)
|
||||
.and_then(|rest| rest.split('/').next())
|
||||
.unwrap_or("").to_string();
|
||||
let (username, password) = tokio::task::spawn_blocking(move || {
|
||||
webdav::load_credentials(&domain)
|
||||
.map(|(u, p)| ((*u).clone(), (*p).clone()))
|
||||
.map_err(|e| e.to_string())
|
||||
}).await.map_err(|e| e.to_string())??;
|
||||
|
||||
let client = webdav::WebDavClient::new(base_url, &username, &password)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Compute new remote path by replacing the last segment
|
||||
let new_remote_path = if remote_path.is_empty() || remote_path == "/" {
|
||||
new_name.clone()
|
||||
} else if let Some(parent) = remote_path.trim_end_matches('/').rsplit_once('/') {
|
||||
format!("{}/{}", parent.0, new_name)
|
||||
} else {
|
||||
new_name.clone()
|
||||
};
|
||||
|
||||
if new_remote_path != remote_path {
|
||||
client.move_resource(remote_path, &new_remote_path).await.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut s = lock_state(&state)?;
|
||||
s.config.rename_workspace(&id, new_name).map_err(|e| e.to_string())?;
|
||||
if let Some(ws) = s.config.workspaces.get_mut(&id) {
|
||||
ws.webdav_path = Some(new_remote_path);
|
||||
}
|
||||
s.repo = None;
|
||||
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Workspace init ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { app } from "../stores/app.svelte";
|
||||
import ConfirmDialog from "../components/ConfirmDialog.svelte";
|
||||
|
||||
let { onclose, workspaceId, ondelete }: { onclose?: () => void; workspaceId: string; ondelete?: (id: string) => void } = $props();
|
||||
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
let renaming = $state(false);
|
||||
let renameValue = $state("");
|
||||
let showKebab = $state(false);
|
||||
let confirmRename = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!ws?.webdav_url) return;
|
||||
|
|
@ -70,6 +72,13 @@
|
|||
renaming = false;
|
||||
var trimmed = renameValue.trim();
|
||||
if (!trimmed || trimmed === ws?.name) return;
|
||||
confirmRename = true;
|
||||
}
|
||||
|
||||
async function doRename() {
|
||||
confirmRename = false;
|
||||
var trimmed = renameValue.trim();
|
||||
if (!trimmed) return;
|
||||
await app.renameWorkspace(workspaceId, trimmed);
|
||||
}
|
||||
function handleWindowClick(e: MouseEvent) {
|
||||
|
|
@ -250,3 +259,13 @@
|
|||
|
||||
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
||||
</main>
|
||||
|
||||
{#if confirmRename}
|
||||
<ConfirmDialog
|
||||
message="Rename workspace to '{renameValue.trim()}'?"
|
||||
detail={isWebdav ? "This will rename the folder on the WebDAV server." : "This will rename the folder on disk."}
|
||||
confirmText="Rename"
|
||||
onconfirm={doRename}
|
||||
oncancel={() => confirmRename = false}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@
|
|||
|
||||
function goBack() {
|
||||
mode = null;
|
||||
name = "";
|
||||
name = "Onyx";
|
||||
path = "";
|
||||
webdavUrl = "";
|
||||
webdavUser = "";
|
||||
|
|
|
|||
|
|
@ -187,6 +187,28 @@ impl WebDavClient {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Move/rename a resource (file or directory) on the server using WebDAV MOVE.
|
||||
pub async fn move_resource(&self, from: &str, to: &str) -> Result<()> {
|
||||
let from_url = self.full_url(from);
|
||||
let to_url = self.full_url(to);
|
||||
let resp = self._client
|
||||
.request(reqwest::Method::from_bytes(b"MOVE").unwrap(), &from_url)
|
||||
.basic_auth(self._username.as_str(), Some(self._password.as_str()))
|
||||
.header("Destination", &to_url)
|
||||
.header("Overwrite", "F")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = resp.status().as_u16();
|
||||
if status == 412 {
|
||||
return Err(Error::WebDav("Destination already exists".into()));
|
||||
}
|
||||
if !(200..=299).contains(&status) {
|
||||
return Err(Error::WebDav(format!("MOVE failed with status {}", status)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure a directory exists, creating it and parents as needed.
|
||||
pub async fn ensure_dir(&self, path: &str) -> Result<()> {
|
||||
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
||||
|
|
|
|||
Loading…
Reference in a new issue