}
@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;
}
}