feat: add bulk delete and visibility change to AlbumDetail select mode
- Fix AssetController.BulkUpdate/BulkDelete — add missing Save() call - Add BulkUpdateAssetsAsync and BulkDeleteAssetsAsync to AssetService - Rename 'Remove N' → 'Unlink N' (removes from album, original behavior) - Add 'Delete N' button — soft-deletes selected assets via BulkDelete - Add 'Visibility' button — opens ModalFrame to pick Public/Protected/Private - Apply calls BulkUpdateAssetsAsync with chosen visibility level
This commit is contained in:
@@ -207,6 +207,7 @@ public class AssetController(
|
||||
);
|
||||
|
||||
assetRepository.UpdateBulk(enumerable);
|
||||
assetRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -273,6 +274,7 @@ public class AssetController(
|
||||
enumerable.ForEach(x => x.DeletedAt = DateTime.UtcNow);
|
||||
|
||||
assetRepository.UpdateBulk(enumerable);
|
||||
assetRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@page "/albums/{Id:guid}"
|
||||
@using Butter.Dtos.Album
|
||||
@using Butter.Dtos.Asset
|
||||
@using Butter.Types
|
||||
@using Microsoft.Extensions.Options
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@inject AlbumService albumService
|
||||
@inject AssetService assetService
|
||||
@inject LoginService loginService
|
||||
@inject IJSRuntime jsRuntime
|
||||
@inject IOptions<ServiceOptions> serviceOptions
|
||||
@@ -56,10 +58,16 @@
|
||||
@* Desktop: inline buttons *@
|
||||
<div class="d-none d-md-flex gap-1 align-items-center">
|
||||
@if (selectMode) {
|
||||
<button class="btn btn-sm btn-danger" @onclick="RemoveSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-trash"></i> Remove @selectedImageIds.Count
|
||||
<button class="btn btn-sm btn-secondary" @onclick="UnlinkSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-link-45deg"></i> Unlink @selectedImageIds.Count
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" @onclick="CancelSelectMode">
|
||||
<button class="btn btn-sm btn-danger" @onclick="DeleteSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-trash"></i> Delete @selectedImageIds.Count
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" @onclick="OpenVisibilityModal" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-eye"></i> Visibility
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @onclick="CancelSelectMode">
|
||||
Done
|
||||
</button>
|
||||
} else {
|
||||
@@ -79,8 +87,18 @@
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
@if (selectMode) {
|
||||
<li>
|
||||
<button class="dropdown-item text-danger" @onclick="RemoveSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-trash"></i> Remove @selectedImageIds.Count
|
||||
<button class="dropdown-item" @onclick="UnlinkSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-link-45deg"></i> Unlink @selectedImageIds.Count
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item text-danger" @onclick="DeleteSelectedImages" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-trash"></i> Delete @selectedImageIds.Count
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item" @onclick="OpenVisibilityModal" disabled="@(selectedImageIds.Count == 0)">
|
||||
<i class="bi bi-eye"></i> Visibility
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -171,6 +189,39 @@
|
||||
</ImagePreview>
|
||||
}
|
||||
|
||||
@if (showVisibilityModal) {
|
||||
<ModalFrame Title="Change Visibility" Show="true" OnClose="CloseVisibilityModal" Size="normal">
|
||||
<Body>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<p class="text-muted mb-0">Select the visibility level for the @selectedImageIds.Count selected asset(s):</p>
|
||||
@foreach (var vis in new[] { EVisibility.Public, EVisibility.Protected, EVisibility.Private }) {
|
||||
<button class="btn @(selectedVisibility == vis ? "btn-primary" : "btn-outline-primary") w-100 text-start"
|
||||
@onclick="() => selectedVisibility = vis">
|
||||
@switch (vis) {
|
||||
case EVisibility.Public:
|
||||
<i class="bi bi-globe me-2"></i>
|
||||
<span>Public</span>
|
||||
break;
|
||||
case EVisibility.Protected:
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
<span>Protected</span>
|
||||
break;
|
||||
case EVisibility.Private:
|
||||
<i class="bi bi-lock me-2"></i>
|
||||
<span>Private</span>
|
||||
break;
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</Body>
|
||||
<Footer>
|
||||
<button class="btn btn-secondary" @onclick="CloseVisibilityModal">Cancel</button>
|
||||
<button class="btn btn-primary" @onclick="ApplyVisibility">Apply</button>
|
||||
</Footer>
|
||||
</ModalFrame>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Guid Id { get; set; }
|
||||
@@ -179,6 +230,8 @@
|
||||
bool isLoading = true;
|
||||
bool showForm;
|
||||
bool selectMode;
|
||||
bool showVisibilityModal;
|
||||
EVisibility selectedVisibility = EVisibility.Public;
|
||||
HashSet<Guid> selectedImageIds = [];
|
||||
Guid? selectedAssetId;
|
||||
int selectedIndex = -1;
|
||||
@@ -283,7 +336,7 @@
|
||||
selectedImageIds.Add(id);
|
||||
}
|
||||
|
||||
async Task RemoveSelectedImages() {
|
||||
async Task UnlinkSelectedImages() {
|
||||
if (album == null || selectedImageIds.Count == 0) return;
|
||||
var remaining = album.Images.Where(id => !selectedImageIds.Contains(id)).ToList();
|
||||
await albumService.UpdateAlbumAsync(album.Id, new AlbumUpdateDto { Assets = remaining });
|
||||
@@ -292,6 +345,32 @@
|
||||
await LoadAlbum();
|
||||
}
|
||||
|
||||
async Task DeleteSelectedImages() {
|
||||
if (album == null || selectedImageIds.Count == 0) return;
|
||||
await assetService.BulkDeleteAssetsAsync([.. selectedImageIds]);
|
||||
selectMode = false;
|
||||
selectedImageIds.Clear();
|
||||
await LoadAlbum();
|
||||
}
|
||||
|
||||
void OpenVisibilityModal() {
|
||||
showVisibilityModal = true;
|
||||
selectedVisibility = EVisibility.Public;
|
||||
}
|
||||
|
||||
void CloseVisibilityModal() {
|
||||
showVisibilityModal = false;
|
||||
}
|
||||
|
||||
async Task ApplyVisibility() {
|
||||
if (album == null || selectedImageIds.Count == 0) return;
|
||||
await assetService.BulkUpdateAssetsAsync([.. selectedImageIds], new AssetUpdateDto { Visibility = selectedVisibility });
|
||||
showVisibilityModal = false;
|
||||
selectMode = false;
|
||||
selectedImageIds.Clear();
|
||||
await LoadAlbum();
|
||||
}
|
||||
|
||||
string GetTileClass(bool isSelected) {
|
||||
if (!selectMode) return "masonry-tile";
|
||||
return isSelected ? "masonry-tile masonry-tile-selected" : "masonry-tile";
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace MilkStream.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to asset listing and searching via the API.
|
||||
/// Provides access to asset listing, searching, and bulk operations via the API.
|
||||
/// </summary>
|
||||
public sealed class AssetService(
|
||||
IOptions<ServiceOptions> options,
|
||||
@@ -39,4 +39,27 @@ public sealed class AssetService(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk updates selected assets (e.g. visibility, soft-delete).
|
||||
/// </summary>
|
||||
/// <param name="ids">The asset IDs to update.</param>
|
||||
/// <param name="dto">The shared update values.</param>
|
||||
public async Task<bool> BulkUpdateAssetsAsync(List<Guid> ids, AssetUpdateDto dto) {
|
||||
var payload = new BulkDto<AssetUpdateDto> { Ids = ids, Data = dto };
|
||||
var response = await Client.PostAsJsonAsync("/api/asset", payload);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk soft-deletes selected assets.
|
||||
/// </summary>
|
||||
/// <param name="ids">The asset IDs to delete.</param>
|
||||
public async Task<bool> BulkDeleteAssetsAsync(List<Guid> ids) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/asset") {
|
||||
Content = JsonContent.Create(ids)
|
||||
};
|
||||
var response = await Client.SendAsync(request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user