@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 @inject AliasVault.Client.Services.QuickCreateStateService QuickCreateStateService @inject LanguageService LanguageService @implements IAsyncDisposable @using AliasClientDb @using AliasClientDb.Models @using ItemTypeClass = AliasClientDb.Models.ItemType @if (IsPopupVisible) {
@* Item Type Selector *@
@foreach (var itemType in ItemType.All) { }

@GetPopupTitle()

@if (Model.ItemType == ItemType.Login || Model.ItemType == ItemType.Alias) {
}
} @code { private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Widgets.CreateNewIdentityWidget", "AliasVault.Client"); private bool IsPopupVisible = false; private bool IsCreating = false; private CreateModel Model = new(); private string PopupStyle { get; set; } = string.Empty; private ElementReference buttonRef; private IJSObjectReference? Module; /// async ValueTask IAsyncDisposable.DisposeAsync() { await KeyboardShortcutService.UnregisterShortcutAsync("gc"); LanguageService.LanguageChanged -= OnLanguageChanged; if (Module is not null) { await Module.DisposeAsync(); } } /// protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await KeyboardShortcutService.RegisterShortcutAsync("gc", ShowPopup); LanguageService.LanguageChanged += OnLanguageChanged; Module = await JSRuntime.InvokeAsync("import", "./js/modules/newIdentityWidget.js"); } } /// /// Handles language change events and triggers component refresh. /// /// The new language code. private void OnLanguageChanged(string languageCode) { InvokeAsync(StateHasChanged); } /// /// When the URL input is focused, place cursor at the end of the default URL to allow for easy typing. /// private void OnFocusUrlInput(FocusEventArgs e) { if (Model.ServiceUrl != ItemService.DefaultServiceUrl) { return; } // Use a small delay to ensure the focus is set after the browser's default behavior. Task.Delay(1).ContinueWith(_ => { JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('serviceUrl').setSelectionRange({ItemService.DefaultServiceUrl.Length}, {ItemService.DefaultServiceUrl.Length})"); }); } /// /// Toggle the popup. /// private async Task TogglePopup() { IsPopupVisible = !IsPopupVisible; if (IsPopupVisible) { await ShowPopup(); } } /// /// Show the popup. /// private async Task ShowPopup() { IsPopupVisible = true; // Clear the input fields and default to Login type Model = new(); Model.ItemType = ItemType.Login; Model.ServiceUrl = ItemService.DefaultServiceUrl; await UpdatePopupStyle(); await Task.Delay(100); // Give time for the DOM to update await JsInteropService.FocusElementById("serviceName"); } /// /// Close the popup. /// private void ClosePopup() { IsPopupVisible = false; } /// /// Update the popup style so that it is positioned correctly. /// private async Task UpdatePopupStyle() { var windowWidth = await JSRuntime.InvokeAsync("getWindowWidth"); var buttonRect = await JSRuntime.InvokeAsync("getElementRect", buttonRef); // Constrain the popup width to 400px minus some padding. var popupWidth = Math.Min(400, windowWidth - 20); PopupStyle = $"width: {popupWidth}px; top: {buttonRect.Bottom}px;"; StateHasChanged(); } /// /// Select an item type. /// private async Task SelectItemType(string itemType) { Model.ItemType = itemType; StateHasChanged(); // Refocus the service name input so user can continue typing await Task.Delay(50); await JsInteropService.FocusElementById("serviceName"); } /// /// Handle form submission - either create alias directly or navigate to AddEdit page. /// private async Task HandleFormSubmit() { if (Model.ItemType == ItemType.Alias) { await CreateAlias(); } else { NavigateToAddEdit(); } } /// /// Create a new alias directly (quick create). /// private async Task CreateAlias() { if (IsCreating) { return; } IsCreating = true; GlobalLoadingSpinner.Show(Localizer["CreatingNewAliasMessage"]); StateHasChanged(); var item = new Item { Name = Model.ServiceName, ItemType = ItemType.Alias, FieldValues = new List() }; // Set folder ID if we're in a folder view var folderId = GetCurrentFolderIdFromUrl(); if (folderId.HasValue) { item.FolderId = folderId.Value; } // Set email with default domain ItemService.SetFieldValue(item, FieldKey.LoginEmail, "@" + ItemService.GetDefaultEmailDomain()); // Set URL if provided if (Model.ServiceUrl != ItemService.DefaultServiceUrl) { ItemService.SetFieldValue(item, FieldKey.LoginUrl, Model.ServiceUrl); } // Generate random identity await ItemService.GenerateRandomIdentityAsync(item); var id = await ItemService.InsertEntryAsync(item); if (id == Guid.Empty) { // Error saving. IsCreating = false; GlobalLoadingSpinner.Hide(); GlobalNotificationService.AddErrorMessage(Localizer["CreateItemErrorMessage"], true); return; } // No error, add success message. GlobalNotificationService.AddSuccessMessage(Localizer["ItemCreatedSuccessMessage"]); NavigationManager.NavigateTo("/items/" + id); IsCreating = false; GlobalLoadingSpinner.Hide(); StateHasChanged(); ClosePopup(); } /// /// Navigate to the AddEdit page with prefilled data. /// private void NavigateToAddEdit() { // 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; QuickCreateStateService.FolderId = GetCurrentFolderIdFromUrl(); NavigationManager.NavigateTo("/items/create"); // Notify subscribers (e.g. AddEdit page) that the state has changed. This handles // the case where the user is already on the AddEdit page and the navigation above // does not trigger a re-initialization. QuickCreateStateService.NotifyStateChanged(); ClosePopup(); } /// /// Extract the folder ID from the current URL if we're in a folder view. /// /// The folder ID if in a folder view, null otherwise. private Guid? GetCurrentFolderIdFromUrl() { var uri = new Uri(NavigationManager.Uri); var path = uri.AbsolutePath; if (path.StartsWith("/items/folder/") && Guid.TryParse(path.Replace("/items/folder/", ""), out var folderId)) { return folderId; } return null; } /// /// Get the popup title based on the selected item type. /// private string GetPopupTitle() { return Model.ItemType switch { ItemType.Alias => Localizer["CreateNewAliasTitle"], ItemType.Login => Localizer["CreateNewLoginTitle"], ItemType.CreditCard => Localizer["CreateNewCreditCardTitle"], ItemType.Note => Localizer["CreateNewNoteTitle"], _ => Localizer["CreateNewAliasTitle"] }; } /// /// Get the name placeholder based on the selected item type. /// private string GetNamePlaceholder() { return Model.ItemType switch { ItemType.Login => Localizer["NamePlaceholderLogin"], ItemType.Alias => Localizer["NamePlaceholderAlias"], ItemType.CreditCard => Localizer["NamePlaceholderCard"], ItemType.Note => Localizer["NamePlaceholderNote"], _ => Localizer["NamePlaceholderLogin"] }; } /// /// Get the submit button CSS classes based on the selected item type. /// private string GetSubmitButtonClasses() { return Model.ItemType == ItemType.Alias ? "bg-green-600 hover:bg-green-700" : "bg-primary-600 hover:bg-primary-700"; } /// /// Get the display name for an item type. /// private string GetTypeDisplayName(string itemType) { return itemType switch { ItemType.Login => Localizer["TypeLogin"], ItemType.Alias => Localizer["TypeAlias"], ItemType.CreditCard => Localizer["TypeCard"], ItemType.Note => Localizer["TypeNote"], _ => itemType }; } /// /// Get the icon markup for an item type. /// private static MarkupString GetTypeIcon(string itemType) { var svg = itemType switch { ItemType.Login => """""", ItemType.Alias => """""", ItemType.CreditCard => """""", ItemType.Note => """""", _ => """""" }; return new MarkupString(svg); } /// /// Bounding client rectangle returned from JavaScript. /// /// /// Properties are populated via JavaScript interop deserialization. /// SonarCloud warnings are suppressed as these are required for JS interop. /// private sealed class BoundingClientRect { public double Left { get; set; } public double Top { get; set; } public double Right { get; set; } public double Bottom { get; set; } public double Width { get; set; } public double Height { get; set; } } /// /// Local model for the form with support for validation. /// private sealed class CreateModel { /// /// The item type to create. /// public string ItemType { get; set; } = ItemTypeClass.Login; /// /// The service name. /// [Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.ServiceNameRequired))] [Display(Name = "Service Name")] public string ServiceName { get; set; } = string.Empty; /// /// The service URL. /// [Display(Name = "Service URL")] public string ServiceUrl { get; set; } = string.Empty; } }