mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-16 12:26:42 -04:00
feat: enhance location update process and event emission
- Updated the LocationUpdateAction to clone the location before building the ActiveModel, ensuring data integrity during updates. - Implemented a new event emission for ResourceChanged, providing real-time updates to the UI upon location changes. - Enhanced error handling for missing entry IDs and directory paths, improving robustness in the update process. - Refactored the LocationInspector component to include new quick actions for reindexing locations, enhancing user interaction. - Removed redundant isGlobalList flags from various components to streamline the codebase.
This commit is contained in:
@@ -66,7 +66,7 @@ impl LibraryAction for LocationUpdateAction {
|
||||
.ok_or_else(|| ActionError::LocationNotFound(self.input.id))?;
|
||||
|
||||
// Build the update
|
||||
let mut active: entities::location::ActiveModel = location.into();
|
||||
let mut active: entities::location::ActiveModel = location.clone().into();
|
||||
|
||||
if let Some(name) = &self.input.name {
|
||||
active.name = Set(Some(name.clone()));
|
||||
@@ -82,7 +82,76 @@ impl LibraryAction for LocationUpdateAction {
|
||||
active.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
// Execute update
|
||||
active.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
let updated_location = active.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
// Emit ResourceChanged event for UI reactivity
|
||||
// Note: job_policies is local-only config (not synced), so we emit regular event not sync event
|
||||
// Build LocationInfo for the event
|
||||
let entry = entities::entry::Entity::find_by_id(
|
||||
updated_location
|
||||
.entry_id
|
||||
.ok_or_else(|| ActionError::Internal("Location has no entry_id".to_string()))?,
|
||||
)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal("Location entry not found".to_string()))?;
|
||||
|
||||
let directory_path = entities::directory_paths::Entity::find_by_id(entry.id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| {
|
||||
ActionError::Internal(format!(
|
||||
"No directory path found for location {} entry {}",
|
||||
updated_location.uuid, entry.id
|
||||
))
|
||||
})?;
|
||||
|
||||
let device = entities::device::Entity::find_by_id(updated_location.device_id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| {
|
||||
ActionError::Internal(format!(
|
||||
"Device not found for location {}",
|
||||
updated_location.uuid
|
||||
))
|
||||
})?;
|
||||
|
||||
let sd_path = crate::domain::SdPath::Physical {
|
||||
device_slug: device.slug.clone(),
|
||||
path: directory_path.path.clone().into(),
|
||||
};
|
||||
|
||||
let job_policies = updated_location
|
||||
.job_policies
|
||||
.as_ref()
|
||||
.and_then(|json| serde_json::from_str(json).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let location_info = crate::ops::locations::list::LocationInfo {
|
||||
id: updated_location.uuid,
|
||||
path: directory_path.path.clone().into(),
|
||||
name: updated_location.name.clone(),
|
||||
sd_path,
|
||||
job_policies,
|
||||
index_mode: updated_location.index_mode.clone(),
|
||||
scan_state: updated_location.scan_state.clone(),
|
||||
last_scan_at: updated_location.last_scan_at,
|
||||
error_message: updated_location.error_message.clone(),
|
||||
total_file_count: updated_location.total_file_count,
|
||||
total_byte_size: updated_location.total_byte_size,
|
||||
created_at: updated_location.created_at,
|
||||
updated_at: updated_location.updated_at,
|
||||
};
|
||||
|
||||
context.events.emit(crate::infra::event::Event::ResourceChanged {
|
||||
resource_type: "location".to_string(),
|
||||
resource: serde_json::to_value(&location_info)
|
||||
.map_err(|e| ActionError::Internal(format!("Failed to serialize location: {}", e)))?,
|
||||
metadata: None,
|
||||
});
|
||||
|
||||
Ok(LocationUpdateOutput { id: self.input.id })
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export function ExplorerLayout() {
|
||||
wireMethod: "query:locations.list",
|
||||
input: null,
|
||||
resourceType: "location",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
// Get current location if we're on a location route
|
||||
|
||||
@@ -18,7 +18,6 @@ export function LocationsSection() {
|
||||
wireMethod: "query:locations.list",
|
||||
input: null,
|
||||
resourceType: "location",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
const locations = locationsQuery.data?.locations || [];
|
||||
|
||||
@@ -16,7 +16,6 @@ export function LocationsGroup({ isCollapsed, onToggle }: LocationsGroupProps) {
|
||||
wireMethod: "query:locations.list",
|
||||
input: null, // Unit struct serializes as null, not {}
|
||||
resourceType: "location",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
const locations = locationsData?.locations ?? [];
|
||||
|
||||
@@ -20,7 +20,6 @@ export function VolumesGroup({
|
||||
wireMethod: "query:volumes.list",
|
||||
input: { filter },
|
||||
resourceType: "volume",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
const volumes = volumesData?.volumes || [];
|
||||
|
||||
@@ -5,7 +5,6 @@ export function useSpaces() {
|
||||
wireMethod: 'query:spaces.list',
|
||||
input: null, // Unit struct serializes as null, not {}
|
||||
resourceType: 'space',
|
||||
isGlobalList: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { HardDrive } from "@phosphor-icons/react";
|
||||
import { HardDrive, Plus } from "@phosphor-icons/react";
|
||||
import DriveIcon from "@sd/assets/icons/Drive.png";
|
||||
import HDDIcon from "@sd/assets/icons/HDD.png";
|
||||
import ServerIcon from "@sd/assets/icons/Server.png";
|
||||
@@ -7,7 +7,7 @@ import DatabaseIcon from "@sd/assets/icons/Database.png";
|
||||
import DriveAmazonS3Icon from "@sd/assets/icons/Drive-AmazonS3.png";
|
||||
import DriveGoogleDriveIcon from "@sd/assets/icons/Drive-GoogleDrive.png";
|
||||
import DriveDropboxIcon from "@sd/assets/icons/Drive-Dropbox.png";
|
||||
import { useNormalizedCache } from "../../context";
|
||||
import { useNormalizedCache, useLibraryMutation } from "../../context";
|
||||
import type {
|
||||
VolumeListOutput,
|
||||
VolumeListQueryInput,
|
||||
@@ -65,7 +65,6 @@ export function StorageOverview() {
|
||||
wireMethod: "query:volumes.list",
|
||||
input: { filter: "All" },
|
||||
resourceType: "volume",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
// Fetch all devices using normalized cache
|
||||
@@ -76,7 +75,6 @@ export function StorageOverview() {
|
||||
wireMethod: "query:devices.list",
|
||||
input: { include_offline: true, include_details: false },
|
||||
resourceType: "device",
|
||||
isGlobalList: true,
|
||||
});
|
||||
|
||||
if (volumesLoading || devicesLoading) {
|
||||
@@ -189,6 +187,18 @@ function getDummyVolumeStats(volumeName: string) {
|
||||
}
|
||||
|
||||
function VolumeBar({ volume, index }: VolumeBarProps) {
|
||||
const trackVolume = useLibraryMutation("volumes.track");
|
||||
|
||||
const handleTrack = async () => {
|
||||
try {
|
||||
await trackVolume.mutateAsync({
|
||||
fingerprint: volume.fingerprint,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to track volume:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Use real data from backend, fallback to dummy data if not available
|
||||
const useDummyData = !volume.total_capacity;
|
||||
const dummy = useDummyData ? getDummyVolumeStats(volume.name) : null;
|
||||
@@ -244,9 +254,15 @@ function VolumeBar({ volume, index }: VolumeBarProps) {
|
||||
</span>
|
||||
)}
|
||||
{!volume.is_tracked && (
|
||||
<span className="px-2 py-0.5 bg-accent/10 text-accent text-xs rounded-md border border-accent/20">
|
||||
Untracked
|
||||
</span>
|
||||
<button
|
||||
onClick={handleTrack}
|
||||
disabled={trackVolume.isPending}
|
||||
className="px-2 py-0.5 bg-accent/10 hover:bg-accent/20 text-accent text-xs rounded-md border border-accent/20 hover:border-accent/30 transition-colors flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Track this volume to enable deduplication and search"
|
||||
>
|
||||
<Plus className="size-3" weight="bold" />
|
||||
{trackVolume.isPending ? "Tracking..." : "Track"}
|
||||
</button>
|
||||
)}
|
||||
{useDummyData && (
|
||||
<span className="px-2 py-0.5 bg-yellow-500/10 text-yellow-600 text-xs rounded-md border border-yellow-500/20">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user