mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-03 22:44:49 -04:00
Show subfolders in all item breadcrumb paths (#1695)
This commit is contained in:
@@ -468,12 +468,9 @@ else
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
if (EditMode)
|
||||
{
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"], Url = $"/items/{Id}" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["EditItemBreadcrumb"] });
|
||||
}
|
||||
else
|
||||
// In edit mode, we'll build breadcrumbs after loading the item in LoadExistingCredential
|
||||
// so we can include folder breadcrumbs
|
||||
if (!EditMode)
|
||||
{
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["AddNewItemBreadcrumb"] });
|
||||
}
|
||||
@@ -903,6 +900,15 @@ else
|
||||
|
||||
Obj = ItemEdit.FromEntity(item);
|
||||
|
||||
// Build breadcrumbs with folder hierarchy
|
||||
if (Obj.FolderId.HasValue)
|
||||
{
|
||||
await BuildFolderBreadcrumbsAsync(Obj.FolderId.Value);
|
||||
}
|
||||
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"], Url = $"/items/{Id}" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["EditItemBreadcrumb"] });
|
||||
|
||||
// Track initially visible fields
|
||||
foreach (var fieldDef in SystemFieldRegistry.GetFieldsForItemType(Obj.ItemType))
|
||||
{
|
||||
|
||||
@@ -54,8 +54,7 @@ else
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { Url = "items/" + Id, DisplayName = Localizer["ViewItemBreadcrumb"] });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["DeleteItemBreadcrumb"] });
|
||||
// Breadcrumb items will be added after loading the item in OnAfterRenderAsync
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -68,6 +67,15 @@ else
|
||||
// Load existing Obj, retrieve from service
|
||||
Obj = await ItemService.LoadEntryAsync(Id);
|
||||
|
||||
// Build breadcrumbs with folder hierarchy
|
||||
if (Obj?.FolderId.HasValue == true)
|
||||
{
|
||||
await BuildFolderBreadcrumbsAsync(Obj.FolderId.Value);
|
||||
}
|
||||
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { Url = "items/" + Id, DisplayName = Localizer["ViewItemBreadcrumb"] });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["DeleteItemBreadcrumb"] });
|
||||
|
||||
// Hide loading spinner
|
||||
IsLoading = false;
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
@page "/items/folder/{FolderId:guid}"
|
||||
@inherits MainBase
|
||||
@inject ItemService ItemService
|
||||
@inject FolderService FolderService
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@using AliasVault.RazorComponents.Models
|
||||
@using AliasVault.Client.Main.Models
|
||||
@using AliasVault.Client.Main.Components.Items
|
||||
@using AliasClientDb.Models
|
||||
@using AliasVault.Client.Main.Components.Folders
|
||||
@using AliasVault.Client.Main.Utilities
|
||||
@using AliasClientDb
|
||||
@using Microsoft.Extensions.Localization
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@@ -183,27 +185,30 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Folders section - only show at root level when ShowFolders is enabled *@
|
||||
@if (!IsInFolder && !IsSearching && ShowFolders)
|
||||
@* Folders section - show at root OR inside folders to display subfolders *@
|
||||
@if (!IsSearching && ShowFolders)
|
||||
{
|
||||
<div class="flex flex-wrap items-center gap-2 mb-4">
|
||||
@foreach (var folder in Folders)
|
||||
@foreach (var folder in CurrentLevelFolders)
|
||||
{
|
||||
<FolderPill Folder="@folder" OnClick="() => NavigateToFolder(folder.Id)" />
|
||||
}
|
||||
<button @onclick="ShowCreateFolderModal" class="@GetAddFolderButtonClass()">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<svg class="w-3.5 h-3.5 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
@if (Folders.Count == 0)
|
||||
{
|
||||
<span>@Localizer["NewFolder"]</span>
|
||||
}
|
||||
</button>
|
||||
@if (CanCreateSubfolder)
|
||||
{
|
||||
<button @onclick="ShowCreateFolderModal" class="@GetAddFolderButtonClass()">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<svg class="w-3.5 h-3.5 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
@if (CurrentLevelFolders.Count == 0)
|
||||
{
|
||||
<span>@Localizer["NewFolder"]</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -339,10 +344,49 @@ else
|
||||
private List<ItemListEntry> Items { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folders.
|
||||
/// Gets or sets all folders in the system.
|
||||
/// </summary>
|
||||
private List<FolderWithCount> Folders { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all folders as Folder entities (for breadcrumb path computation).
|
||||
/// </summary>
|
||||
private List<Folder> AllFolders => Folders.Select(f => new Folder
|
||||
{
|
||||
Id = f.Id,
|
||||
Name = f.Name,
|
||||
ParentFolderId = f.ParentFolderId,
|
||||
Weight = f.Weight
|
||||
}).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folders to display at the current level (root folders if not in folder, subfolders if in folder).
|
||||
/// </summary>
|
||||
private List<FolderWithCount> CurrentLevelFolders =>
|
||||
Folders.Where(f => f.ParentFolderId == FolderId).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether we can create a subfolder at the current level.
|
||||
/// Returns true if at root, or if current folder is not at max depth.
|
||||
/// </summary>
|
||||
private bool CanCreateSubfolder
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsInFolder)
|
||||
{
|
||||
return true; // Can always create folders at root
|
||||
}
|
||||
|
||||
if (!FolderId.HasValue || AllFolders.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return FolderTreeUtilities.CanHaveSubfolders(FolderId.Value, AllFolders);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current folder name (when in folder view).
|
||||
/// </summary>
|
||||
@@ -765,11 +809,13 @@ else
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new folder.
|
||||
/// Create a new folder. If inside a folder, creates as subfolder.
|
||||
/// </summary>
|
||||
private async Task CreateFolderAsync(string folderName)
|
||||
{
|
||||
var folderId = await FolderService.CreateAsync(folderName);
|
||||
// Use current folder as parent if we're inside a folder, otherwise null for root
|
||||
var parentFolderId = IsInFolder ? FolderId : null;
|
||||
var folderId = await FolderService.CreateAsync(folderName, parentFolderId);
|
||||
if (folderId != Guid.Empty)
|
||||
{
|
||||
// Reload folders
|
||||
@@ -892,15 +938,21 @@ else
|
||||
var storedShowFolders = await LocalStorage.GetItemAsync<bool?>(ShowFoldersStorageKey);
|
||||
ShowFolders = storedShowFolders ?? true;
|
||||
|
||||
// Load folder name if in folder view
|
||||
// Load folder name and build breadcrumbs if in folder view
|
||||
if (FolderId.HasValue)
|
||||
{
|
||||
var folder = await FolderService.GetByIdAsync(FolderId.Value);
|
||||
CurrentFolderName = folder?.Name ?? string.Empty;
|
||||
|
||||
// Build breadcrumbs for folder hierarchy using shared helper
|
||||
BreadcrumbItems.RemoveAll(b => !b.ShowHomeIcon);
|
||||
AddFolderBreadcrumbs(FolderId, AllFolders, Localizer["Items"], includeCurrentFolder: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentFolderName = string.Empty;
|
||||
// Clear breadcrumbs at root level (only show Home from base class)
|
||||
BreadcrumbItems.RemoveAll(b => !b.ShowHomeIcon);
|
||||
}
|
||||
|
||||
await LoadItemsAsync();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@page "/items/{id:guid}"
|
||||
@inherits MainBase
|
||||
@inject ItemService ItemService
|
||||
@inject FolderService FolderService
|
||||
@implements IAsyncDisposable
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasClientDb
|
||||
@@ -312,10 +311,10 @@ else
|
||||
Folder = await FolderService.GetByIdAsync(Item.FolderId.Value);
|
||||
}
|
||||
|
||||
// Add breadcrumb items - Home is already added by MainBase, add folder if present
|
||||
if (Folder != null)
|
||||
// Add breadcrumb items - Home is already added by MainBase, add folder hierarchy if present
|
||||
if (Item.FolderId.HasValue)
|
||||
{
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Folder.Name, Url = $"/items/folder/{Folder.Id}" });
|
||||
await BuildFolderBreadcrumbsAsync(Item.FolderId.Value);
|
||||
}
|
||||
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"] });
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
namespace AliasVault.Client.Main.Pages;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Client.Main.Utilities;
|
||||
using AliasVault.Client.Services;
|
||||
using AliasVault.Client.Services.Auth;
|
||||
using AliasVault.RazorComponents.Models;
|
||||
@@ -96,6 +99,12 @@ public abstract class MainBase : OwningComponentBase
|
||||
[Inject]
|
||||
public ILocalStorageService LocalStorage { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FolderService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public FolderService FolderService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SharedLocalizer. This is used to access shared resource translations like buttons, etc.
|
||||
/// </summary>
|
||||
@@ -195,6 +204,104 @@ public abstract class MainBase : OwningComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds breadcrumb navigation for folder hierarchy using async folder loading.
|
||||
/// This helper method recursively builds folder breadcrumbs from the given folder up to the root.
|
||||
/// </summary>
|
||||
/// <param name="folderId">The folder ID to build breadcrumbs for.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected async Task BuildFolderBreadcrumbsAsync(Guid folderId)
|
||||
{
|
||||
// Load all folders to build the path
|
||||
var foldersWithCounts = await FolderService.GetAllWithCountsAsync();
|
||||
|
||||
// Convert FolderWithCount to Folder for use with FolderTreeUtilities
|
||||
var allFolders = foldersWithCounts.Select(f => new Folder
|
||||
{
|
||||
Id = f.Id,
|
||||
Name = f.Name,
|
||||
ParentFolderId = f.ParentFolderId,
|
||||
Weight = f.Weight,
|
||||
}).ToList();
|
||||
|
||||
// Get the folder ID path from root to current folder
|
||||
var folderIdPath = FolderTreeUtilities.GetFolderIdPath(folderId, allFolders);
|
||||
|
||||
if (folderIdPath.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get localized "Folder" text
|
||||
var folderLabel = SharedLocalizer["Folder"];
|
||||
|
||||
// Add breadcrumb for each folder in the path (from root to current)
|
||||
foreach (var currentFolderId in folderIdPath)
|
||||
{
|
||||
var folder = allFolders.FirstOrDefault(f => f.Id == currentFolderId);
|
||||
if (folder != null)
|
||||
{
|
||||
BreadcrumbItems.Add(new BreadcrumbItem
|
||||
{
|
||||
DisplayName = $"{folderLabel}: {folder.Name}",
|
||||
Url = $"/items/folder/{folder.Id}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds breadcrumb navigation for folder hierarchy and adds it to BreadcrumbItems.
|
||||
/// This helper method can be called from any page to add folder breadcrumbs.
|
||||
/// </summary>
|
||||
/// <param name="folderId">The folder ID to build breadcrumbs for.</param>
|
||||
/// <param name="allFolders">List of all folders (for path computation).</param>
|
||||
/// <param name="itemsLabel">Label for the "Items" breadcrumb (default: "Items" from localization).</param>
|
||||
/// <param name="includeCurrentFolder">Whether to include the current folder as the last breadcrumb (default: true).</param>
|
||||
protected void AddFolderBreadcrumbs(Guid? folderId, List<Folder> allFolders, string? itemsLabel = null, bool includeCurrentFolder = true)
|
||||
{
|
||||
if (!folderId.HasValue || allFolders.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the folder path (list of folder names from root to current)
|
||||
var folderPath = FolderTreeUtilities.GetFolderPath(folderId, allFolders);
|
||||
var folderIdPath = FolderTreeUtilities.GetFolderIdPath(folderId, allFolders);
|
||||
|
||||
if (folderPath.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add breadcrumb for "Items" (vault home)
|
||||
var itemsLabelText = itemsLabel ?? SharedLocalizer["Items"];
|
||||
BreadcrumbItems.Add(new BreadcrumbItem
|
||||
{
|
||||
DisplayName = itemsLabelText,
|
||||
Url = "/items",
|
||||
});
|
||||
|
||||
// Determine how many folders to add as breadcrumbs
|
||||
int endIndex = includeCurrentFolder ? folderPath.Count : folderPath.Count - 1;
|
||||
|
||||
// Add breadcrumb for each folder in the path
|
||||
for (int i = 0; i < endIndex; i++)
|
||||
{
|
||||
var currentFolderId = folderIdPath[i];
|
||||
var folderName = folderPath[i];
|
||||
|
||||
// Last item should not have a URL if includeCurrentFolder is true
|
||||
bool isLastItem = includeCurrentFolder && i == folderPath.Count - 1;
|
||||
|
||||
BreadcrumbItems.Add(new BreadcrumbItem
|
||||
{
|
||||
DisplayName = folderName,
|
||||
Url = isLastItem ? null : $"/items/folder/{currentFolderId}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the encryption key is set. If not, redirect to the unlock screen
|
||||
/// where the user can re-enter the master password so the encryption key gets refreshed.
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
<value>Find all of your items below.</value>
|
||||
<comment>Page description text</comment>
|
||||
</data>
|
||||
<data name="Items" xml:space="preserve">
|
||||
<value>Items</value>
|
||||
<comment>Items label used in breadcrumb navigation</comment>
|
||||
</data>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<data name="ViewModeLabel" xml:space="preserve">
|
||||
|
||||
@@ -1317,6 +1317,11 @@ video {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
--tw-rotate: 90deg;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@@ -1439,6 +1444,10 @@ video {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -3676,6 +3685,11 @@ video {
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-700:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
|
||||
Reference in New Issue
Block a user