From 12e9c0db2a8b1fef1e76de72708d8ca2b0a74bdf Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 19 Dec 2025 09:59:11 +0100 Subject: [PATCH] Add Vault/Item navigation, list and details pages to AliasVault.Client (#1404) --- .../Credentials/CredentialCard.razor | 2 +- .../Main/Components/Email/EmailPreview.razor | 4 +- .../Main/Components/Fields/FieldBlock.razor | 151 ++++ .../Folders/DeleteFolderModal.razor | 167 ++++ .../Main/Components/Folders/FolderModal.razor | 168 ++++ .../Main/Components/Folders/FolderPill.razor | 24 + .../Main/Components/Items/ItemCard.razor | 121 +++ .../Main/Components/Items/ItemIcon.razor | 148 ++++ .../ItemsTable.razor} | 2 +- .../Widgets/CreateNewIdentityWidget.razor | 8 +- .../Components/Widgets/SearchWidget.razor | 2 +- .../Main/Layout/TopMenu.razor | 8 +- .../Main/Models/DisplayField.cs | 56 ++ .../Main/Models/FolderWithCount.cs | 31 + .../Main/Models/ItemFilterType.cs | 49 ++ .../Main/Models/ItemListEntry.cs | 25 + .../Main/Models/ItemTypes.cs | 49 ++ .../Main/Pages/Credentials/Home.razor | 334 -------- .../Main/Pages/Credentials/View.razor | 349 -------- .../Main/Pages/Emails/Home.razor | 2 +- .../AliasVault.Client/Main/Pages/Home.razor | 2 +- .../{Credentials => Items}/AddEdit.razor | 38 +- .../Pages/{Credentials => Items}/Delete.razor | 24 +- .../Main/Pages/Items/Home.razor | 756 ++++++++++++++++++ .../Main/Pages/Items/View.razor | 346 ++++++++ .../Components/ImportServiceCard.razor | 2 +- .../Settings/ImportExport/ResetVault.razor | 6 +- .../Main/Utilities/CardBrandDetector.cs | 111 +++ .../Main/Utilities/FieldGrouper.cs | 166 ++++ apps/server/AliasVault.Client/Program.cs | 1 + .../Components/Fields/FieldBlock.en.resx | 129 +++ .../Folders/DeleteFolderModal.en.resx | 90 +++ .../Components/Folders/FolderModal.en.resx | 94 +++ .../Main/Email/EmailPreview.en.resx | 8 +- .../Settings/ImportExport/ResetVault.en.resx | 10 +- .../Widgets/CreateNewIdentityWidget.en.resx | 12 +- .../Resources/Layout/TopMenu.en.resx | 6 +- .../{Credentials => Items}/AddEdit.ca.resx | 0 .../{Credentials => Items}/AddEdit.de.resx | 0 .../{Credentials => Items}/AddEdit.en.resx | 82 +- .../{Credentials => Items}/AddEdit.es.resx | 0 .../{Credentials => Items}/AddEdit.fi.resx | 0 .../{Credentials => Items}/AddEdit.fr.resx | 0 .../{Credentials => Items}/AddEdit.he.resx | 0 .../{Credentials => Items}/AddEdit.it.resx | 0 .../{Credentials => Items}/AddEdit.nl.resx | 0 .../{Credentials => Items}/AddEdit.pl.resx | 0 .../{Credentials => Items}/AddEdit.pt.resx | 0 .../{Credentials => Items}/AddEdit.ru.resx | 0 .../{Credentials => Items}/AddEdit.sv.resx | 0 .../{Credentials => Items}/AddEdit.tr.resx | 0 .../{Credentials => Items}/AddEdit.uk.resx | 0 .../{Credentials => Items}/AddEdit.zh.resx | 0 .../{Credentials => Items}/Delete.ca.resx | 0 .../{Credentials => Items}/Delete.de.resx | 0 .../{Credentials => Items}/Delete.en.resx | 56 +- .../{Credentials => Items}/Delete.es.resx | 0 .../{Credentials => Items}/Delete.fi.resx | 0 .../{Credentials => Items}/Delete.fr.resx | 0 .../{Credentials => Items}/Delete.he.resx | 0 .../{Credentials => Items}/Delete.it.resx | 0 .../{Credentials => Items}/Delete.nl.resx | 0 .../{Credentials => Items}/Delete.pl.resx | 0 .../{Credentials => Items}/Delete.pt.resx | 0 .../{Credentials => Items}/Delete.ru.resx | 0 .../{Credentials => Items}/Delete.sv.resx | 0 .../{Credentials => Items}/Delete.tr.resx | 0 .../{Credentials => Items}/Delete.uk.resx | 0 .../{Credentials => Items}/Delete.zh.resx | 0 .../Main/{Credentials => Items}/Home.ca.resx | 0 .../Main/{Credentials => Items}/Home.de.resx | 0 .../Main/{Credentials => Items}/Home.en.resx | 94 ++- .../Main/{Credentials => Items}/Home.es.resx | 0 .../Main/{Credentials => Items}/Home.fi.resx | 0 .../Main/{Credentials => Items}/Home.fr.resx | 0 .../Main/{Credentials => Items}/Home.he.resx | 0 .../Main/{Credentials => Items}/Home.it.resx | 0 .../Main/{Credentials => Items}/Home.nl.resx | 0 .../Main/{Credentials => Items}/Home.pl.resx | 0 .../Main/{Credentials => Items}/Home.pt.resx | 0 .../Main/{Credentials => Items}/Home.ru.resx | 0 .../Main/{Credentials => Items}/Home.sv.resx | 0 .../Main/{Credentials => Items}/Home.tr.resx | 0 .../Main/{Credentials => Items}/Home.uk.resx | 0 .../Main/{Credentials => Items}/Home.zh.resx | 0 .../Main/{Credentials => Items}/View.ca.resx | 0 .../Main/{Credentials => Items}/View.de.resx | 0 .../Main/{Credentials => Items}/View.en.resx | 64 +- .../Main/{Credentials => Items}/View.es.resx | 0 .../Main/{Credentials => Items}/View.fi.resx | 0 .../Main/{Credentials => Items}/View.fr.resx | 0 .../Main/{Credentials => Items}/View.he.resx | 0 .../Main/{Credentials => Items}/View.it.resx | 0 .../Main/{Credentials => Items}/View.nl.resx | 0 .../Main/{Credentials => Items}/View.pl.resx | 0 .../Main/{Credentials => Items}/View.pt.resx | 0 .../Main/{Credentials => Items}/View.ru.resx | 0 .../Main/{Credentials => Items}/View.sv.resx | 0 .../Main/{Credentials => Items}/View.tr.resx | 0 .../Main/{Credentials => Items}/View.uk.resx | 0 .../Main/{Credentials => Items}/View.zh.resx | 0 .../Services/FolderService.cs | 190 +++++ .../AliasVault.Client/Services/ItemService.cs | 7 + .../Services/JsInterop/JsInteropService.cs | 20 + .../wwwroot/css/tailwind.css | 305 +++++++ .../AliasVault.Client/wwwroot/js/utilities.js | 49 ++ 106 files changed, 3502 insertions(+), 866 deletions(-) create mode 100644 apps/server/AliasVault.Client/Main/Components/Fields/FieldBlock.razor create mode 100644 apps/server/AliasVault.Client/Main/Components/Folders/DeleteFolderModal.razor create mode 100644 apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor create mode 100644 apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor create mode 100644 apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor create mode 100644 apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor rename apps/server/AliasVault.Client/Main/Components/{Credentials/CredentialsTable.razor => Items/ItemsTable.razor} (98%) create mode 100644 apps/server/AliasVault.Client/Main/Models/DisplayField.cs create mode 100644 apps/server/AliasVault.Client/Main/Models/FolderWithCount.cs create mode 100644 apps/server/AliasVault.Client/Main/Models/ItemFilterType.cs create mode 100644 apps/server/AliasVault.Client/Main/Models/ItemTypes.cs delete mode 100644 apps/server/AliasVault.Client/Main/Pages/Credentials/Home.razor delete mode 100644 apps/server/AliasVault.Client/Main/Pages/Credentials/View.razor rename apps/server/AliasVault.Client/Main/Pages/{Credentials => Items}/AddEdit.razor (96%) rename apps/server/AliasVault.Client/Main/Pages/{Credentials => Items}/Delete.razor (81%) create mode 100644 apps/server/AliasVault.Client/Main/Pages/Items/Home.razor create mode 100644 apps/server/AliasVault.Client/Main/Pages/Items/View.razor create mode 100644 apps/server/AliasVault.Client/Main/Utilities/CardBrandDetector.cs create mode 100644 apps/server/AliasVault.Client/Main/Utilities/FieldGrouper.cs create mode 100644 apps/server/AliasVault.Client/Resources/Components/Fields/FieldBlock.en.resx create mode 100644 apps/server/AliasVault.Client/Resources/Components/Folders/DeleteFolderModal.en.resx create mode 100644 apps/server/AliasVault.Client/Resources/Components/Folders/FolderModal.en.resx rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.ca.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.de.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.en.resx (70%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.es.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.fi.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.fr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.he.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.it.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.nl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.pl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.pt.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.ru.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.sv.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.tr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.uk.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/AddEdit.zh.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.ca.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.de.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.en.resx (53%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.es.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.fi.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.fr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.he.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.it.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.nl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.pl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.pt.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.ru.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.sv.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.tr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.uk.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Delete.zh.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.ca.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.de.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.en.resx (67%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.es.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.fi.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.fr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.he.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.it.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.nl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.pl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.pt.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.ru.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.sv.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.tr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.uk.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/Home.zh.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.ca.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.de.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.en.resx (64%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.es.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.fi.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.fr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.he.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.it.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.nl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.pl.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.pt.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.ru.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.sv.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.tr.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.uk.resx (100%) rename apps/server/AliasVault.Client/Resources/Pages/Main/{Credentials => Items}/View.zh.resx (100%) create mode 100644 apps/server/AliasVault.Client/Services/FolderService.cs diff --git a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor b/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor index 9f709f1f5..639b5a1da 100644 --- a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor +++ b/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor @@ -74,6 +74,6 @@ private void ShowDetails() { // Redirect to view page instead for now. - NavigationManager.NavigateTo($"/credentials/{Obj.Id}"); + NavigationManager.NavigateTo($"/items/{Obj.Id}"); } } diff --git a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor index e507d0083..06466a4fa 100644 --- a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor +++ b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor @@ -44,7 +44,7 @@

@Localizer["DateLabel"] @Email.DateSystem

@if (!string.IsNullOrEmpty(CredentialName) && CredentialId != Guid.Empty) { -

@Localizer["CredentialLabel"] +

@Localizer["ItemLabel"] + + @* Option 2: Delete folder and contents - only show if folder has items *@ + @if (ItemCount > 0) + { + + } + + @* Cancel button *@ + + + + + +} + +@code { + [Inject] + private IStringLocalizerFactory LocalizerFactory { get; set; } = default!; + + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Folders.DeleteFolderModal", "AliasVault.Client"); + + ///

+ /// Gets or sets whether the modal is open. + /// + [Parameter] + public bool IsOpen { get; set; } + + /// + /// Gets or sets the folder name to display. + /// + [Parameter] + public string FolderName { get; set; } = string.Empty; + + /// + /// Gets or sets the number of items in the folder. + /// + [Parameter] + public int ItemCount { get; set; } + + /// + /// Gets or sets the close callback. + /// + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Gets or sets the callback for deleting folder only (keeping items). + /// + [Parameter] + public EventCallback OnDeleteFolderOnly { get; set; } + + /// + /// Gets or sets the callback for deleting folder and its contents. + /// + [Parameter] + public EventCallback OnDeleteFolderAndContents { get; set; } + + private bool IsDeleting { get; set; } + + private async Task HandleClose() + { + await OnClose.InvokeAsync(); + } + + private async Task HandleDeleteFolderOnly() + { + IsDeleting = true; + StateHasChanged(); + + try + { + await OnDeleteFolderOnly.InvokeAsync(); + await OnClose.InvokeAsync(); + } + finally + { + IsDeleting = false; + StateHasChanged(); + } + } + + private async Task HandleDeleteFolderAndContents() + { + IsDeleting = true; + StateHasChanged(); + + try + { + await OnDeleteFolderAndContents.InvokeAsync(); + await OnClose.InvokeAsync(); + } + finally + { + IsDeleting = false; + StateHasChanged(); + } + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor b/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor new file mode 100644 index 000000000..8214f6002 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor @@ -0,0 +1,168 @@ +@using Microsoft.Extensions.Localization + +@* FolderModal component - modal for creating or editing a folder *@ +@if (IsOpen) +{ +
+
+ @* Background overlay *@ +
+ + @* Modal panel *@ +
+
+
+
+ + + +
+
+

+ @(Mode == "create" ? Localizer["CreateFolderTitle"] : Localizer["EditFolderTitle"]) +

+
+ + + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +

@ErrorMessage

+ } +
+
+
+
+
+ + +
+
+
+
+} + +@code { + [Inject] + private IStringLocalizerFactory LocalizerFactory { get; set; } = default!; + + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Folders.FolderModal", "AliasVault.Client"); + + /// + /// Gets or sets whether the modal is open. + /// + [Parameter] + public bool IsOpen { get; set; } + + /// + /// Gets or sets the mode (create or edit). + /// + [Parameter] + public string Mode { get; set; } = "create"; + + /// + /// Gets or sets the initial folder name (for edit mode). + /// + [Parameter] + public string InitialName { get; set; } = string.Empty; + + /// + /// Gets or sets the close callback. + /// + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Gets or sets the save callback. + /// + [Parameter] + public EventCallback OnSave { get; set; } + + private string FolderName { get; set; } = string.Empty; + private string ErrorMessage { get; set; } = string.Empty; + private bool IsSaving { get; set; } + + /// + protected override void OnParametersSet() + { + if (IsOpen) + { + FolderName = InitialName; + ErrorMessage = string.Empty; + IsSaving = false; + } + } + + private async Task HandleSave() + { + var trimmedName = FolderName?.Trim() ?? string.Empty; + + if (string.IsNullOrEmpty(trimmedName)) + { + ErrorMessage = Localizer["FolderNameRequired"]; + return; + } + + IsSaving = true; + ErrorMessage = string.Empty; + StateHasChanged(); + + try + { + await OnSave.InvokeAsync(trimmedName); + await OnClose.InvokeAsync(); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsSaving = false; + StateHasChanged(); + } + } + + private async Task HandleClose() + { + await OnClose.InvokeAsync(); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await HandleSave(); + } + else if (e.Key == "Escape") + { + await HandleClose(); + } + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor new file mode 100644 index 000000000..9696ab496 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor @@ -0,0 +1,24 @@ +@using AliasVault.Client.Main.Models + +@* FolderPill component - displays a folder as a compact clickable pill *@ + + +@code { + /// + /// Gets or sets the folder to display. + /// + [Parameter] + public required FolderWithCount Folder { get; set; } + + /// + /// Gets or sets the click callback. + /// + [Parameter] + public EventCallback OnClick { get; set; } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor new file mode 100644 index 000000000..c65aeaf25 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor @@ -0,0 +1,121 @@ +@using AliasVault.Client.Main.Models +@inject NavigationManager NavigationManager + +@* ItemCard component - displays an item in a card format with appropriate icon and information *@ +
+
+
+ +
+
+
@GetServiceName()
+ @if (Obj.HasTotp) + { + + + + + } + @if (Obj.HasPasskey) + { + + + + } + @if (Obj.HasAttachment) + { + + + + } +
+
@GetDisplayText()
+ @if (ShowFolderPath && !string.IsNullOrEmpty(Obj.FolderName)) + { +
+ + + + @Obj.FolderName +
+ } +
+
+ +@code { + /// + /// Gets or sets the item list entry object to show. + /// + [Parameter] + public required ItemListEntry Obj { get; set; } + + /// + /// Gets or sets whether to show the folder path (used when searching). + /// + [Parameter] + public bool ShowFolderPath { get; set; } + + /// + /// Gets the display text for the item, showing username by default, + /// falling back to email. For credit cards, shows cardholder name or masked number. + /// + private string GetDisplayText() + { + if (Obj.ItemType == ItemTypes.CreditCard) + { + // For credit cards, show masked card number if available + if (!string.IsNullOrEmpty(Obj.CardNumber) && Obj.CardNumber.Length >= 4) + { + return "•••• " + Obj.CardNumber[^4..]; + } + + return string.Empty; + } + + if (Obj.ItemType == ItemTypes.Note) + { + // For notes, no secondary text needed + return string.Empty; + } + + // For Login/Alias, prioritize username then email + if (!string.IsNullOrEmpty(Obj.Username)) + { + return Obj.Username; + } + + if (!string.IsNullOrEmpty(Obj.Email)) + { + return Obj.Email; + } + + return string.Empty; + } + + /// + /// Get the service name (item name) for the item. + /// + private string GetServiceName() + { + if (!string.IsNullOrEmpty(Obj.Service)) + { + return Obj.Service; + } + + return "Untitled"; + } + + /// + /// Navigate to the details page of the item. + /// + private void ShowDetails() + { + NavigationManager.NavigateTo($"/items/{Obj.Id}"); + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor new file mode 100644 index 000000000..6f0cca183 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor @@ -0,0 +1,148 @@ +@using AliasVault.Client.Main.Models +@using AliasVault.Client.Main.Utilities + +@* ItemIcon component - displays contextually appropriate icons based on item type *@ +@* For Login/Alias: Uses the Logo field if available, falls back to key placeholder *@ +@* For CreditCard: Shows card brand icons (Visa, MC, Amex, Discover) based on card number *@ +@* For Note: Shows a document/note icon *@ + +@if (ItemType == ItemTypes.Note) +{ + @* Note icon - document style *@ + + + + + + + +} +else if (ItemType == ItemTypes.CreditCard) +{ + @* Credit card icon - detect brand and show appropriate icon *@ + @switch (CardBrandDetector.Detect(CardNumber)) + { + case CardBrandDetector.CardBrand.Visa: + @* Visa card icon *@ + + + + + break; + case CardBrandDetector.CardBrand.Mastercard: + @* Mastercard icon *@ + + + + + + + break; + case CardBrandDetector.CardBrand.Amex: + @* Amex card icon *@ + + + AMEX + + break; + case CardBrandDetector.CardBrand.Discover: + @* Discover card icon *@ + + + + + + + + break; + default: + @* Generic credit card icon *@ + + + + + + + break; + } +} +else if (Logo != null && Logo.Length > 0) +{ + @* Login/Alias with logo *@ + @AltText +} +else +{ + @* Default placeholder - key icon for Login/Alias without logo *@ + + @* Key bow (circular head) - positioned top-left *@ + + @* Key hole in bow *@ + + @* Key shaft - diagonal *@ + + @* Key teeth - perpendicular to shaft *@ + + + +} + +@code { + /// + /// Gets or sets the item type (Login, Alias, CreditCard, Note). + /// + [Parameter] + public string ItemType { get; set; } = ItemTypes.Login; + + /// + /// Gets or sets the logo bytes for Login/Alias items. + /// + [Parameter] + public byte[]? Logo { get; set; } + + /// + /// Gets or sets the card number for CreditCard items (used for brand detection). + /// + [Parameter] + public string? CardNumber { get; set; } + + /// + /// Gets or sets the alt text for the image. + /// + [Parameter] + public string AltText { get; set; } = "Item"; + + /// + /// Gets or sets the size class for the icon (Tailwind CSS classes). + /// + [Parameter] + public string SizeClass { get; set; } = "w-10 h-10"; + + /// + /// Gets or sets whether to show placeholder on image error. + /// + private bool ShowPlaceholder { get; set; } + + /// + /// Converts logo bytes to data URL. + /// + private string GetLogoSrc() + { + if (Logo == null || Logo.Length == 0) + { + return string.Empty; + } + + var base64 = Convert.ToBase64String(Logo); + return $"data:image/png;base64,{base64}"; + } + + /// + /// Handle image load error by showing placeholder. + /// + private void OnImageError() + { + ShowPlaceholder = true; + StateHasChanged(); + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor similarity index 98% rename from apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor rename to apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor index 67259b811..142db6d49 100644 --- a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor @@ -153,6 +153,6 @@ /// The ID of the credential to navigate to. private void NavigateToCredential(Guid credentialId) { - NavigationManager.NavigateTo($"/credentials/{credentialId}"); + NavigationManager.NavigateTo($"/items/{credentialId}"); } } diff --git a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor index 3cff5445d..681086841 100644 --- a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor +++ b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor @@ -193,14 +193,14 @@ // Error saving. IsCreating = false; GlobalLoadingSpinner.Hide(); - GlobalNotificationService.AddErrorMessage(Localizer["CreateCredentialErrorMessage"], true); + GlobalNotificationService.AddErrorMessage(Localizer["CreateItemErrorMessage"], true); return; } // No error, add success message. - GlobalNotificationService.AddSuccessMessage(Localizer["CredentialCreatedSuccessMessage"]); + GlobalNotificationService.AddSuccessMessage(Localizer["ItemCreatedSuccessMessage"]); - NavigationManager.NavigateTo("/credentials/" + id); + NavigationManager.NavigateTo("/items/" + id); IsCreating = false; GlobalLoadingSpinner.Hide(); @@ -217,7 +217,7 @@ QuickCreateStateService.ServiceName = Model.ServiceName; QuickCreateStateService.ServiceUrl = Model.ServiceUrl; - NavigationManager.NavigateTo("/credentials/create"); + NavigationManager.NavigateTo("/items/create"); ClosePopup(); } diff --git a/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor b/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor index 089f52d14..c394b5bc4 100644 --- a/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor +++ b/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor @@ -278,7 +278,7 @@ private async Task SelectResult(Item item) { await JsInteropService.BlurElementById("searchWidget"); - NavigationManager.NavigateTo($"/credentials/{item.Id}"); + NavigationManager.NavigateTo($"/items/{item.Id}"); } private void ResetSearchField(object? sender, LocationChangedEventArgs e) diff --git a/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor b/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor index bef1d17c0..41e929304 100644 --- a/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor +++ b/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor @@ -17,8 +17,8 @@