Conversation
Epic planning docs for enabling land cover class renaming in ProjectSetup. Classes currently use name-as-key throughout the system (training samples, sessions, colors). This epic adds cascading rename support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cascading rename that updates all state in a single batch: - Configuration classes (LandCoverClass list) - Training samples (LabeledPixelSample.ClassName) - Training regions (TrainingRegion.ClassName) - Training session (Statistics dictionary re-keyed) - Class colors (dictionary re-keyed) Validates: non-empty name, no duplicates, class exists. Uses BeginBatch/EndBatch to fire exactly one OnStateChanged event, which triggers auto-persist via the existing debounce mechanism. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Edit button per row and double-click on name to enter edit mode - RadzenTextBox with Enter to confirm, Escape to cancel, blur to confirm - Validates no duplicates, no empty names before applying - OnClassRenamed callback propagates to ProjectSetup which calls ProjectStateService.RenameClass() and re-saves configuration - Success/failure notifications via NotificationService Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests cover: config update, training samples propagation, training regions, training session re-keying, class colors, duplicate name rejection, empty name rejection, non-existent class, same-name no-op, no-config error, and single OnStateChanged event via batch. All 495 tests pass (349 Core + 130 Web + 16 CLI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
Adds end-to-end “rename land cover class” support in the Blazor web app by introducing a state-level rename operation, wiring it into the Project Setup UI, and documenting the epic/design.
Changes:
- Added
ProjectStateService.RenameClass(oldName, newName)to cascade renames across in-memory project state. - Added inline rename UX in
LandCoverClassPaneland connected it toProjectSetupfor persistence + notifications. - Added unit tests and epic documentation for the rename feature.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/FuzzySat.Web.Tests/Services/ProjectStateServiceTests.cs | Adds rename cascade/validation tests for the new state service method. |
| src/FuzzySat.Web/Services/ProjectStateService.cs | Implements RenameClass with batch notification and multi-structure propagation. |
| src/FuzzySat.Web/Components/Shared/ProjectSetup/LandCoverClassPanel.razor | Adds inline editing UI (dblclick/edit button, Enter/Escape/blur) and rename callback. |
| src/FuzzySat.Web/Components/Pages/ProjectSetup.razor | Handles rename callback, persists config, and shows notifications. |
| docs/epics/epic-012-class-rename/00-overview.md | Epic overview for class rename. |
| docs/epics/epic-012-class-rename/01-plan.md | Implementation plan for epic #12. |
| docs/epics/epic-012-class-rename/02-technical-design.md | Technical design notes for cascading rename. |
| docs/epics/ACTIVE_EPICS.md | Registers Epic #12 as in development. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (_trainingSession is not null && _trainingSession.Statistics.ContainsKey(oldName)) | ||
| { | ||
| var newStats = new Dictionary<string, SpectralStatistics>(); | ||
| foreach (var (key, value) in _trainingSession.Statistics) | ||
| newStats[key == oldName ? newName : key] = value; | ||
|
|
||
| var newClassNames = _trainingSession.ClassNames | ||
| .Select(n => n == oldName ? newName : n).ToList(); | ||
|
|
||
| _trainingSession = TrainingSession.CreateFromStatistics( | ||
| newStats, newClassNames, _trainingSession.BandNames, | ||
| _trainingSession.Id, _trainingSession.CreatedAt); |
There was a problem hiding this comment.
When rebuilding the TrainingSession statistics dictionary, the values (SpectralStatistics) keep their original ClassName. After a rename, you'll end up with a key of newName but SpectralStatistics.ClassName == oldName, which can produce inconsistent persisted DTOs (TrainingSessionDto.FromSession uses kvp.Value.ClassName). Rebuild the SpectralStatistics instance for the renamed entry (copy mean/stddev) so the internal ClassName matches the new key.
| if (string.IsNullOrWhiteSpace(newName) || newName == originalName) | ||
| return; | ||
|
|
||
| // Check for duplicates locally | ||
| if (Classes.Any(c => c.Name == newName)) | ||
| return; | ||
|
|
There was a problem hiding this comment.
Duplicate-name renames are currently blocked locally (if (Classes.Any(c => c.Name == newName)) return;) with no callback to the parent, so the user gets no warning/notification. This conflicts with the PR behavior described (duplicate should show a warning) and makes failures silent. Consider invoking OnClassRenamed (or a separate failure callback) so ProjectSetup can display the warning, or surface an inline validation message in the panel instead of returning silently.
|
|
||
| // Re-save configuration to persist the rename | ||
| var projectName = ProjectState.Configuration!.ProjectName; | ||
| var safeName = string.Join("_", projectName.Trim().Split(Path.GetInvalidFileNameChars())) | ||
| .Trim('_').Trim('.'); | ||
| await ProjectLoader.SaveProjectAsync(safeName, ProjectState.Configuration); | ||
|
|
||
| NotificationService.Notify(NotificationSeverity.Success, "Class Renamed", | ||
| $"'{args.OldName}' renamed to '{args.NewName}'.", duration: 3000); | ||
| } | ||
| catch (ArgumentException ex) | ||
| { | ||
| NotificationService.Notify(NotificationSeverity.Warning, "Rename Failed", | ||
| ex.Message, duration: 4000); |
There was a problem hiding this comment.
OnClassRenamed only catches ArgumentException from RenameClass. If safeName ends up empty/invalid (or SaveProjectAsync throws due to I/O issues), the exception will bubble out of the UI event and can break the Blazor circuit. Reuse the same safeName validation as SaveConfiguration (including empty/len/reserved checks) and broaden error handling around SaveProjectAsync (log + notify) so persistence failures don't crash the page.
| // Re-save configuration to persist the rename | |
| var projectName = ProjectState.Configuration!.ProjectName; | |
| var safeName = string.Join("_", projectName.Trim().Split(Path.GetInvalidFileNameChars())) | |
| .Trim('_').Trim('.'); | |
| await ProjectLoader.SaveProjectAsync(safeName, ProjectState.Configuration); | |
| NotificationService.Notify(NotificationSeverity.Success, "Class Renamed", | |
| $"'{args.OldName}' renamed to '{args.NewName}'.", duration: 3000); | |
| } | |
| catch (ArgumentException ex) | |
| { | |
| NotificationService.Notify(NotificationSeverity.Warning, "Rename Failed", | |
| ex.Message, duration: 4000); | |
| } | |
| catch (ArgumentException ex) | |
| { | |
| NotificationService.Notify(NotificationSeverity.Warning, "Rename Failed", | |
| ex.Message, duration: 4000); | |
| return; | |
| } | |
| // Re-save configuration to persist the rename | |
| var projectName = ProjectState.Configuration!.ProjectName; | |
| var safeName = string.Join("_", projectName.Trim().Split(Path.GetInvalidFileNameChars())) | |
| .Trim('_').Trim('.'); | |
| // Validate safe project name (empty, length, reserved device names) | |
| if (string.IsNullOrWhiteSpace(safeName)) | |
| { | |
| NotificationService.Notify(NotificationSeverity.Warning, "Save Skipped", | |
| "The project name is empty or invalid after sanitization.", duration: 4000); | |
| return; | |
| } | |
| // Basic length check to avoid filesystem issues with very long names | |
| const int maxNameLength = 128; | |
| if (safeName.Length > maxNameLength) | |
| { | |
| NotificationService.Notify(NotificationSeverity.Warning, "Save Skipped", | |
| $"The project name is too long (>{maxNameLength} characters) after sanitization.", duration: 4000); | |
| return; | |
| } | |
| // Windows reserved device names (case-insensitive) | |
| var reservedNames = new[] | |
| { | |
| "CON", "PRN", "AUX", "NUL", | |
| "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", | |
| "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" | |
| }; | |
| if (reservedNames.Contains(safeName, StringComparer.OrdinalIgnoreCase)) | |
| { | |
| NotificationService.Notify(NotificationSeverity.Warning, "Save Skipped", | |
| "The project name is reserved by the operating system and cannot be used.", duration: 4000); | |
| return; | |
| } | |
| try | |
| { | |
| await ProjectLoader.SaveProjectAsync(safeName, ProjectState.Configuration); | |
| NotificationService.Notify(NotificationSeverity.Success, "Class Renamed", | |
| $"'{args.OldName}' renamed to '{args.NewName}'.", duration: 3000); | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.LogError(ex, "Failed to save project '{ProjectName}' after renaming class from '{OldName}' to '{NewName}'.", projectName, args.OldName, args.NewName); | |
| NotificationService.Notify(NotificationSeverity.Error, "Save Failed", | |
| "The project could not be saved after renaming the class. Please try again.", duration: 5000); |
| | Archivo | Cambio | | ||
| |---------|--------| | ||
| | `ProjectStateService.cs` | Nuevo metodo `RenameClass()` | | ||
| | `LandCoverClassPanel.razor` | Boton edit + inline editing | | ||
| | `ProjectSetup.razor` | Handler para rename callback | | ||
| | Tests nuevos | Rename cascading + validacion | |
There was a problem hiding this comment.
This markdown table uses double pipes (||) at the start of each row, which doesn't render as a standard GitHub-flavored markdown table. Use single leading/trailing | per row (consistent with other epic docs) so the table formats correctly.
|
|
||
| _service.TrainingSession!.Statistics.Should().ContainKey("Lake"); | ||
| _service.TrainingSession.Statistics.Should().NotContainKey("Water"); | ||
| _service.TrainingSession.ClassNames.Should().Contain("Lake"); |
There was a problem hiding this comment.
The TrainingSession rename test verifies the dictionary key and ClassNames list, but it doesn't assert that the underlying SpectralStatistics object was updated (SpectralStatistics.ClassName). Adding an assertion for Statistics["Lake"].ClassName == "Lake" would catch internal inconsistencies during persistence (TrainingSessionDto serializes kvp.Value.ClassName).
| _service.TrainingSession.ClassNames.Should().Contain("Lake"); | |
| _service.TrainingSession.ClassNames.Should().Contain("Lake"); | |
| _service.TrainingSession.Statistics["Lake"].ClassName.Should().Be("Lake"); |
Inline rename on the Training page class list — edit button per class card, input field with Enter/Escape/blur. Propagates rename via ProjectStateService.RenameClass() and updates local UI lists (classCards, classOptions, samples, regions, selectedClass). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…licate UX - Rebuild SpectralStatistics with new ClassName during rename to keep internal ClassName consistent with dictionary key (fixes persistence inconsistency in TrainingSessionDto.FromSession) - Duplicate name rename now flows through OnClassRenamed so the parent shows a warning notification instead of failing silently - Broaden error handling in OnClassRenamed (ProjectSetup + Training) to catch I/O errors from SaveProjectAsync, preventing Blazor circuit crash - Add ClassName assertion to RenameClass_UpdatesTrainingSession test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…name RenameClass() now sets _classificationResult and _confusionMatrix to null after rename. These artifacts contain class names in string arrays and dictionaries that would become inconsistent. Invalidating forces re-run which is safer than attempting in-place re-mapping of 2D arrays. Adds 2 tests: RenameClass_InvalidatesClassificationResult and RenameClass_InvalidatesConfusionMatrix. All 497 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
RenameClass()method toProjectStateServicewith cascading atomic rename across all state (configuration, training samples, training regions, training session statistics, class colors)LandCoverClassPanel— edit button per row + double-click on name, Enter/Escape/blur to confirm/cancelProjectSetuppage with auto-persist and user notificationsDetails
Land cover classes used name-as-key throughout the system, making rename impossible. This PR adds a
RenameClass(oldName, newName)method that atomically updates all references in a singleBeginBatch/EndBatchblock, triggering exactly oneOnStateChangedevent. Auto-persistence is handled by the existingProjectPersistenceServicedebounce mechanism.Epic docs: docs/epics/epic-012-class-rename/
Test plan
dotnet buildpasses (0 errors)🤖 Generated with Claude Code