mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-04 06:52:16 -04:00
Add folder selector component (#1404)
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
@using AliasVault.Client.Main.Models
|
||||
|
||||
@* FolderPill component - displays a folder as a compact clickable pill *@
|
||||
<button @onclick="OnClick" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700/50 hover:bg-gray-200 dark:hover:bg-gray-600/50 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800">
|
||||
<svg class="w-3.5 h-3.5 text-orange-500 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
||||
<button @onclick="OnClick" class="inline-flex items-center gap-2 px-3.5 py-2 text-sm rounded-lg bg-gray-100 dark:bg-gray-700/50 hover:bg-gray-200 dark:hover:bg-gray-600/50 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800">
|
||||
<svg class="w-4 h-4 text-orange-500 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/>
|
||||
</svg>
|
||||
<span class="text-gray-700 dark:text-gray-300 truncate max-w-[120px]" title="@Folder.Name">@Folder.Name</span>
|
||||
<span class="text-gray-400 dark:text-gray-500 text-[10px]">@Folder.ItemCount</span>
|
||||
<span class="text-gray-700 dark:text-gray-300 truncate max-w-[150px]" title="@Folder.Name">@Folder.Name</span>
|
||||
<span class="text-gray-400 dark:text-gray-500 text-xs">@Folder.ItemCount</span>
|
||||
</button>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -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
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700">
|
||||
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">@Localizer["FolderLabel"]</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@if (SelectedFolderId.HasValue)
|
||||
{
|
||||
var folder = Folders.FirstOrDefault(f => f.Id == SelectedFolderId.Value);
|
||||
<span class="text-primary-600 dark:text-primary-400">@folder?.Name</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Localizer["NoFolder"]</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
@onclick="OpenFolderModal"
|
||||
@onclick:preventDefault="true"
|
||||
class="px-3 py-1.5 text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-md transition-colors">
|
||||
@Localizer["ChangeFolder"]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Folder Selection Modal *@
|
||||
<FormModal
|
||||
IsOpen="ShowFolderModal"
|
||||
Title="@Localizer["SelectFolderTitle"]"
|
||||
ShowDefaultFooter="false"
|
||||
MaxWidth="sm"
|
||||
OnClose="CloseFolderModal">
|
||||
<Icon>
|
||||
<svg class="h-6 w-6 text-orange-600 dark:text-orange-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/>
|
||||
</svg>
|
||||
</Icon>
|
||||
<ChildContent>
|
||||
<div class="space-y-1 max-h-64 overflow-y-auto -mx-2">
|
||||
@* No Folder Option *@
|
||||
<button type="button"
|
||||
@onclick="() => SelectFolder(null)"
|
||||
@onclick:preventDefault="true"
|
||||
class="@GetFolderButtonClass(null)">
|
||||
<svg class="@GetFolderIconClass(null)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
<span class="font-medium">—</span>
|
||||
@if (!SelectedFolderId.HasValue)
|
||||
{
|
||||
<svg class="w-5 h-5 ml-auto text-primary-600 dark:text-primary-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
@* Folder Options *@
|
||||
@foreach (var folder in Folders)
|
||||
{
|
||||
<button type="button"
|
||||
@onclick="() => SelectFolder(folder.Id)"
|
||||
@onclick:preventDefault="true"
|
||||
class="@GetFolderButtonClass(folder.Id)">
|
||||
<svg class="@GetFolderIconClass(folder.Id)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span class="font-medium">@folder.Name</span>
|
||||
@if (folder.ItemCount > 0)
|
||||
{
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">(@folder.ItemCount)</span>
|
||||
}
|
||||
@if (SelectedFolderId == folder.Id)
|
||||
{
|
||||
<svg class="w-5 h-5 ml-auto text-primary-600 dark:text-primary-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (Folders.Count == 0)
|
||||
{
|
||||
<p class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
@Localizer["NoFoldersAvailable"]
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</ChildContent>
|
||||
</FormModal>
|
||||
|
||||
@code {
|
||||
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Items.FolderSelector", "AliasVault.Client");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently selected folder ID.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Guid? SelectedFolderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback when folder selection changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<Guid?> SelectedFolderIdChanged { get; set; }
|
||||
|
||||
private List<FolderWithCount> Folders { get; set; } = [];
|
||||
private bool ShowFolderModal { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FormModal.razor" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
@* FormModal component - generic modal for displaying forms and content *@
|
||||
|
||||
@@ -55,6 +55,11 @@ public sealed class ItemEdit
|
||||
/// </summary>
|
||||
public byte[]? ServiceLogo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder ID.
|
||||
/// </summary>
|
||||
public Guid? FolderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username field.
|
||||
/// </summary>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -40,6 +40,9 @@ else
|
||||
ShowDropdownChanged="@((show) => ShowTypeDropdown = show)"
|
||||
OnRegenerateAlias="@GenerateRandomAlias" />
|
||||
|
||||
@* Folder Selector *@
|
||||
<FolderSelector @bind-SelectedFolderId="Obj.FolderId" />
|
||||
|
||||
@* 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>
|
||||
|
||||
@@ -157,10 +157,10 @@ else
|
||||
<FolderPill Folder="@folder" OnClick="() => NavigateToFolder(folder.Id)" />
|
||||
}
|
||||
<button @onclick="ShowCreateFolderModal" class="@GetAddFolderButtonClass()">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<svg class="w-3 h-3 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<svg class="w-3.5 h-3.5 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
@@ -490,10 +490,10 @@ else
|
||||
{
|
||||
if (Folders.Count > 0)
|
||||
{
|
||||
return "inline-flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-full transition-colors focus:outline-none text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700/50";
|
||||
return "inline-flex items-center gap-1.5 px-3.5 py-2 text-sm rounded-lg transition-colors focus:outline-none text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700/50";
|
||||
}
|
||||
|
||||
return "inline-flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-full transition-colors focus:outline-none text-gray-400 dark:text-gray-500 border border-dashed border-gray-300 dark:border-gray-600 hover:border-orange-400 dark:hover:border-orange-500 hover:text-orange-600 dark:hover:text-orange-400";
|
||||
return "inline-flex items-center gap-1.5 px-3.5 py-2 text-sm rounded-lg transition-colors focus:outline-none text-gray-400 dark:text-gray-500 border border-dashed border-gray-300 dark:border-gray-600 hover:border-orange-400 dark:hover:border-orange-500 hover:text-orange-600 dark:hover:text-orange-400";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@page "/items/{id:guid}"
|
||||
@inherits MainBase
|
||||
@inject ItemService ItemService
|
||||
@inject FolderService FolderService
|
||||
@implements IAsyncDisposable
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasClientDb
|
||||
@@ -63,6 +64,15 @@ else
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (Folder != null)
|
||||
{
|
||||
<a href="/items/folder/@Folder.Id" class="inline-flex items-center gap-2 mt-2 px-3 py-1.5 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||||
<svg class="w-4 h-4 text-orange-500" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/>
|
||||
</svg>
|
||||
<span>@Folder.Name</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,6 +255,7 @@ else
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private Item? Item { get; set; } = new();
|
||||
private Folder? Folder { get; set; }
|
||||
private Dictionary<FieldCategory, List<DisplayField>> GroupedFields { get; set; } = new();
|
||||
private List<string> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
|
||||
<data name="FolderLabel">
|
||||
<value>Folder</value>
|
||||
<comment>Label for the folder section</comment>
|
||||
</data>
|
||||
<data name="NoFolder">
|
||||
<value>No folder</value>
|
||||
<comment>Text shown when no folder is selected</comment>
|
||||
</data>
|
||||
<data name="ChangeFolder">
|
||||
<value>Change</value>
|
||||
<comment>Button text to change folder</comment>
|
||||
</data>
|
||||
<data name="SelectFolderTitle">
|
||||
<value>Select Folder</value>
|
||||
<comment>Title of the folder selection modal</comment>
|
||||
</data>
|
||||
<data name="NoFoldersAvailable">
|
||||
<value>No folders available. Create a folder from the vault home page.</value>
|
||||
<comment>Message shown when no folders exist</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user