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]
|
#[tauri::command]
|
||||||
fn rename_workspace(
|
async fn rename_workspace(
|
||||||
id: String,
|
id: String,
|
||||||
new_name: String,
|
new_name: String,
|
||||||
state: State<'_, Mutex<AppState>>,
|
state: State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut s = lock_state(&state)?;
|
// Extract workspace info while holding the lock briefly
|
||||||
s.config.rename_workspace(&id, new_name).map_err(|e| e.to_string())?;
|
let (mode, old_path, webdav_url, webdav_path) = {
|
||||||
s.repo = None;
|
let s = lock_state(&state)?;
|
||||||
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())
|
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 ───────────────────────────────────────────────────
|
// ── Workspace init ───────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { app } from "../stores/app.svelte";
|
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();
|
let { onclose, workspaceId, ondelete }: { onclose?: () => void; workspaceId: string; ondelete?: (id: string) => void } = $props();
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
let renaming = $state(false);
|
let renaming = $state(false);
|
||||||
let renameValue = $state("");
|
let renameValue = $state("");
|
||||||
let showKebab = $state(false);
|
let showKebab = $state(false);
|
||||||
|
let confirmRename = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!ws?.webdav_url) return;
|
if (!ws?.webdav_url) return;
|
||||||
|
|
@ -70,6 +72,13 @@
|
||||||
renaming = false;
|
renaming = false;
|
||||||
var trimmed = renameValue.trim();
|
var trimmed = renameValue.trim();
|
||||||
if (!trimmed || trimmed === ws?.name) return;
|
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);
|
await app.renameWorkspace(workspaceId, trimmed);
|
||||||
}
|
}
|
||||||
function handleWindowClick(e: MouseEvent) {
|
function handleWindowClick(e: MouseEvent) {
|
||||||
|
|
@ -250,3 +259,13 @@
|
||||||
|
|
||||||
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
||||||
</main>
|
</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() {
|
function goBack() {
|
||||||
mode = null;
|
mode = null;
|
||||||
name = "";
|
name = "Onyx";
|
||||||
path = "";
|
path = "";
|
||||||
webdavUrl = "";
|
webdavUrl = "";
|
||||||
webdavUser = "";
|
webdavUser = "";
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,28 @@ impl WebDavClient {
|
||||||
Ok(())
|
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.
|
/// Ensure a directory exists, creating it and parents as needed.
|
||||||
pub async fn ensure_dir(&self, path: &str) -> Result<()> {
|
pub async fn ensure_dir(&self, path: &str) -> Result<()> {
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue