mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 07:46:13 -04:00
Persist item filter on folder navigation (#1970)
This commit is contained in:
committed by
Leendert de Borst
parent
28f03ea321
commit
3ce53185f7
@@ -24,7 +24,7 @@ import type { Folder } from '@/utils/db/repositories/FolderRepository';
|
||||
import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsRepository';
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { canHaveSubfolders, getDescendantFolderIds, getFolderPath, getRecursiveItemCount } from '@/utils/folderUtils';
|
||||
import { applyTypeFilter, isItemTypeFilter, type ItemFilterType } from '@/utils/itemFilters';
|
||||
import { applyTypeFilter, isItemTypeFilter, parseItemFilterType, type ItemFilterType } from '@/utils/itemFilters';
|
||||
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -108,7 +108,11 @@ const ItemsList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState(() => {
|
||||
return searchParams.get('search') || '';
|
||||
});
|
||||
const [filterType, setItemFilterType] = useState<ItemFilterType>(getStoredFilter());
|
||||
// URL filter takes precedence so navigating between folders preserves the active filter.
|
||||
const [filterType, setItemFilterType] = useState<ItemFilterType>(() => {
|
||||
const urlFilter = searchParams.get('filter');
|
||||
return urlFilter ? parseItemFilterType(urlFilter) : getStoredFilter();
|
||||
});
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
const [showFolderModal, setShowFolderModal] = useState(false);
|
||||
const [showDeleteFolderModal, setShowDeleteFolderModal] = useState(false);
|
||||
@@ -157,10 +161,19 @@ const ItemsList: React.FC = () => {
|
||||
|
||||
/**
|
||||
* Update URL when search term changes to persist it in navigation history.
|
||||
* Preserves any existing filter param in the URL (set by folder navigation) — the
|
||||
* filter is initialized from the URL on mount and otherwise managed in state, so
|
||||
* we just leave whatever filter param is there alone.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Build the new URL with or without search param
|
||||
const newUrl = searchTerm ? `${location.pathname}?search=${encodeURIComponent(searchTerm)}` : location.pathname;
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (searchTerm) {
|
||||
params.set('search', searchTerm);
|
||||
} else {
|
||||
params.delete('search');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
const newUrl = queryString ? `${location.pathname}?${queryString}` : location.pathname;
|
||||
|
||||
// Only update if the URL actually changed
|
||||
const currentUrl = location.pathname + location.search;
|
||||
@@ -434,13 +447,11 @@ const ItemsList: React.FC = () => {
|
||||
}, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Get the title based on the active filter and current folder
|
||||
* Get the title based on the active filter and current folder.
|
||||
* An active filter takes precedence over the folder name so the title matches the
|
||||
* filter that's actually being applied — same behavior as the root view.
|
||||
*/
|
||||
const getFilterTitle = () : string => {
|
||||
if (currentFolderId && currentFolderName) {
|
||||
return currentFolderName;
|
||||
}
|
||||
|
||||
switch (filterType) {
|
||||
case 'passkeys':
|
||||
return t('items.filters.passkeys');
|
||||
@@ -449,6 +460,9 @@ const ItemsList: React.FC = () => {
|
||||
case 'totp':
|
||||
return t('items.filters.totp');
|
||||
case 'all':
|
||||
if (currentFolderId && currentFolderName) {
|
||||
return currentFolderName;
|
||||
}
|
||||
return t('items.title');
|
||||
default:
|
||||
// Check if it's an item type filter
|
||||
@@ -458,17 +472,21 @@ const ItemsList: React.FC = () => {
|
||||
return t(itemTypeOption.titleKey);
|
||||
}
|
||||
}
|
||||
if (currentFolderId && currentFolderName) {
|
||||
return currentFolderName;
|
||||
}
|
||||
return t('items.title');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate into a folder via URL
|
||||
* Navigate into a folder via URL, preserving the active filter so the folder view
|
||||
* shows the same subset reflected in the folder pill's badge count.
|
||||
*/
|
||||
const handleFolderClick = useCallback((folderId: string, _folderName: string) => {
|
||||
setSearchTerm(''); // Clear search when entering folder
|
||||
navigate(`/items/folder/${folderId}`);
|
||||
}, [navigate]);
|
||||
navigate(`/items/folder/${folderId}?filter=${encodeURIComponent(filterType)}`);
|
||||
}, [navigate, filterType]);
|
||||
|
||||
/**
|
||||
* Get folders with item counts.
|
||||
|
||||
@@ -18,6 +18,23 @@ export function isItemTypeFilter(filter: ItemFilterType): filter is ItemType {
|
||||
return Object.values(ItemTypes).includes(filter as ItemType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a filter value from a URL/route param. Returns 'all' if the value is missing
|
||||
* or doesn't match a known filter, so an unexpected param can't break the screen.
|
||||
*/
|
||||
export function parseItemFilterType(value: string | null | undefined): ItemFilterType {
|
||||
if (!value) {
|
||||
return 'all';
|
||||
}
|
||||
if (value === 'all' || value === 'passkeys' || value === 'attachments' || value === 'totp') {
|
||||
return value;
|
||||
}
|
||||
if (isItemTypeFilter(value as ItemFilterType)) {
|
||||
return value as ItemType;
|
||||
}
|
||||
return 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the active type/feature filter to a list of items.
|
||||
* Used both for the visible item list and for computing folder badge counts so they
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vau
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { canHaveSubfolders, getRecursiveItemCount } from '@/utils/folderUtils';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { applyTypeFilter, isItemTypeFilter, type ItemFilterType } from '@/utils/itemFilters';
|
||||
import { applyTypeFilter, isItemTypeFilter, parseItemFilterType, type ItemFilterType } from '@/utils/itemFilters';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -65,7 +65,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
* Simplified view with search scoped to this folder only.
|
||||
*/
|
||||
export default function FolderViewScreen(): React.ReactNode {
|
||||
const { id: folderId } = useLocalSearchParams<{ id: string }>();
|
||||
const { id: folderId, filter: filterParam } = useLocalSearchParams<{ id: string; filter?: string }>();
|
||||
const { syncVault } = useVaultSync();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
@@ -85,9 +85,9 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
const [refreshing, setRefreshing] = useMinDurationLoading(false, 200);
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
|
||||
// Search and filter state (scoped to this folder)
|
||||
// Initial filter comes from the route param so navigating from a filtered list into a folder keeps the filter active.
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterType, setFilterType] = useState<ItemFilterType>('all');
|
||||
const [filterType, setFilterType] = useState<ItemFilterType>(() => parseItemFilterType(filterParam));
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
|
||||
// Sort state
|
||||
@@ -428,11 +428,14 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
}, [folderId, router, navigate]);
|
||||
|
||||
/**
|
||||
* Handle subfolder click - navigate to subfolder view.
|
||||
* Handle subfolder click - navigate to subfolder view, preserving the active filter
|
||||
* so the subfolder opens with the same filter that produced its badge count.
|
||||
*/
|
||||
const handleSubfolderClick = useCallback((subfolderId: string) => {
|
||||
navigate(() => router.push(`/(tabs)/items/folder/${subfolderId}`));
|
||||
}, [router, navigate]);
|
||||
navigate(() => {
|
||||
router.push(`/(tabs)/items/folder/${subfolderId}?filter=${encodeURIComponent(filterType)}` as '/(tabs)/items/folder/[id]');
|
||||
});
|
||||
}, [router, navigate, filterType]);
|
||||
|
||||
/**
|
||||
* Create a new subfolder.
|
||||
|
||||
@@ -504,11 +504,14 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
}, [dbContext.sqliteClient, executeVaultMutation, loadItems]);
|
||||
|
||||
/**
|
||||
* Navigate to a folder.
|
||||
* Navigate to a folder, preserving the active type/feature filter so the folder view
|
||||
* shows the same subset of items reflected in the folder's badge count.
|
||||
*/
|
||||
const handleFolderClick = useCallback((folderId: string) => {
|
||||
navigate(() => router.push(`/(tabs)/items/folder/${folderId}`));
|
||||
}, [router, navigate]);
|
||||
navigate(() => {
|
||||
router.push(`/(tabs)/items/folder/${folderId}?filter=${encodeURIComponent(filterType)}` as '/(tabs)/items/folder/[id]');
|
||||
});
|
||||
}, [router, navigate, filterType]);
|
||||
|
||||
/**
|
||||
* Create a new folder.
|
||||
|
||||
@@ -18,6 +18,23 @@ export function isItemTypeFilter(filter: ItemFilterType): filter is ItemType {
|
||||
return Object.values(ItemTypes).includes(filter as ItemType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a filter value from a URL/route param. Returns 'all' if the value is missing
|
||||
* or doesn't match a known filter, so an unexpected param can't break the screen.
|
||||
*/
|
||||
export function parseItemFilterType(value: string | null | undefined): ItemFilterType {
|
||||
if (!value) {
|
||||
return 'all';
|
||||
}
|
||||
if (value === 'all' || value === 'passkeys' || value === 'attachments' || value === 'totp') {
|
||||
return value;
|
||||
}
|
||||
if (isItemTypeFilter(value as ItemFilterType)) {
|
||||
return value as ItemType;
|
||||
}
|
||||
return 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the active type/feature filter to a list of items.
|
||||
* Used both for the visible item list and for computing folder badge counts so they
|
||||
|
||||
Reference in New Issue
Block a user