mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Improve folder navigation (#1404)
This commit is contained in:
@@ -187,11 +187,12 @@ const App: React.FC = () => {
|
||||
{ path: '/unlock-success', element: <UnlockSuccess />, showBackButton: false },
|
||||
{ path: '/upgrade', element: <Upgrade />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: t('settings.title') },
|
||||
{ path: '/items', element: <ItemsList />, showBackButton: false },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.addCredential') },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: t('credentials.credentialDetails') },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.editCredential') },
|
||||
{ path: '/items', element: <ItemsList />, showBackButton: false },
|
||||
{ path: '/items/folder/:folderId', element: <ItemsList />, showBackButton: true, title: t('items.title') },
|
||||
{ path: '/items/select-type', element: <ItemTypeSelector />, showBackButton: true, title: t('itemTypes.selectType') },
|
||||
{ path: '/items/add', element: <ItemAddEdit />, showBackButton: true, title: 'Add Item' },
|
||||
{ path: '/items/deleted', element: <RecentlyDeleted />, showBackButton: true, title: t('recentlyDeleted.title') },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import FolderModal from '@/entrypoints/popup/components/Folders/FolderModal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
@@ -79,6 +79,7 @@ type FolderWithCount = {
|
||||
*/
|
||||
const ItemsList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { folderId: folderIdParam } = useParams<{ folderId?: string }>();
|
||||
const dbContext = useDb();
|
||||
const app = useApp();
|
||||
const navigate = useNavigate();
|
||||
@@ -89,12 +90,23 @@ const ItemsList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterType, setFilterType] = useState<FilterType>(getStoredFilter());
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
|
||||
const [folderPath, setFolderPath] = useState<string>('');
|
||||
const [showFolderModal, setShowFolderModal] = useState(false);
|
||||
const [recentlyDeletedCount, setRecentlyDeletedCount] = useState(0);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
// Derive current folder from URL params
|
||||
const currentFolderId = folderIdParam ?? null;
|
||||
|
||||
// Get current folder name from database
|
||||
const currentFolderName = useMemo(() => {
|
||||
if (!currentFolderId || !dbContext?.sqliteClient) {
|
||||
return null;
|
||||
}
|
||||
const folders = dbContext.sqliteClient.getAllFolders();
|
||||
const folder = folders.find(f => f.Id === currentFolderId);
|
||||
return folder?.Name ?? null;
|
||||
}, [currentFolderId, dbContext?.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
*/
|
||||
@@ -242,8 +254,8 @@ const ItemsList: React.FC = () => {
|
||||
* Get the title based on the active filter and current folder
|
||||
*/
|
||||
const getFilterTitle = () : string => {
|
||||
if (currentFolderId && folderPath) {
|
||||
return folderPath;
|
||||
if (currentFolderId && currentFolderName) {
|
||||
return currentFolderName;
|
||||
}
|
||||
|
||||
switch (filterType) {
|
||||
@@ -257,21 +269,19 @@ const ItemsList: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate into a folder
|
||||
* Navigate into a folder via URL
|
||||
*/
|
||||
const handleFolderClick = useCallback((folderId: string, folderName: string) => {
|
||||
setCurrentFolderId(folderId);
|
||||
setFolderPath(folderName);
|
||||
const handleFolderClick = useCallback((folderId: string, _folderName: string) => {
|
||||
setSearchTerm(''); // Clear search when entering folder
|
||||
}, []);
|
||||
navigate(`/items/folder/${folderId}`);
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Navigate back to root or parent folder
|
||||
* Navigate back to root (items list)
|
||||
*/
|
||||
const handleBackToRoot = useCallback(() => {
|
||||
setCurrentFolderId(null);
|
||||
setFolderPath('');
|
||||
}, []);
|
||||
navigate('/items');
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Get folders with item counts (only for root level when not searching)
|
||||
@@ -375,55 +385,28 @@ const ItemsList: React.FC = () => {
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="relative flex-1">
|
||||
{currentFolderId ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleBackToRoot}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
title={t('common.back')}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<h2 className="flex items-baseline gap-1.5 text-gray-900 dark:text-white text-xl">
|
||||
{getFilterTitle()}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">({filteredItems.length})</span>
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowFilterMenu(!showFilterMenu)}
|
||||
className="flex items-center gap-1 text-gray-900 dark:text-white text-xl hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none"
|
||||
<button
|
||||
onClick={() => setShowFilterMenu(!showFilterMenu)}
|
||||
className="flex items-center gap-1 text-gray-900 dark:text-white text-xl hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none"
|
||||
>
|
||||
<h2 className="flex items-baseline gap-1.5">
|
||||
{getFilterTitle()}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({filteredItems.length})
|
||||
</span>
|
||||
</h2>
|
||||
<svg
|
||||
className="w-4 h-4 mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<h2 className="flex items-baseline gap-1.5">
|
||||
{getFilterTitle()}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({filteredItems.length})
|
||||
</span>
|
||||
</h2>
|
||||
<svg
|
||||
className="w-4 h-4 mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
{showFilterMenu && (
|
||||
<>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user