using Microsoft.AspNetCore.Components; namespace AIStudio.Components; /// /// A compact, two-group editor for the exclusion string shared by folder-based features. /// public partial class FileExclusionEditor : MSGComponentBase { private static readonly Dictionary INPUT_ATTRIBUTES = new() { ["spellcheck"] = false, ["autocorrect"] = "off", ["autocapitalize"] = "off", }; [Parameter] public string? Text { get; set; } = string.Empty; [Parameter] public EventCallback TextChanged { get; set; } [Parameter] public string Label { get; set; } = string.Empty; [Parameter] public bool Disabled { get; set; } [Parameter] public bool ShowReset { get; set; } = true; [Parameter] public bool ShowHelp { get; set; } = true; [Parameter] public string ResetValue { get; set; } = FileExclusions.DEFAULT_PATTERNS; [Parameter] public string ResetButtonText { get; set; } = string.Empty; /// /// Immediately persists the combined value after restoring just one of its groups. /// Ordinary edits continue through TextChanged. /// [Parameter] public EventCallback ResetRequested { get; set; } private readonly FileExclusionEditorState state = new(); private readonly string folderInputId = $"exclusion-folders-{Guid.NewGuid():N}"; private readonly string fileInputId = $"exclusion-files-{Guid.NewGuid():N}"; private bool showHelp; /// /// False while a field holds an invalid draft. The editor keeps such a draft to itself, so the /// bound text is still the last valid one and cannot tell. /// public bool IsValid => this.state.IsValid; private string EditorLabel => string.IsNullOrWhiteSpace(this.Label) ? T("Excluded files and folders") : this.Label; private string RestoreLabel => string.IsNullOrWhiteSpace(this.ResetButtonText) ? T("Restore default") : this.ResetButtonText; private string FolderRestoreLabel => $"{this.RestoreLabel}: {T("Folders")}"; private string FileRestoreLabel => $"{this.RestoreLabel}: {T("Files")}"; protected override void OnParametersSet() { this.state.Receive(this.Text, discardDraft: this.Disabled); base.OnParametersSet(); } protected Task ChangeFoldersAsync(string? text) => this.ChangePatternsAsync(text ?? string.Empty, folders: true); protected Task ChangeFilesAsync(string? text) => this.ChangePatternsAsync(text ?? string.Empty, folders: false); private async Task ChangePatternsAsync(string text, bool folders) { if (this.Disabled) return; var previousText = this.state.Text; var wasValid = this.state.IsValid; if (folders) this.state.EditFolders(text); else this.state.EditFiles(text); // Moving a rule to the correct field can keep the combined text identical. It must still // publish the now-valid draft, because the previous, invalid arrangement was not sent. if (this.state.IsValid && (!wasValid || !string.Equals(previousText, this.state.Text, StringComparison.Ordinal))) await this.TextChanged.InvokeAsync(this.state.Text); } protected async Task RestoreDefaultsAsync(bool folders) { if (this.Disabled || !this.ShowReset) return; if (folders) this.state.RestoreFolders(this.ResetValue); else this.state.RestoreFiles(this.ResetValue); if (!this.state.IsValid) return; if (this.ResetRequested.HasDelegate) await this.ResetRequested.InvokeAsync(this.state.Text); else await this.TextChanged.InvokeAsync(this.state.Text); } private void ToggleHelp() => this.showHelp = !this.showHelp; }