Add show folders option to items list in browser extension (#1598)

This commit is contained in:
Leendert de Borst
2026-02-02 20:40:31 +01:00
committed by Leendert de Borst
parent 6cee55029b
commit 32421ef286
3 changed files with 390 additions and 20 deletions

View File

@@ -22,6 +22,7 @@ import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsRepository';
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
import { ItemTypes } from '@/utils/dist/core/models/vault';
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
@@ -125,8 +126,14 @@ const ItemsList: React.FC = () => {
const [folderRefreshKey, setFolderRefreshKey] = useState(0);
const [sortOrder, setSortOrder] = useState<CredentialSortOrder>('OldestFirst');
const [showSortMenu, setShowSortMenu] = useState(false);
const [showFolders, setShowFolders] = useState(true);
const { setIsInitialLoading } = useLoading();
// Load showFolders preference from storage on mount
useEffect(() => {
LocalPreferencesService.getShowFolders().then(setShowFolders);
}, []);
// Derive current folder from URL params
const currentFolderId = folderIdParam ?? null;
@@ -442,8 +449,11 @@ const ItemsList: React.FC = () => {
if (item.FolderId !== currentFolderId) {
return false;
}
} else if (!searchTerm) {
// In root view without search, exclude items that are in folders
} else if (!searchTerm && showFolders) {
/*
* When showing folders (checkbox ON): only show root items (exclude items in folders)
* When not showing folders (checkbox OFF): show all items flat
*/
if (item.FolderId) {
return false;
}
@@ -587,22 +597,49 @@ const ItemsList: React.FC = () => {
setShowFilterMenu(false);
}}
/>
<div className="absolute left-0 top-full mt-1 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl ring-1 ring-black/5 dark:ring-white/10 z-20">
<div className="absolute left-0 top-full mt-1 w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl ring-1 ring-black/5 dark:ring-white/10 z-20">
<div className="py-1">
{/* All items filter */}
<button
onClick={() => {
const newFilter = 'all';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'all' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('items.filters.all')}
</button>
{/* All items filter with show folders toggle (only show toggle on root view) */}
<div className="relative">
<button
onClick={() => {
const newFilter = 'all';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'all' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('items.title')}
</button>
{!currentFolderId && (
<button
onClick={(e) => {
e.stopPropagation();
const newValue = !showFolders;
setShowFolders(newValue);
LocalPreferencesService.setShowFolders(newValue);
}}
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1.5 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
>
<span>{t('items.filters.showFolders')}</span>
<svg
className={`w-5 h-5 ${showFolders ? 'text-orange-500 dark:text-orange-400' : 'text-gray-400 dark:text-gray-500'}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
{showFolders && (
<polyline points="7 12 10 15 17 8" />
)}
</svg>
</button>
)}
</div>
<div className="border-t border-gray-200 dark:border-gray-700 my-1" />
{/* Item type filters - dynamically generated from ItemTypes */}
{ITEM_TYPE_OPTIONS.map((option) => (
@@ -851,8 +888,8 @@ const ItemsList: React.FC = () => {
</>
) : (
<>
{/* Folders as inline pills (only show at root level when not searching) */}
{!currentFolderId && !searchTerm && (
{/* Folders as inline pills (only show at root level when not searching and showFolders is enabled) */}
{!currentFolderId && !searchTerm && showFolders && (
<div className="flex flex-wrap items-center gap-2 mb-4">
{folders.map(folder => (
<FolderPill

View File

@@ -202,7 +202,7 @@
"deleteFolderAndItems": "Delete folder and all items",
"deleteFolderAndItemsDescription": "{{count}} item(s) will be moved to Recently Deleted.",
"filters": {
"all": "(All) Items",
"showFolders": "show folders",
"passkeys": "Passkeys"
},
"sort": {

View File

@@ -0,0 +1,333 @@
import { storage } from '#imports';
/*
* Storage keys for local preferences.
* These are defined inline since they're only used by this service.
*/
const KEYS = {
// Site settings
DISABLED_SITES: 'local:aliasvault_disabled_sites',
TEMPORARY_DISABLED_SITES: 'local:aliasvault_temporary_disabled_sites',
PASSKEY_DISABLED_SITES: 'local:aliasvault_passkey_disabled_sites',
// Global toggles
GLOBAL_AUTOFILL_POPUP_ENABLED: 'local:aliasvault_global_autofill_popup_enabled',
GLOBAL_CONTEXT_MENU_ENABLED: 'local:aliasvault_global_context_menu_enabled',
PASSKEY_PROVIDER_ENABLED: 'local:aliasvault_passkey_provider_enabled',
// Timeouts
CLIPBOARD_CLEAR_TIMEOUT: 'local:aliasvault_clipboard_clear_timeout',
AUTO_LOCK_TIMEOUT: 'local:aliasvault_auto_lock_timeout',
VAULT_LOCKED_DISMISS_UNTIL: 'local:aliasvault_vault_locked_dismiss_until',
// Matching mode
AUTOFILL_MATCHING_MODE: 'local:aliasvault_autofill_matching_mode',
// History (TODO: move to vault in v1.0)
CUSTOM_EMAIL_HISTORY: 'local:aliasvault_custom_email_history',
CUSTOM_USERNAME_HISTORY: 'local:aliasvault_custom_username_history',
// UI preferences
SHOW_FOLDERS: 'local:aliasvault_show_folders',
} as const;
/**
* Autofill matching mode options.
*/
export enum AutofillMatchingMode {
DEFAULT = 'default',
URL_SUBDOMAIN = 'url_subdomain',
URL_EXACT = 'url_exact',
}
/**
* Service for managing user preferences that are stored locally (not in the vault).
* Provides typed getters/setters with sensible defaults for all local storage settings.
*/
export const LocalPreferencesService = {
/*
* ============================================
* UI Preferences
* ============================================
*/
/**
* Get the show folders preference.
* @returns Whether to show folders (true) or show all items flat (false). Defaults to true.
*/
async getShowFolders(): Promise<boolean> {
const value = await storage.getItem(KEYS.SHOW_FOLDERS) as boolean | null;
return value ?? true;
},
/**
* Set the show folders preference.
*/
async setShowFolders(showFolders: boolean): Promise<void> {
await storage.setItem(KEYS.SHOW_FOLDERS, showFolders);
},
/*
* ============================================
* Autofill Settings
* ============================================
*/
/**
* Get whether the global autofill popup is enabled.
* @returns Whether autofill popup is globally enabled. Defaults to true.
*/
async getGlobalAutofillPopupEnabled(): Promise<boolean> {
const value = await storage.getItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED) as boolean | null;
return value !== false;
},
/**
* Set whether the global autofill popup is enabled.
*/
async setGlobalAutofillPopupEnabled(enabled: boolean): Promise<void> {
await storage.setItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED, enabled);
},
/**
* Get the autofill matching mode.
* @returns The matching mode. Defaults to DEFAULT.
*/
async getAutofillMatchingMode(): Promise<AutofillMatchingMode> {
const value = await storage.getItem(KEYS.AUTOFILL_MATCHING_MODE) as AutofillMatchingMode | null;
return value ?? AutofillMatchingMode.DEFAULT;
},
/**
* Set the autofill matching mode.
*/
async setAutofillMatchingMode(mode: AutofillMatchingMode): Promise<void> {
await storage.setItem(KEYS.AUTOFILL_MATCHING_MODE, mode);
},
/**
* Get the list of permanently disabled sites.
* @returns Array of disabled site URLs. Defaults to empty array.
*/
async getDisabledSites(): Promise<string[]> {
const value = await storage.getItem(KEYS.DISABLED_SITES) as string[] | null;
return value ?? [];
},
/**
* Set the list of permanently disabled sites.
*/
async setDisabledSites(sites: string[]): Promise<void> {
await storage.setItem(KEYS.DISABLED_SITES, sites);
},
/**
* Get the map of temporarily disabled sites with their expiry timestamps.
* @returns Record of site URL to expiry timestamp. Defaults to empty object.
*/
async getTemporaryDisabledSites(): Promise<Record<string, number>> {
const value = await storage.getItem(KEYS.TEMPORARY_DISABLED_SITES) as Record<string, number> | null;
return value ?? {};
},
/**
* Set the map of temporarily disabled sites.
*/
async setTemporaryDisabledSites(sites: Record<string, number>): Promise<void> {
await storage.setItem(KEYS.TEMPORARY_DISABLED_SITES, sites);
},
/*
* ============================================
* Context Menu Settings
* ============================================
*/
/**
* Get whether the global context menu is enabled.
* @returns Whether context menu is globally enabled. Defaults to true.
*/
async getGlobalContextMenuEnabled(): Promise<boolean> {
const value = await storage.getItem(KEYS.GLOBAL_CONTEXT_MENU_ENABLED) as boolean | null;
return value !== false;
},
/**
* Set whether the global context menu is enabled.
*/
async setGlobalContextMenuEnabled(enabled: boolean): Promise<void> {
await storage.setItem(KEYS.GLOBAL_CONTEXT_MENU_ENABLED, enabled);
},
/*
* ============================================
* Passkey Settings
* ============================================
*/
/**
* Get whether the passkey provider is globally enabled.
* @returns Whether passkey provider is enabled. Defaults to true.
*/
async getPasskeyProviderEnabled(): Promise<boolean> {
const value = await storage.getItem(KEYS.PASSKEY_PROVIDER_ENABLED) as boolean | null;
return value !== false;
},
/**
* Set whether the passkey provider is globally enabled.
*/
async setPasskeyProviderEnabled(enabled: boolean): Promise<void> {
await storage.setItem(KEYS.PASSKEY_PROVIDER_ENABLED, enabled);
},
/**
* Get the list of sites where passkey provider is disabled.
* @returns Array of disabled site URLs. Defaults to empty array.
*/
async getPasskeyDisabledSites(): Promise<string[]> {
const value = await storage.getItem(KEYS.PASSKEY_DISABLED_SITES) as string[] | null;
return value ?? [];
},
/**
* Set the list of sites where passkey provider is disabled.
*/
async setPasskeyDisabledSites(sites: string[]): Promise<void> {
await storage.setItem(KEYS.PASSKEY_DISABLED_SITES, sites);
},
/*
* ============================================
* Timeout Settings
* ============================================
*/
/**
* Get the clipboard clear timeout in seconds.
* @returns Timeout in seconds. Defaults to 10.
*/
async getClipboardClearTimeout(): Promise<number> {
const value = await storage.getItem(KEYS.CLIPBOARD_CLEAR_TIMEOUT) as number | null;
return value ?? 10;
},
/**
* Set the clipboard clear timeout in seconds.
*/
async setClipboardClearTimeout(timeout: number): Promise<void> {
await storage.setItem(KEYS.CLIPBOARD_CLEAR_TIMEOUT, timeout);
},
/**
* Get the auto-lock timeout in seconds.
* @returns Timeout in seconds. Defaults to 0 (never).
*/
async getAutoLockTimeout(): Promise<number> {
const value = await storage.getItem(KEYS.AUTO_LOCK_TIMEOUT) as number | null;
return value ?? 0;
},
/**
* Set the auto-lock timeout in seconds.
*/
async setAutoLockTimeout(timeout: number): Promise<void> {
await storage.setItem(KEYS.AUTO_LOCK_TIMEOUT, timeout);
},
/**
* Get the vault locked dismiss until timestamp.
* @returns Timestamp until which the vault locked message is dismissed. Defaults to 0.
*/
async getVaultLockedDismissUntil(): Promise<number> {
const value = await storage.getItem(KEYS.VAULT_LOCKED_DISMISS_UNTIL) as number | null;
return value ?? 0;
},
/**
* Set the vault locked dismiss until timestamp.
*/
async setVaultLockedDismissUntil(timestamp: number): Promise<void> {
await storage.setItem(KEYS.VAULT_LOCKED_DISMISS_UNTIL, timestamp);
},
/*
* ============================================
* History Settings (for custom email/username)
* ============================================
*/
/**
* Get the custom email history.
* @returns Array of previously used custom emails. Defaults to empty array.
*/
async getCustomEmailHistory(): Promise<string[]> {
const value = await storage.getItem(KEYS.CUSTOM_EMAIL_HISTORY) as string[] | null;
return value ?? [];
},
/**
* Set the custom email history.
*/
async setCustomEmailHistory(history: string[]): Promise<void> {
await storage.setItem(KEYS.CUSTOM_EMAIL_HISTORY, history);
},
/**
* Get the custom username history.
* @returns Array of previously used custom usernames. Defaults to empty array.
*/
async getCustomUsernameHistory(): Promise<string[]> {
const value = await storage.getItem(KEYS.CUSTOM_USERNAME_HISTORY) as string[] | null;
return value ?? [];
},
/**
* Set the custom username history.
*/
async setCustomUsernameHistory(history: string[]): Promise<void> {
await storage.setItem(KEYS.CUSTOM_USERNAME_HISTORY, history);
},
/*
* ============================================
* Utility Methods
* ============================================
*/
/**
* Clear all UI preferences. Can be called on logout.
* Note: This only clears UI preferences, not security-related settings.
*/
async clearUiPreferences(): Promise<void> {
await storage.removeItem(KEYS.SHOW_FOLDERS);
},
/**
* Reset all site-specific settings (disabled sites, temporary disabled sites).
*/
async resetAllSiteSettings(): Promise<void> {
await storage.setItem(KEYS.DISABLED_SITES, []);
await storage.setItem(KEYS.TEMPORARY_DISABLED_SITES, {});
await storage.setItem(KEYS.PASSKEY_DISABLED_SITES, []);
},
/**
* Clear all preferences. Called on logout to reset everything.
*/
async clearAll(): Promise<void> {
await Promise.all([
storage.removeItem(KEYS.SHOW_FOLDERS),
storage.removeItem(KEYS.DISABLED_SITES),
storage.removeItem(KEYS.TEMPORARY_DISABLED_SITES),
storage.removeItem(KEYS.PASSKEY_DISABLED_SITES),
storage.removeItem(KEYS.VAULT_LOCKED_DISMISS_UNTIL),
storage.removeItem(KEYS.CUSTOM_EMAIL_HISTORY),
storage.removeItem(KEYS.CUSTOM_USERNAME_HISTORY),
/*
* Note: We don't clear global settings like autofill enabled, clipboard timeout, etc.
* as those are user preferences that should persist across logins.
*/
]);
},
};