Skip to content

Epic #12: Add class rename support#59

Merged
ivanrlg merged 8 commits intomainfrom
feature/epic-012-class-rename
Apr 2, 2026
Merged

Epic #12: Add class rename support#59
ivanrlg merged 8 commits intomainfrom
feature/epic-012-class-rename

Conversation

@ivanrlg
Copy link
Copy Markdown
Owner

@ivanrlg ivanrlg commented Apr 2, 2026

Summary

  • Add RenameClass() method to ProjectStateService with cascading atomic rename across all state (configuration, training samples, training regions, training session statistics, class colors)
  • Add inline edit UI to LandCoverClassPanel — edit button per row + double-click on name, Enter/Escape/blur to confirm/cancel
  • Wire rename through ProjectSetup page with auto-persist and user notifications
  • Add 11 unit tests covering rename propagation, validation (duplicates, empty, non-existent), no-op same-name, and single event firing

Details

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 single BeginBatch/EndBatch block, triggering exactly one OnStateChanged event. Auto-persistence is handled by the existing ProjectPersistenceService debounce mechanism.

Epic docs: docs/epics/epic-012-class-rename/

Test plan

  • dotnet build passes (0 errors)
  • All 495 tests pass (349 Core + 130 Web + 16 CLI)
  • 11 new tests for rename cascading and validation
  • Manual: open ProjectSetup, click edit button on a class, rename it, verify training data updates
  • Manual: try duplicate name — should show warning notification
  • Manual: verify rename persists after page refresh

🤖 Generated with Claude Code

Ivan Singleton and others added 5 commits April 1, 2026 20:07
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>
Copilot AI review requested due to automatic review settings April 2, 2026 01:26
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LandCoverClassPanel and connected it to ProjectSetup for 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.

Comment on lines +215 to +226
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);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +82
if (string.IsNullOrWhiteSpace(newName) || newName == originalName)
return;

// Check for duplicates locally
if (Classes.Any(c => c.Name == newName))
return;

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +473

// 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);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +41
| Archivo | Cambio |
|---------|--------|
| `ProjectStateService.cs` | Nuevo metodo `RenameClass()` |
| `LandCoverClassPanel.razor` | Boton edit + inline editing |
| `ProjectSetup.razor` | Handler para rename callback |
| Tests nuevos | Rename cascading + validacion |
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

_service.TrainingSession!.Statistics.Should().ContainKey("Lake");
_service.TrainingSession.Statistics.Should().NotContainKey("Water");
_service.TrainingSession.ClassNames.Should().Contain("Lake");
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
_service.TrainingSession.ClassNames.Should().Contain("Lake");
_service.TrainingSession.ClassNames.Should().Contain("Lake");
_service.TrainingSession.Statistics["Lake"].ClassName.Should().Be("Lake");

Copilot uses AI. Check for mistakes.
Ivan Singleton and others added 3 commits April 1, 2026 23:42
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>
@ivanrlg ivanrlg merged commit 45f08a8 into main Apr 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants