feat: add MergeModal shared component for merge destination selection

This commit is contained in:
2026-07-16 19:28:45 +02:00
parent 2c292d2d81
commit ff2ed4f7cd
@@ -0,0 +1,99 @@
<ModalFrame Title="@GetTitle()" Show="Show" OnClose="OnClose" Size="normal">
<Body>
<div class="d-flex flex-column gap-3">
<div class="alert alert-warning py-2 mb-0">
<i class="bi bi-exclamation-triangle me-1"></i>
This action is irreversible. Merged @LabelLower data will be reassigned to the destination.
</div>
<p class="text-muted mb-0">Select the destination @LabelLower to keep:</p>
@foreach (var item in Items) {
<div class="form-check">
<input class="form-check-input" type="radio"
name="merge-destination"
id="merge-@item.Id"
value="@item.Id"
checked="@(DestinationId == item.Id)"
@onchange="() => SelectDestination(item.Id)" />
<label class="form-check-label" for="merge-@item.Id">
<strong>@item.Name</strong>
<span class="text-muted">(@item.Subtitle)</span>
</label>
</div>
}
</div>
</Body>
<Footer>
<button class="btn btn-secondary" @onclick="OnClose">Cancel</button>
<button class="btn btn-warning" @onclick="OnMerge" disabled="@(!DestinationId.HasValue)">
<i class="bi bi-arrow-right-circle"></i> Merge
</button>
</Footer>
</ModalFrame>
@code {
/// <summary>
/// Represents an item in the merge list.
/// </summary>
public sealed class MergeItem {
/// <summary>
/// Gets or sets the item ID.
/// </summary>
public required Guid Id { get; set; }
/// <summary>
/// Gets or sets the item display name.
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Gets or sets the item subtitle (e.g. asset count).
/// </summary>
public string Subtitle { get; set; } = string.Empty;
}
/// <summary>
/// Gets or sets whether the modal is visible.
/// </summary>
[Parameter] public bool Show { get; set; }
/// <summary>
/// Gets or sets the label for the item type (e.g. "Album", "Cosplayer").
/// </summary>
[Parameter] public string ItemLabel { get; set; } = "Item";
/// <summary>
/// Gets or sets the list of items available for merge.
/// </summary>
[Parameter] public required List<MergeItem> Items { get; set; }
/// <summary>
/// Gets or sets the callback invoked when the user confirms the merge.
/// Passes the selected destination ID and the full list of source IDs.
/// </summary>
[Parameter] public EventCallback<(Guid DestinationId, List<Guid> SourceIds)> OnMergeConfirmed { get; set; }
/// <summary>
/// Gets or sets the callback invoked when the modal is closed.
/// </summary>
[Parameter] public EventCallback OnClose { get; set; }
Guid? DestinationId;
string GetTitle() => $"Merge {ItemLabel}(s)";
string LabelLower => ItemLabel.ToLowerInvariant();
protected override void OnParametersSet() {
if (Show && Items.Count > 0 && DestinationId is null) {
DestinationId = Items[0].Id;
}
}
void SelectDestination(Guid id) {
DestinationId = id;
}
async Task OnMerge() {
if (!DestinationId.HasValue) return;
var sourceIds = Items.Where(i => i.Id != DestinationId.Value).Select(i => i.Id).ToList();
await OnMergeConfirmed.InvokeAsync((DestinationId.Value, sourceIds));
}
}