mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-04 14:54:11 -04:00
Add show folders option to items list in browser extension (#1598)
This commit is contained in:
committed by
Leendert de Borst
parent
6cee55029b
commit
32421ef286
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
333
apps/browser-extension/src/utils/LocalPreferencesService.ts
Normal file
333
apps/browser-extension/src/utils/LocalPreferencesService.ts
Normal 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.
|
||||
*/
|
||||
]);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user