From cb0c8d71f414b6d227959f2ce5c12dbe045334fc Mon Sep 17 00:00:00 2001 From: haex Date: Sat, 25 Oct 2025 08:09:15 +0200 Subject: [PATCH] fix window on workspace rendering --- src-tauri/database/schemas/haex.ts | 4 +- src-tauri/src/extension/core/manager.rs | 76 +++-- src-tauri/src/extension/crypto.rs | 69 +++-- src-tauri/src/extension/mod.rs | 35 ++- src/components/haex/desktop/icon.vue | 4 +- src/components/haex/desktop/index.vue | 274 +++++++++-------- src/components/haex/extension/launcher.vue | 98 +++++- src/components/haex/window/overview.vue | 2 +- src/pages/vault/[vaultId]/settings.vue | 139 --------- .../vault/[vaultId]/settings/developer.vue | 279 ------------------ src/stores/desktop/index.ts | 41 ++- src/stores/desktop/workspace.ts | 43 ++- 12 files changed, 424 insertions(+), 640 deletions(-) delete mode 100644 src/pages/vault/[vaultId]/settings.vue delete mode 100644 src/pages/vault/[vaultId]/settings/developer.vue diff --git a/src-tauri/database/schemas/haex.ts b/src-tauri/database/schemas/haex.ts index 8ec98ed..8f86a22 100644 --- a/src-tauri/database/schemas/haex.ts +++ b/src-tauri/database/schemas/haex.ts @@ -156,11 +156,11 @@ export const haexDesktopItems = sqliteTable( .notNull() .references(() => haexWorkspaces.id, { onDelete: 'cascade' }), itemType: text(tableNames.haex.desktop_items.columns.itemType, { - enum: ['extension', 'file', 'folder'], + enum: ['system', 'extension', 'file', 'folder'], }).notNull(), referenceId: text( tableNames.haex.desktop_items.columns.referenceId, - ).notNull(), // extensionId für extensions, filePath für files/folders + ).notNull(), // systemId für system windows, extensionId für extensions, filePath für files/folders positionX: integer(tableNames.haex.desktop_items.columns.positionX) .notNull() .default(0), diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 538a900..c53ebef 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -88,30 +88,64 @@ impl ExtensionManager { reason: format!("Cannot extract ZIP: {}", e), })?; - // Check if manifest.json is directly in temp or in a subdirectory - let manifest_path = temp.join("manifest.json"); - let actual_dir = if manifest_path.exists() { - temp.clone() - } else { - // manifest.json is in a subdirectory - find it - let mut found_dir = None; - for entry in fs::read_dir(&temp) - .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))? - { - let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; - let path = entry.path(); - if path.is_dir() && path.join("manifest.json").exists() { - found_dir = Some(path); - break; - } + // Read haextension_dir from config if it exists, otherwise use default + let config_path = temp.join("haextension.config.json"); + let haextension_dir = if config_path.exists() { + let config_content = std::fs::read_to_string(&config_path) + .map_err(|e| ExtensionError::ManifestError { + reason: format!("Cannot read haextension.config.json: {}", e), + })?; + + let config: serde_json::Value = serde_json::from_str(&config_content) + .map_err(|e| ExtensionError::ManifestError { + reason: format!("Invalid haextension.config.json: {}", e), + })?; + + let dir = config + .get("dev") + .and_then(|dev| dev.get("haextension_dir")) + .and_then(|dir| dir.as_str()) + .unwrap_or("haextension") + .to_string(); + + // Security: Validate that haextension_dir doesn't contain ".." for path traversal + if dir.contains("..") { + return Err(ExtensionError::ManifestError { + reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(), + }); } - found_dir.ok_or_else(|| ExtensionError::ManifestError { - reason: "manifest.json not found in extension archive".to_string(), - })? + dir + } else { + "haextension".to_string() }; - let manifest_path = actual_dir.join("manifest.json"); + // Build the manifest path + let manifest_path = temp.join(&haextension_dir).join("manifest.json"); + + // Ensure the resolved path is still within temp directory (safety check against path traversal) + let canonical_temp = temp.canonicalize() + .map_err(|e| ExtensionError::Filesystem { source: e })?; + + // Only check if manifest_path parent exists to avoid errors + if let Some(parent) = manifest_path.parent() { + if let Ok(canonical_manifest_dir) = parent.canonicalize() { + if !canonical_manifest_dir.starts_with(&canonical_temp) { + return Err(ExtensionError::ManifestError { + reason: "Security violation: manifest path outside extension directory".to_string(), + }); + } + } + } + + // Check if manifest exists + if !manifest_path.exists() { + return Err(ExtensionError::ManifestError { + reason: format!("manifest.json not found at {}/manifest.json", haextension_dir), + }); + } + + let actual_dir = temp.clone(); let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError { reason: format!("Cannot read manifest: {}", e), @@ -119,7 +153,7 @@ impl ExtensionManager { let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; - let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| { + let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| { ExtensionError::SignatureVerificationFailed { reason: e.to_string(), } diff --git a/src-tauri/src/extension/crypto.rs b/src-tauri/src/extension/crypto.rs index 393afe1..a49bc74 100644 --- a/src-tauri/src/extension/crypto.rs +++ b/src-tauri/src/extension/crypto.rs @@ -4,28 +4,13 @@ use std::{ }; // src-tauri/src/extension/crypto.rs +use crate::extension::error::ExtensionError; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use sha2::{Digest, Sha256}; pub struct ExtensionCrypto; impl ExtensionCrypto { - /// Berechnet Hash vom Public Key (wie im SDK) - pub fn calculate_key_hash(public_key_hex: &str) -> Result { - let public_key_bytes = - hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?; - - let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap()) - .map_err(|e| format!("Invalid public key: {}", e))?; - - let mut hasher = Sha256::new(); - hasher.update(public_key.as_bytes()); - let result = hasher.finalize(); - - // Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK - Ok(hex::encode(&result[..10])) - } - /// Verifiziert Extension-Signatur pub fn verify_signature( public_key_hex: &str, @@ -50,26 +35,48 @@ impl ExtensionCrypto { } /// Berechnet Hash eines Verzeichnisses (für Verifikation) - pub fn hash_directory(dir: &Path) -> Result { + pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result { // 1. Alle Dateipfade rekursiv sammeln let mut all_files = Vec::new(); Self::collect_files_recursively(dir, &mut all_files) - .map_err(|e| format!("Failed to collect files: {}", e))?; - all_files.sort(); + .map_err(|e| ExtensionError::Filesystem { source: e })?; + + // 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK) + let mut relative_files: Vec<(String, PathBuf)> = all_files + .into_iter() + .map(|path| { + let relative = path.strip_prefix(dir) + .unwrap_or(&path) + .to_string_lossy() + .to_string(); + (relative, path) + }) + .collect(); + + // 3. Sortiere nach relativen Pfaden + relative_files.sort_by(|a, b| a.0.cmp(&b.0)); + + println!("=== Files to hash ({}): ===", relative_files.len()); + for (rel, _) in &relative_files { + println!(" - {}", rel); + } let mut hasher = Sha256::new(); - let manifest_path = dir.join("manifest.json"); - // 2. Inhalte der sortierten Dateien hashen - for file_path in all_files { + // 4. Inhalte der sortierten Dateien hashen + for (_relative, file_path) in relative_files { if file_path == manifest_path { // FÜR DIE MANIFEST.JSON: let content_str = fs::read_to_string(&file_path) - .map_err(|e| format!("Cannot read manifest file: {}", e))?; + .map_err(|e| ExtensionError::Filesystem { source: e })?; // Parse zu einem generischen JSON-Wert - let mut manifest: serde_json::Value = serde_json::from_str(&content_str) - .map_err(|e| format!("Cannot parse manifest JSON: {}", e))?; + let mut manifest: serde_json::Value = + serde_json::from_str(&content_str).map_err(|e| { + ExtensionError::ManifestError { + reason: format!("Cannot parse manifest JSON: {}", e), + } + })?; // Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten if let Some(obj) = manifest.as_object_mut() { @@ -80,13 +87,19 @@ impl ExtensionCrypto { } // Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS) - let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap(); + // serde_json sortiert die Keys automatisch alphabetisch + let canonical_manifest_content = + serde_json::to_string_pretty(&manifest).map_err(|e| { + ExtensionError::ManifestError { + reason: format!("Failed to serialize manifest: {}", e), + } + })?; println!("canonical_manifest_content: {}", canonical_manifest_content); hasher.update(canonical_manifest_content.as_bytes()); } else { // FÜR ALLE ANDEREN DATEIEN: - let content = fs::read(&file_path) - .map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?; + let content = + fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?; hasher.update(&content); } } diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 6e49db0..23f77e9 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -223,6 +223,16 @@ pub fn is_extension_installed( #[derive(serde::Deserialize, Debug)] struct HaextensionConfig { dev: DevConfig, + #[serde(default)] + keys: KeysConfig, +} + +#[derive(serde::Deserialize, Debug, Default)] +struct KeysConfig { + #[serde(default)] + public_key_path: Option, + #[serde(default)] + private_key_path: Option, } #[derive(serde::Deserialize, Debug)] @@ -231,6 +241,8 @@ struct DevConfig { port: u16, #[serde(default = "default_host")] host: String, + #[serde(default = "default_haextension_dir")] + haextension_dir: String, } fn default_port() -> u16 { @@ -241,6 +253,10 @@ fn default_host() -> String { "localhost".to_string() } +fn default_haextension_dir() -> String { + "haextension".to_string() +} + /// Check if a dev server is reachable by making a simple HTTP request async fn check_dev_server_health(url: &str) -> bool { use tauri_plugin_http::reqwest; @@ -276,29 +292,30 @@ pub async fn load_dev_extension( let extension_path_buf = PathBuf::from(&extension_path); - // 1. Read haextension.json to get dev server config - let config_path = extension_path_buf.join("haextension.json"); - let (host, port) = if config_path.exists() { + // 1. Read haextension.config.json to get dev server config and haextension directory + let config_path = extension_path_buf.join("haextension.config.json"); + let (host, port, haextension_dir) = if config_path.exists() { let config_content = std::fs::read_to_string(&config_path).map_err(|e| { ExtensionError::ValidationError { - reason: format!("Failed to read haextension.json: {}", e), + reason: format!("Failed to read haextension.config.json: {}", e), } })?; let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| { ExtensionError::ValidationError { - reason: format!("Failed to parse haextension.json: {}", e), + reason: format!("Failed to parse haextension.config.json: {}", e), } })?; - (config.dev.host, config.dev.port) + (config.dev.host, config.dev.port, config.dev.haextension_dir) } else { // Default values if config doesn't exist - (default_host(), default_port()) + (default_host(), default_port(), default_haextension_dir()) }; let dev_server_url = format!("http://{}:{}", host, port); eprintln!("📡 Dev server URL: {}", dev_server_url); + eprintln!("📁 Haextension directory: {}", haextension_dir); // 1.5. Check if dev server is running if !check_dev_server_health(&dev_server_url).await { @@ -311,8 +328,8 @@ pub async fn load_dev_extension( } eprintln!("✅ Dev server is reachable"); - // 2. Build path to manifest: /haextension/manifest.json - let manifest_path = extension_path_buf.join("haextension").join("manifest.json"); + // 2. Build path to manifest: //manifest.json + let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json"); // Check if manifest exists if !manifest_path.exists() { diff --git a/src/components/haex/desktop/icon.vue b/src/components/haex/desktop/icon.vue index 6aee673..7e1fd4b 100644 --- a/src/components/haex/desktop/icon.vue +++ b/src/components/haex/desktop/icon.vue @@ -37,7 +37,7 @@ :alt="label" class="w-14 h-14 object-contain transition-transform duration-200" :class="{ 'scale-110': isSelected }" - > + /> const props = defineProps<{ id: string - itemType: 'extension' | 'file' | 'folder' + itemType: DesktopItemType referenceId: string initialX: number initialY: number diff --git a/src/components/haex/desktop/index.vue b/src/components/haex/desktop/index.vue index d34e401..fa878c5 100644 --- a/src/components/haex/desktop/index.vue +++ b/src/components/haex/desktop/index.vue @@ -27,6 +27,8 @@ class="w-full h-full relative isolate" @click.self.stop="handleDesktopClick" @mousedown.left.self="handleAreaSelectStart" + @dragover.prevent="handleDragOver" + @drop.prevent="handleDrop($event, workspace.id)" >
- -
- - + - - - - - - - - + + + +
+ + + + + + + + +
@@ -342,6 +329,18 @@ const getWorkspaceIcons = (workspaceId: string) => { return desktopItems.value .filter((item) => item.workspaceId === workspaceId) .map((item) => { + if (item.itemType === 'system') { + const systemWindow = windowManager.getAllSystemWindows().find( + (win) => win.id === item.referenceId, + ) + + return { + ...item, + label: systemWindow?.name || 'Unknown', + icon: systemWindow?.icon || '', + } + } + if (item.itemType === 'extension') { const extension = availableExtensions.value.find( (ext) => ext.id === item.referenceId, @@ -416,6 +415,49 @@ const handleDragEnd = async () => { allowSwipe.value = true // Re-enable Swiper after drag } +// Handle drag over for launcher items +const handleDragOver = (event: DragEvent) => { + if (!event.dataTransfer) return + + // Check if this is a launcher item + if (event.dataTransfer.types.includes('application/haex-launcher-item')) { + event.dataTransfer.dropEffect = 'copy' + } +} + +// Handle drop for launcher items +const handleDrop = async (event: DragEvent, workspaceId: string) => { + if (!event.dataTransfer) return + + const launcherItemData = event.dataTransfer.getData('application/haex-launcher-item') + if (!launcherItemData) return + + try { + const item = JSON.parse(launcherItemData) as { + id: string + name: string + icon: string + type: 'system' | 'extension' + } + + // Get drop position relative to desktop + const desktopRect = (event.currentTarget as HTMLElement).getBoundingClientRect() + const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2) + const y = Math.max(0, event.clientY - desktopRect.top - 32) + + // Create desktop icon on the specific workspace + await desktopStore.addDesktopItemAsync( + item.type as DesktopItemType, + item.id, + x, + y, + workspaceId + ) + } catch (error) { + console.error('Failed to create desktop icon:', error) + } +} + const handleDesktopClick = () => { // Only clear selection if it was a simple click, not an area selection // Check if we just finished an area selection (box size > threshold) diff --git a/src/components/haex/extension/launcher.vue b/src/components/haex/extension/launcher.vue index e0e1c68..1aea498 100644 --- a/src/components/haex/extension/launcher.vue +++ b/src/components/haex/extension/launcher.vue @@ -11,22 +11,29 @@ - - - - -de: - language: Sprache - design: Design - save: Änderung speichern - notifications: - label: Benachrichtigungen - requestPermission: Benachrichtigung erlauben - vaultName: - label: Vaultname - update: - success: Vaultname erfolgreich aktualisiert - error: Vaultname konnte nicht aktualisiert werden - deviceName: - label: Gerätename - update: - success: Gerätename wurde erfolgreich aktualisiert - error: Gerätename konnte nich aktualisiert werden -en: - language: Language - design: Design - save: save changes - notifications: - label: Notifications - requestPermission: Grant Permission - vaultName: - label: Vault Name - update: - success: Vault Name successfully updated - error: Vault name could not be updated - deviceName: - label: Device name - update: - success: Device name has been successfully updated - error: Device name could not be updated - diff --git a/src/pages/vault/[vaultId]/settings/developer.vue b/src/pages/vault/[vaultId]/settings/developer.vue deleted file mode 100644 index cae3138..0000000 --- a/src/pages/vault/[vaultId]/settings/developer.vue +++ /dev/null @@ -1,279 +0,0 @@ - - - - - -de: - title: Entwicklereinstellungen - description: Lade Extensions im Entwicklungsmodus für schnelleres Testen mit Hot-Reload. - add: - title: Dev-Extension hinzufügen - extensionPath: Extension-Pfad - extensionPathPlaceholder: /pfad/zu/deiner/extension - extensionPathHint: Pfad zum Extension-Projekt (enthält haextension/ und haextension.json) - browse: Durchsuchen - browseTitle: Extension-Verzeichnis auswählen - loadExtension: Extension laden - success: Dev-Extension erfolgreich geladen - errors: - browseFailed: Verzeichnis konnte nicht ausgewählt werden - loadFailed: Extension konnte nicht geladen werden - list: - title: Geladene Dev-Extensions - empty: Keine Dev-Extensions geladen - reload: Neu laden - remove: Entfernen - reloadInfo: Extension wird beim nächsten Laden automatisch aktualisiert - removeSuccess: Dev-Extension erfolgreich entfernt - errors: - reloadFailed: Extension konnte nicht neu geladen werden - removeFailed: Extension konnte nicht entfernt werden - -en: - title: Developer Settings - description: Load extensions in development mode for faster testing with hot-reload. - add: - title: Add Dev Extension - extensionPath: Extension Path - extensionPathPlaceholder: /path/to/your/extension - extensionPathHint: Path to your extension project (contains haextension/ and haextension.json) - browse: Browse - browseTitle: Select Extension Directory - loadExtension: Load Extension - success: Dev extension loaded successfully - errors: - browseFailed: Failed to select directory - loadFailed: Failed to load extension - list: - title: Loaded Dev Extensions - empty: No dev extensions loaded - reload: Reload - remove: Remove - reloadInfo: Extension will be automatically updated on next load - removeSuccess: Dev extension removed successfully - errors: - reloadFailed: Failed to reload extension - removeFailed: Failed to remove extension - diff --git a/src/stores/desktop/index.ts b/src/stores/desktop/index.ts index a6a55ac..3df7038 100644 --- a/src/stores/desktop/index.ts +++ b/src/stores/desktop/index.ts @@ -7,7 +7,7 @@ import type { import de from './de.json' import en from './en.json' -export type DesktopItemType = 'extension' | 'file' | 'folder' +export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system' export interface IDesktopItem extends SelectHaexDesktopItems { label?: string @@ -57,18 +57,20 @@ export const useDesktopStore = defineStore('desktopStore', () => { referenceId: string, positionX: number = 0, positionY: number = 0, + workspaceId?: string, ) => { if (!currentVault.value?.drizzle) { throw new Error('Kein Vault geöffnet') } - if (!currentWorkspace.value) { + const targetWorkspaceId = workspaceId || currentWorkspace.value?.id + if (!targetWorkspaceId) { throw new Error('Kein Workspace aktiv') } try { const newItem: InsertHaexDesktopItems = { - workspaceId: currentWorkspace.value.id, + workspaceId: targetWorkspaceId, itemType: itemType, referenceId: referenceId, positionX: positionX, @@ -85,7 +87,19 @@ export const useDesktopStore = defineStore('desktopStore', () => { return result[0] } } catch (error) { - console.error('Fehler beim Hinzufügen des Desktop-Items:', error) + console.error('Fehler beim Hinzufügen des Desktop-Items:', { + error, + itemType, + referenceId, + workspaceId: targetWorkspaceId, + position: { x: positionX, y: positionY } + }) + + // Log full error details + if (error && typeof error === 'object') { + console.error('Full error object:', JSON.stringify(error, null, 2)) + } + throw error } } @@ -154,8 +168,23 @@ export const useDesktopStore = defineStore('desktopStore', () => { referenceId: string, sourcePosition?: { x: number; y: number; width: number; height: number }, ) => { - if (itemType === 'extension') { - const windowManager = useWindowManagerStore() + const windowManager = useWindowManagerStore() + + if (itemType === 'system') { + const systemWindow = windowManager.getAllSystemWindows().find( + (win) => win.id === referenceId, + ) + + if (systemWindow) { + windowManager.openWindowAsync({ + sourceId: systemWindow.id, + type: 'system', + icon: systemWindow.icon, + title: systemWindow.name, + sourcePosition, + }) + } + } else if (itemType === 'extension') { const extensionsStore = useExtensionsStore() const extension = extensionsStore.availableExtensions.find( diff --git a/src/stores/desktop/workspace.ts b/src/stores/desktop/workspace.ts index 97a627c..6ca7575 100644 --- a/src/stores/desktop/workspace.ts +++ b/src/stores/desktop/workspace.ts @@ -1,4 +1,4 @@ -import { eq } from 'drizzle-orm' +import { asc, eq } from 'drizzle-orm' import { haexWorkspaces, type SelectHaexWorkspaces, @@ -32,18 +32,18 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => { } try { - /* const items = await currentVault.value.drizzle + const items = await currentVault.value.drizzle .select() .from(haexWorkspaces) .orderBy(asc(haexWorkspaces.position)) console.log('loadWorkspacesAsync', items) - workspaces.value = items */ + workspaces.value = items // Create default workspace if none exist - /* if (items.length === 0) { */ - await addWorkspaceAsync('Workspace 1') - /* } */ + if (items.length === 0) { + await addWorkspaceAsync('Workspace 1') + } } catch (error) { console.error('Fehler beim Laden der Workspaces:', error) throw error @@ -61,15 +61,12 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => { try { const newIndex = workspaces.value.length + 1 - const newWorkspace: SelectHaexWorkspaces = { - id: crypto.randomUUID(), + const newWorkspace = { name: name || `Workspace ${newIndex}`, position: workspaces.value.length, - haexTimestamp: '', } - workspaces.value.push(newWorkspace) - currentWorkspaceIndex.value = workspaces.value.length - 1 - /* const result = await currentVault.value.drizzle + + const result = await currentVault.value.drizzle .insert(haexWorkspaces) .values(newWorkspace) .returning() @@ -78,7 +75,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => { workspaces.value.push(result[0]) currentWorkspaceIndex.value = workspaces.value.length - 1 return result[0] - } */ + } } catch (error) { console.error('Fehler beim Hinzufügen des Workspace:', error) throw error @@ -106,27 +103,27 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => { const index = workspaces.value.findIndex((ws) => ws.id === workspaceId) if (index === -1) return - workspaces.value.splice(index, 1) - workspaces.value.forEach((workspace, index) => (workspace.position = index)) - try { - /* await currentVault.value.drizzle.transaction(async (tx) => { + await currentVault.value.drizzle.transaction(async (tx) => { + // Delete workspace await tx .delete(haexWorkspaces) .where(eq(haexWorkspaces.id, workspaceId)) + // Update local state workspaces.value.splice(index, 1) - workspaces.value.forEach( - (workspace, index) => (workspace.position = index), - ) + workspaces.value.forEach((workspace, idx) => { + workspace.position = idx + }) + // Update positions in database for (const workspace of workspaces.value) { await tx .update(haexWorkspaces) - .set({ position: index }) - .where(eq(haexWorkspaces.position, workspace.position)) + .set({ position: workspace.position }) + .where(eq(haexWorkspaces.id, workspace.id)) } - }) */ + }) // Adjust current index if needed if (currentWorkspaceIndex.value >= workspaces.value.length) {