diff --git a/apps/browser-extension/src/entrypoints/popup/hooks/useNavigationHistory.ts b/apps/browser-extension/src/entrypoints/popup/hooks/useNavigationHistory.ts index 970041339..93dde57b6 100644 --- a/apps/browser-extension/src/entrypoints/popup/hooks/useNavigationHistory.ts +++ b/apps/browser-extension/src/entrypoints/popup/hooks/useNavigationHistory.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigationType } from 'react-router-dom'; /** * Return type for the useNavigationHistory hook. @@ -19,9 +19,20 @@ interface INavigationHistory { */ export const useNavigationHistory = (): INavigationHistory => { const location = useLocation(); + const navigationType = useNavigationType(); const historyRef = useRef([]); useEffect(() => { + if (navigationType === 'REPLACE') { + // Mirror the replace in our tracked stack so the current entry stays accurate. + if (historyRef.current.length === 0) { + historyRef.current.push(location.pathname + location.search); + } else { + historyRef.current[historyRef.current.length - 1] = location.pathname + location.search; + } + return; + } + const currentPath = location.pathname + location.search; // Check if this is a back/forward navigation by looking for the path in history @@ -34,7 +45,7 @@ export const useNavigationHistory = (): INavigationHistory => { // New navigation - add to history historyRef.current.push(currentPath); } - }, [location]); + }, [location, navigationType]); /** * Find how many steps back we need to go to reach the target path. diff --git a/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor b/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor index dc9a93ecd..c0a2cb200 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Items/Home.razor @@ -333,6 +333,15 @@ else [Parameter] public Guid? FolderId { get; set; } + /// + /// Gets or sets the filter from the URL query string. + /// Used to preserve the active filter when navigating into a folder so the folder view + /// matches the count shown on its pill. + /// + [Parameter] + [SupplyParameterFromQuery(Name = "filter")] + public string? FilterQuery { get; set; } + /// /// Gets or sets whether the items are being loaded. /// @@ -663,25 +672,33 @@ else /// /// Gets the title based on the active filter and folder. + /// An active filter takes precedence over the folder name so the title (and the + /// "Filtering by" pill) reflects what's actually being filtered — matching the + /// filter shown in the dropdown and the count in the header. /// private string GetFilterTitle() { + if (FilterType != ItemFilterType.All) + { + return FilterType switch + { + ItemFilterType.Passkeys => Localizer["FilterPasskeysOption"], + ItemFilterType.Attachments => Localizer["FilterAttachmentsOption"], + ItemFilterType.Totp => Localizer["FilterTotpOption"], + ItemFilterType.Login => ItemTypeSelectorLocalizer["TypeLogin"], + ItemFilterType.Alias => ItemTypeSelectorLocalizer["TypeAlias"], + ItemFilterType.CreditCard => ItemTypeSelectorLocalizer["TypeCreditCard"], + ItemFilterType.Note => ItemTypeSelectorLocalizer["TypeNote"], + _ => Localizer["PageTitle"], + }; + } + if (IsInFolder && !string.IsNullOrEmpty(CurrentFolderName)) { return CurrentFolderName; } - return FilterType switch - { - ItemFilterType.Passkeys => Localizer["FilterPasskeysOption"], - ItemFilterType.Attachments => Localizer["FilterAttachmentsOption"], - ItemFilterType.Totp => Localizer["FilterTotpOption"], - ItemFilterType.Login => ItemTypeSelectorLocalizer["TypeLogin"], - ItemFilterType.Alias => ItemTypeSelectorLocalizer["TypeAlias"], - ItemFilterType.CreditCard => ItemTypeSelectorLocalizer["TypeCreditCard"], - ItemFilterType.Note => ItemTypeSelectorLocalizer["TypeNote"], - _ => Localizer["PageTitle"], - }; + return Localizer["PageTitle"]; } /// @@ -716,14 +733,15 @@ else } /// - /// Sets the filter type and closes the dropdown. + /// Sets the filter type and closes the dropdown. The filter is reflected in the URL + /// query string so it persists across folder navigation and browser back/forward. /// private void SetFilter(ItemFilterType filterType) { FilterType = filterType; VisibleItemCount = BatchSize; // Reset visible items when filter changes ShowFilterDropdown = false; - StateHasChanged(); + NavigationManager.NavigateTo(BuildItemsUrl(FolderId, filterType), replace: true); } /// @@ -733,7 +751,21 @@ else { FilterType = ItemFilterType.All; VisibleItemCount = BatchSize; // Reset visible items when filter changes - StateHasChanged(); + NavigationManager.NavigateTo(BuildItemsUrl(FolderId, ItemFilterType.All), replace: true); + } + + /// + /// Builds the items list URL for a given folder and filter. The filter is always + /// included in the query string so the active filter persists across navigation, + /// including when navigating between folder views. + /// + /// The folder ID, or null for the root view. + /// The active filter type. + /// The URL to navigate to. + private static string BuildItemsUrl(Guid? folderId, ItemFilterType filterType) + { + var basePath = folderId.HasValue ? $"/items/folder/{folderId.Value}" : "/items"; + return $"{basePath}?filter={filterType}"; } /// @@ -819,21 +851,22 @@ else } /// - /// Navigate to a folder. + /// Navigate to a folder, preserving the active filter so the folder view matches + /// the count shown on the folder pill. /// private void NavigateToFolder(Guid folderId) { VisibleItemCount = BatchSize; // Reset visible items when navigating to folder - NavigationManager.NavigateTo($"/items/folder/{folderId}"); + NavigationManager.NavigateTo(BuildItemsUrl(folderId, FilterType)); } /// - /// Navigate back to root. + /// Navigate back to root, preserving the active filter. /// private void NavigateToRoot() { VisibleItemCount = BatchSize; // Reset visible items when navigating to root - NavigationManager.NavigateTo("/items"); + NavigationManager.NavigateTo(BuildItemsUrl(null, FilterType)); } /// @@ -958,14 +991,7 @@ else if (success) { // Navigate to parent folder if it exists, otherwise root - if (parentFolderId.HasValue) - { - NavigationManager.NavigateTo($"/items/folder/{parentFolderId.Value}"); - } - else - { - NavigationManager.NavigateTo("/items"); - } + NavigationManager.NavigateTo(BuildItemsUrl(parentFolderId, FilterType)); } else { @@ -989,14 +1015,7 @@ else if (success) { // Navigate to parent folder if it exists, otherwise root - if (parentFolderId.HasValue) - { - NavigationManager.NavigateTo($"/items/folder/{parentFolderId.Value}"); - } - else - { - NavigationManager.NavigateTo("/items"); - } + NavigationManager.NavigateTo(BuildItemsUrl(parentFolderId, FilterType)); } else { @@ -1010,6 +1029,12 @@ else { await base.OnParametersSetAsync(); + // Sync the in-memory filter with the URL query so that browser back/forward and + // folder navigation preserve the active filter. Unknown values fall back to All. + FilterType = Enum.TryParse(FilterQuery, ignoreCase: true, out var parsed) + ? parsed + : ItemFilterType.All; + // Initialize table sort state from saved sort order SyncTableSortWithSortOrder();