The dialog for file attachments will only open once.

This commit is contained in:
hart_s3 2026-04-14 14:35:17 +02:00
parent 09258c7548
commit 3d03d36f0c
8 changed files with 146 additions and 78 deletions

View File

@ -21,6 +21,7 @@
<MudTooltip Text="@T("Number of attachments")" Placement="Placement.Bottom"> <MudTooltip Text="@T("Number of attachments")" Placement="Placement.Bottom">
<MudBadge Content="@this.Content.FileAttachments.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header"> <MudBadge Content="@this.Content.FileAttachments.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header">
<MudIconButton Icon="@Icons.Material.Filled.AttachFile" <MudIconButton Icon="@Icons.Material.Filled.AttachFile"
Disabled="@this.isOpeningAttachmentsDialog"
OnClick="@this.OpenAttachmentsDialog"/> OnClick="@this.OpenAttachmentsDialog"/>
</MudBadge> </MudBadge>
</MudTooltip> </MudTooltip>

View File

@ -102,6 +102,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
private ElementReference mathContentContainer; private ElementReference mathContentContainer;
private string lastMathRenderSignature = string.Empty; private string lastMathRenderSignature = string.Empty;
private bool hasActiveMathContainer; private bool hasActiveMathContainer;
private bool isOpeningAttachmentsDialog;
private bool isDisposed; private bool isDisposed;
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -600,8 +601,20 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
private async Task OpenAttachmentsDialog() private async Task OpenAttachmentsDialog()
{ {
var result = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.Content.FileAttachments.ToHashSet()); if (this.isOpeningAttachmentsDialog)
this.Content.FileAttachments = result.ToList(); return;
this.isOpeningAttachmentsDialog = true;
try
{
var result = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.Content.FileAttachments.ToHashSet());
this.Content.FileAttachments = result.ToList();
}
finally
{
this.isOpeningAttachmentsDialog = false;
}
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()

View File

@ -6,11 +6,11 @@
@if (this.isDraggingOver) @if (this.isDraggingOver)
{ {
<MudBadge <MudBadge
Content="@this.DocumentPaths.Count" Content="@this.DocumentPaths.Count"
Color="Color.Primary" Color="Color.Primary"
Overlap="true" Overlap="true"
Class="cursor-pointer" Class="cursor-pointer"
OnClick="@this.OpenAttachmentsDialog"> OnClick="@this.OpenAttachmentsDialog">
<MudLink OnClick="@this.AddFilesManually" Style="text-decoration: none;"> <MudLink OnClick="@this.AddFilesManually" Style="text-decoration: none;">
<MudTextField T="string" <MudTextField T="string"
Text="@DROP_FILES_HERE_TEXT" Text="@DROP_FILES_HERE_TEXT"
@ -19,6 +19,7 @@
Typo="Typo.body2" Typo="Typo.body2"
Variant="Variant.Outlined" Variant="Variant.Outlined"
ReadOnly="true" ReadOnly="true"
Disabled="@(this.isOpeningFileDialog || this.isOpeningAttachmentsDialog)"
/> />
</MudLink> </MudLink>
</MudBadge> </MudBadge>
@ -35,6 +36,7 @@
<MudIconButton <MudIconButton
Icon="@Icons.Material.Filled.AttachFile" Icon="@Icons.Material.Filled.AttachFile"
Color="Color.Default" Color="Color.Default"
Disabled="@(this.isOpeningFileDialog || this.isOpeningAttachmentsDialog)"
OnClick="@this.AddFilesManually"/> OnClick="@this.AddFilesManually"/>
</MudBadge> </MudBadge>
</MudTooltip> </MudTooltip>
@ -45,6 +47,7 @@
<MudIconButton <MudIconButton
Icon="@Icons.Material.Filled.AttachFile" Icon="@Icons.Material.Filled.AttachFile"
Color="Color.Default" Color="Color.Default"
Disabled="@(this.isOpeningFileDialog || this.isOpeningAttachmentsDialog)"
OnClick="@this.AddFilesManually"/> OnClick="@this.AddFilesManually"/>
</MudTooltip> </MudTooltip>
} }
@ -60,6 +63,7 @@ else
Variant="Variant.Filled" Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add" StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary" Color="Color.Primary"
Disabled="@(this.isOpeningFileDialog || this.isOpeningAttachmentsDialog)"
OnClick="@(() => this.AddFilesManually())" OnClick="@(() => this.AddFilesManually())"
Style="vertical-align: top; margin-top: -2px;" Style="vertical-align: top; margin-top: -2px;"
Size="Size.Small"> Size="Size.Small">

View File

@ -78,6 +78,8 @@ public partial class AttachDocuments : MSGComponentBase
private uint numDropAreasAboveThis; private uint numDropAreasAboveThis;
private bool isComponentHovered; private bool isComponentHovered;
private bool isDraggingOver; private bool isDraggingOver;
private bool isOpeningFileDialog;
private bool isOpeningAttachmentsDialog;
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
@ -202,40 +204,64 @@ public partial class AttachDocuments : MSGComponentBase
private async Task AddFilesManually() private async Task AddFilesManually()
{ {
if (this.isOpeningFileDialog)
return;
this.isOpeningFileDialog = true;
try
{
// Ensure that Pandoc is installed and ready: // Ensure that Pandoc is installed and ready:
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync( var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false, showSuccessMessage: false,
showDialog: true); showDialog: true);
// If Pandoc is not available (user cancelled installation), abort file selection: // If Pandoc is not available (user cancelled installation), abort file selection:
if (!pandocState.IsAvailable) if (!pandocState.IsAvailable)
{ {
this.Logger.LogWarning("The user cancelled the Pandoc installation or Pandoc is not available. Aborting file selection."); this.Logger.LogWarning("The user cancelled the Pandoc installation or Pandoc is not available. Aborting file selection.");
return; return;
}
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
if (selectFiles.UserCancelled)
return;
foreach (var selectedFilePath in selectFiles.SelectedFilePaths)
{
if (!File.Exists(selectedFilePath))
continue;
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.ValidateMediaFileTypes, this.Provider))
continue;
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
}
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
} }
finally
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
if (selectFiles.UserCancelled)
return;
foreach (var selectedFilePath in selectFiles.SelectedFilePaths)
{ {
if (!File.Exists(selectedFilePath)) this.isOpeningFileDialog = false;
continue;
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.ValidateMediaFileTypes, this.Provider))
continue;
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
} }
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
} }
private async Task OpenAttachmentsDialog() private async Task OpenAttachmentsDialog()
{ {
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths); if (this.isOpeningAttachmentsDialog)
return;
this.isOpeningAttachmentsDialog = true;
try
{
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths);
}
finally
{
this.isOpeningAttachmentsDialog = false;
}
} }
private async Task ClearAllFiles() private async Task ClearAllFiles()

View File

@ -1,5 +1,5 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<MudButton StartIcon="@Icons.Material.Filled.Description" OnClick="@(async () => await this.SelectFile())" Variant="Variant.Filled" Class="mb-3" Disabled="@this.Disabled"> <MudButton StartIcon="@Icons.Material.Filled.Description" OnClick="@(async () => await this.SelectFile())" Variant="Variant.Filled" Class="mb-3" Disabled="@(this.Disabled || this.isSelectingFile)">
@if (string.IsNullOrWhiteSpace(this.Text)) @if (string.IsNullOrWhiteSpace(this.Text))
{ {
@T("Use file content as input") @T("Use file content as input")

View File

@ -31,52 +31,63 @@ public partial class ReadFileContent : MSGComponentBase
[Inject] [Inject]
private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!; private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!;
private bool isSelectingFile;
private async Task SelectFile() private async Task SelectFile()
{ {
if (this.Disabled) if (this.Disabled || this.isSelectingFile)
return; return;
// Ensure that Pandoc is installed and ready: this.isSelectingFile = true;
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false,
showDialog: true);
// Check if Pandoc is available after the check / installation:
if (!pandocState.IsAvailable)
{
this.Logger.LogWarning("The user cancelled the Pandoc installation or Pandoc is not available. Aborting file selection.");
return;
}
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
if (selectedFile.UserCancelled)
{
this.Logger.LogInformation("User cancelled the file selection");
return;
}
if(!File.Exists(selectedFile.SelectedFilePath))
{
this.Logger.LogWarning("Selected file does not exist: '{FilePath}'", selectedFile.SelectedFilePath);
return;
}
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.DIRECTLY_LOADING_CONTENT, selectedFile.SelectedFilePath))
{
this.Logger.LogWarning("User attempted to load unsupported file: {FilePath}", selectedFile.SelectedFilePath);
return;
}
try try
{ {
var fileContent = await UserFile.LoadFileData(selectedFile.SelectedFilePath, this.RustService, this.DialogService); // Ensure that Pandoc is installed and ready:
await this.FileContentChanged.InvokeAsync(fileContent); var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
this.Logger.LogInformation("Successfully loaded file content: {FilePath}", selectedFile.SelectedFilePath); showSuccessMessage: false,
showDialog: true);
// Check if Pandoc is available after the check / installation:
if (!pandocState.IsAvailable)
{
this.Logger.LogWarning("The user cancelled the Pandoc installation or Pandoc is not available. Aborting file selection.");
return;
}
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
if (selectedFile.UserCancelled)
{
this.Logger.LogInformation("User cancelled the file selection");
return;
}
if(!File.Exists(selectedFile.SelectedFilePath))
{
this.Logger.LogWarning("Selected file does not exist: '{FilePath}'", selectedFile.SelectedFilePath);
return;
}
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.DIRECTLY_LOADING_CONTENT, selectedFile.SelectedFilePath))
{
this.Logger.LogWarning("User attempted to load unsupported file: {FilePath}", selectedFile.SelectedFilePath);
return;
}
try
{
var fileContent = await UserFile.LoadFileData(selectedFile.SelectedFilePath, this.RustService, this.DialogService);
await this.FileContentChanged.InvokeAsync(fileContent);
this.Logger.LogInformation("Successfully loaded file content: {FilePath}", selectedFile.SelectedFilePath);
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Failed to load file content: {FilePath}", selectedFile.SelectedFilePath);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, T("Failed to load file content")));
}
} }
catch (Exception ex) finally
{ {
this.Logger.LogError(ex, "Failed to load file content: {FilePath}", selectedFile.SelectedFilePath); this.isSelectingFile = false;
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, T("Failed to load file content")));
} }
} }
} }

View File

@ -13,7 +13,7 @@
Variant="Variant.Outlined" Variant="Variant.Outlined"
/> />
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog"> <MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isOpeningFileDialog)" OnClick="@this.OpenFileDialog">
@T("Choose File") @T("Choose File")
</MudButton> </MudButton>
</MudStack> </MudStack>

View File

@ -35,6 +35,7 @@ public partial class SelectFile : MSGComponentBase
protected ILogger<SelectFile> Logger { get; init; } = null!; protected ILogger<SelectFile> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
private bool isOpeningFileDialog;
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -55,10 +56,22 @@ public partial class SelectFile : MSGComponentBase
private async Task OpenFileDialog() private async Task OpenFileDialog()
{ {
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File); if (this.Disabled || this.isOpeningFileDialog)
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'."); return;
if (!response.UserCancelled) this.isOpeningFileDialog = true;
this.InternalFileChanged(response.SelectedFilePath);
try
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
if (!response.UserCancelled)
this.InternalFileChanged(response.SelectedFilePath);
}
finally
{
this.isOpeningFileDialog = false;
}
} }
} }