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:
Jamie Pine
2025-11-18 05:38:37 -08:00
parent e103265e20
commit 2f6c8a985d
9 changed files with 1418 additions and 1074 deletions

View File

@@ -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 })
}

View File

@@ -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

View File

@@ -18,7 +18,6 @@ export function LocationsSection() {
wireMethod: "query:locations.list",
input: null,
resourceType: "location",
isGlobalList: true,
});
const locations = locationsQuery.data?.locations || [];

View File

@@ -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 ?? [];

View File

@@ -20,7 +20,6 @@ export function VolumesGroup({
wireMethod: "query:volumes.list",
input: { filter },
resourceType: "volume",
isGlobalList: true,
});
const volumes = volumesData?.volumes || [];

View File

@@ -5,7 +5,6 @@ export function useSpaces() {
wireMethod: 'query:spaces.list',
input: null, // Unit struct serializes as null, not {}
resourceType: 'space',
isGlobalList: true,
});
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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">

View File

File diff suppressed because it is too large Load Diff