From bcbc66e01062c021b658e7a120d0ca97518219b0 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 19 Dec 2025 15:48:42 +0100 Subject: [PATCH] Add folder selector component (#1404) --- .../Main/Components/Folders/FolderPill.razor | 8 +- .../Components/Items/FolderSelector.razor | 170 ++++++++++++++++++ .../Main/Components/Layout/FormModal.razor | 7 - .../AliasVault.Client/Main/Models/ItemEdit.cs | 7 + .../Main/Pages/Items/AddEdit.razor | 3 + .../Main/Pages/Items/Home.razor | 8 +- .../Main/Pages/Items/View.razor | 28 ++- .../Main/Items/FolderSelector.en.resx | 43 +++++ .../wwwroot/css/tailwind.css | 19 ++ 9 files changed, 277 insertions(+), 16 deletions(-) create mode 100644 apps/server/AliasVault.Client/Main/Components/Items/FolderSelector.razor create mode 100644 apps/server/AliasVault.Client/Resources/Components/Main/Items/FolderSelector.en.resx diff --git a/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor index 9696ab496..b271cb4c4 100644 --- a/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor +++ b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor @@ -1,12 +1,12 @@ @using AliasVault.Client.Main.Models @* FolderPill component - displays a folder as a compact clickable pill *@ - @code { diff --git a/apps/server/AliasVault.Client/Main/Components/Items/FolderSelector.razor b/apps/server/AliasVault.Client/Main/Components/Items/FolderSelector.razor new file mode 100644 index 000000000..9d03b3cde --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Items/FolderSelector.razor @@ -0,0 +1,170 @@ +@using AliasVault.Client.Main.Components.Layout +@using AliasVault.Client.Main.Models +@using Microsoft.Extensions.Localization + +@inject FolderService FolderService +@inject IStringLocalizerFactory LocalizerFactory + +
+
+
+
+ + + +
+
+

@Localizer["FolderLabel"]

+

+ @if (SelectedFolderId.HasValue) + { + var folder = Folders.FirstOrDefault(f => f.Id == SelectedFolderId.Value); + @folder?.Name + } + else + { + @Localizer["NoFolder"] + } +

+
+
+ +
+
+ +@* Folder Selection Modal *@ + + + + + + + +
+ @* No Folder Option *@ + + + @* Folder Options *@ + @foreach (var folder in Folders) + { + + } + + @if (Folders.Count == 0) + { +

+ @Localizer["NoFoldersAvailable"] +

+ } +
+
+
+ +@code { + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Items.FolderSelector", "AliasVault.Client"); + + /// + /// Gets or sets the currently selected folder ID. + /// + [Parameter] + public Guid? SelectedFolderId { get; set; } + + /// + /// Gets or sets the callback when folder selection changes. + /// + [Parameter] + public EventCallback SelectedFolderIdChanged { get; set; } + + private List Folders { get; set; } = []; + private bool ShowFolderModal { get; set; } + + /// + protected override async Task OnInitializedAsync() + { + await LoadFoldersAsync(); + } + + private async Task LoadFoldersAsync() + { + Folders = await FolderService.GetAllWithCountsAsync(); + } + + private void OpenFolderModal() + { + ShowFolderModal = true; + } + + private Task CloseFolderModal() + { + ShowFolderModal = false; + return Task.CompletedTask; + } + + private async Task SelectFolder(Guid? folderId) + { + SelectedFolderId = folderId; + await SelectedFolderIdChanged.InvokeAsync(folderId); + ShowFolderModal = false; + } + + private string GetFolderButtonClass(Guid? folderId) + { + var isSelected = SelectedFolderId == folderId; + var baseClass = "w-full px-3 py-2 text-left rounded-md flex items-center gap-3 transition-colors"; + + if (isSelected) + { + return $"{baseClass} bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-300"; + } + + return $"{baseClass} text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"; + } + + private string GetFolderIconClass(Guid? folderId) + { + var isSelected = SelectedFolderId == folderId; + return isSelected ? "w-5 h-5 text-primary-500" : "w-5 h-5 text-gray-400"; + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Layout/FormModal.razor b/apps/server/AliasVault.Client/Main/Components/Layout/FormModal.razor index 743533c31..6524fb955 100644 --- a/apps/server/AliasVault.Client/Main/Components/Layout/FormModal.razor +++ b/apps/server/AliasVault.Client/Main/Components/Layout/FormModal.razor @@ -1,10 +1,3 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) aliasvault. All rights reserved. -// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. -// -//----------------------------------------------------------------------- - @using Microsoft.Extensions.Localization @* FormModal component - generic modal for displaying forms and content *@ diff --git a/apps/server/AliasVault.Client/Main/Models/ItemEdit.cs b/apps/server/AliasVault.Client/Main/Models/ItemEdit.cs index ee45f6124..bd2fc6136 100644 --- a/apps/server/AliasVault.Client/Main/Models/ItemEdit.cs +++ b/apps/server/AliasVault.Client/Main/Models/ItemEdit.cs @@ -55,6 +55,11 @@ public sealed class ItemEdit /// public byte[]? ServiceLogo { get; set; } + /// + /// Gets or sets the folder ID. + /// + public Guid? FolderId { get; set; } + /// /// Gets or sets the username field. /// @@ -173,6 +178,7 @@ public sealed class ItemEdit ServiceUrl = ItemService.GetFieldValue(item, FieldKey.LoginUrl), LogoId = item.LogoId, ServiceLogo = item.Logo?.FileData, + FolderId = item.FolderId, Username = ItemService.GetFieldValue(item, FieldKey.LoginUsername) ?? string.Empty, Password = ItemService.GetFieldValue(item, FieldKey.LoginPassword) ?? string.Empty, Email = ItemService.GetFieldValue(item, FieldKey.LoginEmail) ?? string.Empty, @@ -223,6 +229,7 @@ public sealed class ItemEdit Name = ServiceName, ItemType = ItemType, LogoId = LogoId, + FolderId = FolderId, Attachments = Attachments, TotpCodes = TotpCodes, }; diff --git a/apps/server/AliasVault.Client/Main/Pages/Items/AddEdit.razor b/apps/server/AliasVault.Client/Main/Pages/Items/AddEdit.razor index bfd407ec4..043064578 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Items/AddEdit.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Items/AddEdit.razor @@ -40,6 +40,9 @@ else ShowDropdownChanged="@((show) => ShowTypeDropdown = show)" OnRegenerateAlias="@GenerateRandomAlias" /> + @* Folder Selector *@ + + @* Service/Name Section - Always shown *@

@GetNameSectionTitle()

diff --git a/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor b/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor index da1bb3aaa..ccf4e27b1 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor @@ -157,10 +157,10 @@ else }
} + @if (Folder != null) + { + + + + + @Folder.Name + + } @@ -245,6 +255,7 @@ else private bool IsLoading { get; set; } = true; private Item? Item { get; set; } = new(); + private Folder? Folder { get; set; } private Dictionary> GroupedFields { get; set; } = new(); private List UrlValues { get; set; } = new(); @@ -252,7 +263,7 @@ else protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"] }); + // Breadcrumb items are added dynamically in LoadEntryAsync after folder is loaded } /// @@ -281,6 +292,21 @@ else return; } + // Load the folder if the item is in one + Folder = null; + if (Item.FolderId.HasValue) + { + Folder = await FolderService.GetByIdAsync(Item.FolderId.Value); + } + + // Add breadcrumb items - Home is already added by MainBase, add folder if present + if (Folder != null) + { + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Folder.Name, Url = $"/items/folder/{Folder.Id}" }); + } + + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"] }); + // Group fields by category for display GroupedFields = FieldGrouper.GroupByCategory(Item); diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Items/FolderSelector.en.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Items/FolderSelector.en.resx new file mode 100644 index 000000000..fdb0c3258 --- /dev/null +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Items/FolderSelector.en.resx @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + Folder + Label for the folder section + + + No folder + Text shown when no folder is selected + + + Change + Button text to change folder + + + Select Folder + Title of the folder selection modal + + + No folders available. Create a folder from the vault home page. + Message shown when no folders exist + + diff --git a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css index 24fd38a4f..b1488b911 100644 --- a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css +++ b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css @@ -779,6 +779,11 @@ video { margin-bottom: 1.5rem; } +.-mx-2 { + margin-left: -0.5rem; + margin-right: -0.5rem; +} + .-ml-0 { margin-left: -0px; } @@ -2086,6 +2091,11 @@ video { padding-bottom: 2rem; } +.px-3\.5 { + padding-left: 0.875rem; + padding-right: 0.875rem; +} + .pb-28 { padding-bottom: 7rem; } @@ -2791,6 +2801,11 @@ video { background-color: rgb(244 149 65 / var(--tw-bg-opacity)); } +.hover\:bg-primary-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 224 150 / var(--tw-bg-opacity)); +} + .hover\:from-primary-600:hover { --tw-gradient-from: #d68338 var(--tw-gradient-from-position); --tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position); @@ -3609,6 +3624,10 @@ video { background-color: rgb(123 74 30 / 0.4); } +.dark\:hover\:bg-primary-900\/20:hover:is(.dark *) { + background-color: rgb(123 74 30 / 0.2); +} + .dark\:hover\:from-primary-500:hover:is(.dark *) { --tw-gradient-from: #f49541 var(--tw-gradient-from-position); --tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);