Tweak item AddEdit secure note UI, tweak quick create (#1404)

This commit is contained in:
Leendert de Borst
2025-12-21 20:25:00 +01:00
parent c3cd81eb96
commit ccb757c951
7 changed files with 257 additions and 68 deletions

View File

@@ -1,6 +1,7 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Localization
@using AliasVault.Client.Resources
@using AliasVault.Client.Main.Models
@inherits AliasVault.Client.Main.Pages.MainBase
@inject IJSRuntime JSRuntime
@inject ItemService ItemService
@@ -19,27 +20,51 @@
<ClickOutsideHandler OnClose="ClosePopup" ContentId="quickIdentityPopup,quickIdentityButton">
<div id="quickIdentityPopup" class="absolute z-50 mt-2 p-4 bg-white rounded-lg shadow-xl border border-gray-300 dark:bg-gray-800 dark:border-gray-400"
style="@PopupStyle">
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">@Localizer["CreateNewAliasTitle"]</h3>
<EditForm Model="Model" OnValidSubmit="CreateIdentity">
@* Item Type Selector *@
<div class="mb-4">
<div class="flex gap-1">
@foreach (var itemType in ItemTypes.All)
{
<button type="button"
@onclick="() => SelectItemType(itemType)"
@onclick:preventDefault="true"
class="flex-1 px-2 py-2 text-xs font-medium rounded-md transition-colors flex flex-col items-center gap-1 @(Model.ItemType == itemType ? "bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 border border-primary-300 dark:border-primary-700" : "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-transparent")">
@GetTypeIcon(itemType)
<span>@GetTypeDisplayName(itemType)</span>
</button>
}
</div>
</div>
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">@GetPopupTitle()</h3>
<EditForm Model="Model" OnValidSubmit="HandleFormSubmit">
<DataAnnotationsValidator />
<div class="mb-4">
<EditFormRow Id="serviceName" Label="@Localizer["ServiceNameLabel"]" Placeholder="@Localizer["ServiceNamePlaceholder"]" @bind-Value="Model.ServiceName"></EditFormRow>
<EditFormRow Id="serviceName" Label="@Localizer["NameLabel"]" Placeholder="@GetNamePlaceholder()" @bind-Value="Model.ServiceName"></EditFormRow>
<ValidationMessage For="() => Model.ServiceName"/>
</div>
<div class="mb-4">
<EditFormRow Id="serviceUrl" Label="@Localizer["ServiceUrlLabel"]" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
<ValidationMessage For="() => Model.ServiceUrl"/>
</div>
@if (Model.ItemType == ItemTypes.Login || Model.ItemType == ItemTypes.Alias)
{
<div class="mb-4">
<EditFormRow Id="serviceUrl" Label="@Localizer["WebsiteUrlLabel"]" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
<ValidationMessage For="() => Model.ServiceUrl"/>
</div>
}
<div class="flex justify-between items-center">
<button id="quickIdentitySubmit" type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
@Localizer["CreateButton"]
<button id="quickIdentitySubmit" type="submit" class="@GetSubmitButtonClasses() text-white font-bold py-2 px-4 rounded flex items-center gap-2">
@if (Model.ItemType == ItemTypes.Alias)
{
@Localizer["CreateButton"]
}
else
{
@Localizer["ContinueButton"]
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
}
</button>
</div>
<div class="pt-2">
<a href="#" @onclick="OpenAdvancedMode" @onclick:preventDefault class="text-sm text-blue-500 hover:text-blue-700">
@Localizer["AdvancedModeLink"]
</a>
</div>
</EditForm>
</div>
</ClickOutsideHandler>
@@ -122,8 +147,9 @@
{
IsPopupVisible = true;
// Clear the input fields
// Clear the input fields and default to Alias type
Model = new();
Model.ItemType = ItemTypes.Alias;
Model.ServiceUrl = ItemService.DefaultServiceUrl;
await UpdatePopupStyle();
@@ -155,9 +181,33 @@
}
/// <summary>
/// Create the new identity.
/// Select an item type.
/// </summary>
private async Task CreateIdentity()
private void SelectItemType(string itemType)
{
Model.ItemType = itemType;
StateHasChanged();
}
/// <summary>
/// Handle form submission - either create alias directly or navigate to AddEdit page.
/// </summary>
private async Task HandleFormSubmit()
{
if (Model.ItemType == ItemTypes.Alias)
{
await CreateAlias();
}
else
{
NavigateToAddEdit();
}
}
/// <summary>
/// Create a new alias directly (quick create).
/// </summary>
private async Task CreateAlias()
{
if (IsCreating)
{
@@ -171,7 +221,7 @@
var item = new Item
{
Name = Model.ServiceName,
ItemType = "Login",
ItemType = ItemTypes.Alias,
FieldValues = new List<FieldValue>()
};
@@ -209,18 +259,90 @@
}
/// <summary>
/// Open the advanced mode for creating a new identity.
/// Navigate to the AddEdit page with prefilled data.
/// </summary>
private void OpenAdvancedMode()
private void NavigateToAddEdit()
{
// Store the form data in the state service to prefill in the advanced mode form.
// Store the form data in the state service to prefill in the AddEdit page.
QuickCreateStateService.ServiceName = Model.ServiceName;
QuickCreateStateService.ServiceUrl = Model.ServiceUrl;
QuickCreateStateService.ItemType = Model.ItemType;
NavigationManager.NavigateTo("/items/create");
ClosePopup();
}
/// <summary>
/// Get the popup title based on the selected item type.
/// </summary>
private string GetPopupTitle()
{
return Model.ItemType switch
{
ItemTypes.Alias => Localizer["CreateNewAliasTitle"],
ItemTypes.Login => Localizer["CreateNewLoginTitle"],
ItemTypes.CreditCard => Localizer["CreateNewCreditCardTitle"],
ItemTypes.Note => Localizer["CreateNewNoteTitle"],
_ => Localizer["CreateNewAliasTitle"]
};
}
/// <summary>
/// Get the name placeholder based on the selected item type.
/// </summary>
private string GetNamePlaceholder()
{
return Model.ItemType switch
{
ItemTypes.Login => Localizer["NamePlaceholderLogin"],
ItemTypes.Alias => Localizer["NamePlaceholderAlias"],
ItemTypes.CreditCard => Localizer["NamePlaceholderCard"],
ItemTypes.Note => Localizer["NamePlaceholderNote"],
_ => Localizer["NamePlaceholderLogin"]
};
}
/// <summary>
/// Get the submit button CSS classes based on the selected item type.
/// </summary>
private string GetSubmitButtonClasses()
{
return Model.ItemType == ItemTypes.Alias
? "bg-green-600 hover:bg-green-700"
: "bg-primary-600 hover:bg-primary-700";
}
/// <summary>
/// Get the display name for an item type.
/// </summary>
private string GetTypeDisplayName(string itemType)
{
return itemType switch
{
ItemTypes.Login => Localizer["TypeLogin"],
ItemTypes.Alias => Localizer["TypeAlias"],
ItemTypes.CreditCard => Localizer["TypeCard"],
ItemTypes.Note => Localizer["TypeNote"],
_ => itemType
};
}
/// <summary>
/// Get the icon markup for an item type.
/// </summary>
private static MarkupString GetTypeIcon(string itemType)
{
var svg = itemType switch
{
ItemTypes.Login => """<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>""",
ItemTypes.Alias => """<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>""",
ItemTypes.CreditCard => """<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>""",
ItemTypes.Note => """<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>""",
_ => """<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>"""
};
return new MarkupString(svg);
}
/// <summary>
/// Bounding client rectangle returned from JavaScript.
/// </summary>
@@ -243,6 +365,11 @@
/// </summary>
private sealed class CreateModel
{
/// <summary>
/// The item type to create.
/// </summary>
public string ItemType { get; set; } = ItemTypes.Alias;
/// <summary>
/// The service name.
/// </summary>

View File

@@ -46,7 +46,7 @@ else
@* Service/Name Section - Always shown *@
<div class="p-4 mb-4 bg-white border-2 border-primary-600 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@GetNameSectionTitle()</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["ServiceSectionHeader"]</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-name" Label="@Localizer["ServiceNameLabel"]" Placeholder="@Localizer["ServiceNamePlaceholder"]" @bind-Value="Obj.ServiceName"></EditFormRow>
@@ -70,8 +70,8 @@ else
</div>
}
@* Notes Section - When visible *@
@if (ShouldShowField(FieldKey.NotesContent))
@* Notes Section - When visible (left side for non-Note types) *@
@if (ShouldShowField(FieldKey.NotesContent) && Obj.ItemType != ItemTypes.Note)
{
<div class="col-span-1 md:col-span-1 lg:col-span-1">
<RemovableSection CanRemove="@CanRemoveField(FieldKey.NotesContent)" OnRemove="() => RemoveOptionalField(FieldKey.NotesContent)">
@@ -317,6 +317,21 @@ else
</div>
}
@* Notes Section - For Note type (main content area) *@
@if (Obj.ItemType == ItemTypes.Note)
{
<div class="col-span-1 md:col-span-1 lg:col-span-2">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["NotesLabel"]</h3>
<div class="grid gap-6">
<div class="col-span-6">
<EditFormRow Type="textarea" Id="notes" Label="" @bind-Value="Obj.GetField(FieldKey.NotesContent)!.Value"></EditFormRow>
</div>
</div>
</div>
</div>
}
@* Custom Fields Section *@
@{
var customFields = Obj.GetCustomFields();
@@ -479,6 +494,10 @@ else
{
Obj.SetFieldValue(FieldKey.LoginUrl, QuickCreateStateService.ServiceUrl);
}
if (!string.IsNullOrEmpty(QuickCreateStateService.ItemType))
{
await HandleItemTypeChange(QuickCreateStateService.ItemType);
}
// Clear the state after using it
QuickCreateStateService.ClearState();
@@ -494,19 +513,6 @@ else
}
}
/// <summary>
/// Gets the title for the name/service section based on item type.
/// </summary>
private string GetNameSectionTitle()
{
return Obj.ItemType switch
{
ItemTypes.CreditCard => Localizer["CardNameSectionHeader"],
ItemTypes.Note => Localizer["NoteTitleSectionHeader"],
_ => Localizer["ServiceSectionHeader"]
};
}
/// <summary>
/// Checks if the current item type has login-related fields.
/// </summary>

View File

@@ -89,8 +89,8 @@ else
<TotpViewer TotpCodeList="@Item.TotpCodes" />
}
@* Notes - if present *@
@if (GroupedFields.TryGetValue(FieldCategory.Notes, out var notesFields) && notesFields.Count > 0)
@* Notes - if present (left column for non-Note types) *@
@if (Item.ItemType != ItemTypes.Note && GroupedFields.TryGetValue(FieldCategory.Notes, out var notesFields) && notesFields.Count > 0)
{
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["NotesSection"]</h3>
@@ -225,6 +225,20 @@ else
</div>
}
@* Notes - for Note type (main content area) *@
@if (Item.ItemType == ItemTypes.Note && GroupedFields.TryGetValue(FieldCategory.Notes, out var noteTypeNotesFields) && noteTypeNotesFields.Count > 0)
{
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["NotesSection"]</h3>
<div class="grid grid-cols-6 gap-6">
@foreach (var field in noteTypeNotesFields)
{
<FieldBlock Field="@field" />
}
</div>
</div>
}
@* Custom fields *@
@if (GroupedFields.TryGetValue(FieldCategory.Custom, out var customFields) && customFields.Count > 0)
{

View File

@@ -59,8 +59,8 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NewAliasButtonText" xml:space="preserve">
<value>+ New Alias</value>
<comment>Text for the new alias button</comment>
<value>+ New</value>
<comment>Text for the new item button</comment>
</data>
<data name="NewAliasButtonShort" xml:space="preserve">
<value>+</value>
@@ -70,25 +70,65 @@
<value>Create New Alias</value>
<comment>Title of the create new alias popup</comment>
</data>
<data name="ServiceNameLabel" xml:space="preserve">
<value>Service Name</value>
<comment>Label for service name field</comment>
<data name="NameLabel" xml:space="preserve">
<value>Name</value>
<comment>Label for name field</comment>
</data>
<data name="ServiceNamePlaceholder" xml:space="preserve">
<data name="WebsiteUrlLabel" xml:space="preserve">
<value>Website URL</value>
<comment>Label for website URL field</comment>
</data>
<data name="NamePlaceholderLogin" xml:space="preserve">
<value>E.g. Facebook</value>
<comment>Placeholder text for service name field</comment>
<comment>Placeholder text for login name field</comment>
</data>
<data name="ServiceUrlLabel" xml:space="preserve">
<value>Service URL</value>
<comment>Label for service URL field</comment>
<data name="NamePlaceholderAlias" xml:space="preserve">
<value>E.g. Facebook</value>
<comment>Placeholder text for alias name field</comment>
</data>
<data name="NamePlaceholderCard" xml:space="preserve">
<value>E.g. Mastercard</value>
<comment>Placeholder text for card name field</comment>
</data>
<data name="NamePlaceholderNote" xml:space="preserve">
<value>E.g. Passport Details</value>
<comment>Placeholder text for note name field</comment>
</data>
<data name="CreateButton" xml:space="preserve">
<value>Create</value>
<comment>Create button text</comment>
</data>
<data name="AdvancedModeLink" xml:space="preserve">
<value>Create via advanced mode</value>
<comment>Link text to advanced creation mode</comment>
<data name="ContinueButton" xml:space="preserve">
<value>Continue</value>
<comment>Continue button text for non-alias types</comment>
</data>
<data name="CreateNewLoginTitle" xml:space="preserve">
<value>Create New Login</value>
<comment>Title for creating a new login item</comment>
</data>
<data name="CreateNewCreditCardTitle" xml:space="preserve">
<value>Create New Card</value>
<comment>Title for creating a new credit card item</comment>
</data>
<data name="CreateNewNoteTitle" xml:space="preserve">
<value>Create New Note</value>
<comment>Title for creating a new note item</comment>
</data>
<data name="TypeLogin" xml:space="preserve">
<value>Login</value>
<comment>Login item type label</comment>
</data>
<data name="TypeAlias" xml:space="preserve">
<value>Alias</value>
<comment>Alias item type label</comment>
</data>
<data name="TypeCard" xml:space="preserve">
<value>Card</value>
<comment>Credit card item type label (short)</comment>
</data>
<data name="TypeNote" xml:space="preserve">
<value>Note</value>
<comment>Note item type label</comment>
</data>
<data name="CreatingNewAliasMessage" xml:space="preserve">
<value>Creating new alias...</value>

View File

@@ -22,11 +22,11 @@
<!-- Page titles and descriptions -->
<data name="AddItemTitle">
<value>Add item</value>
<value>Add Item</value>
<comment>Title for adding a new item</comment>
</data>
<data name="EditItemTitle">
<value>Edit item</value>
<value>Edit Item</value>
<comment>Title for editing an existing item</comment>
</data>
<data name="AddItemDescription">
@@ -54,8 +54,8 @@
<!-- Section headers -->
<data name="ServiceSectionHeader">
<value>Service</value>
<comment>Header for the service information section</comment>
<value>Item</value>
<comment>Header for the item information section</comment>
</data>
<data name="LoginDetailsSectionHeader">
<value>Login details</value>
@@ -76,12 +76,12 @@
<!-- Form labels -->
<data name="ServiceNameLabel">
<value>Service Name</value>
<comment>Label for service name input</comment>
<value>Name</value>
<comment>Label for item name input</comment>
</data>
<data name="ServiceUrlLabel">
<value>Service URL</value>
<comment>Label for service URL input</comment>
<value>Website URL</value>
<comment>Label for website URL input</comment>
</data>
<data name="EmailLabel">
<value>Email</value>
@@ -213,14 +213,6 @@
<value>Card Details</value>
<comment>Header for the credit card details section</comment>
</data>
<data name="CardNameSectionHeader">
<value>Card</value>
<comment>Header for the card name section</comment>
</data>
<data name="NoteTitleSectionHeader">
<value>Note</value>
<comment>Header for the note title section</comment>
</data>
<data name="CardholderNameLabel">
<value>Cardholder Name</value>
<comment>Label for cardholder name input</comment>

View File

@@ -22,6 +22,11 @@ public class QuickCreateStateService
/// </summary>
public string? ServiceUrl { get; set; }
/// <summary>
/// Gets or sets the item type from quick create.
/// </summary>
public string? ItemType { get; set; }
/// <summary>
/// Clears the stored state.
/// </summary>
@@ -29,5 +34,6 @@ public class QuickCreateStateService
{
ServiceName = null;
ServiceUrl = null;
ItemType = null;
}
}

View File

@@ -1718,6 +1718,10 @@ video {
border-color: rgb(254 240 138 / var(--tw-border-opacity));
}
.border-transparent {
border-color: transparent;
}
.bg-amber-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 243 199 / var(--tw-bg-opacity));