From 7a9c63b2d5334de6a02663111a07a6db65dac5de Mon Sep 17 00:00:00 2001
From: Leendert de Borst
Date: Fri, 12 Dec 2025 16:48:08 +0100
Subject: [PATCH] Update folder functionality (#1404)
---
.../components/Folders/DeleteFolderModal.tsx | 167 ++++++++++++++++++
.../popup/components/Items/ItemNameInput.tsx | 47 +++--
.../popup/components/Layout/BottomNav.tsx | 8 +-
.../popup/pages/items/ItemsList.tsx | 124 ++++++++++++-
.../src/i18n/locales/en.json | 8 +
.../src/utils/SqliteClient.ts | 45 +++++
6 files changed, 369 insertions(+), 30 deletions(-)
create mode 100644 apps/browser-extension/src/entrypoints/popup/components/Folders/DeleteFolderModal.tsx
diff --git a/apps/browser-extension/src/entrypoints/popup/components/Folders/DeleteFolderModal.tsx b/apps/browser-extension/src/entrypoints/popup/components/Folders/DeleteFolderModal.tsx
new file mode 100644
index 000000000..bdcc8a8b6
--- /dev/null
+++ b/apps/browser-extension/src/entrypoints/popup/components/Folders/DeleteFolderModal.tsx
@@ -0,0 +1,167 @@
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type DeleteFolderModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onDeleteFolderOnly: () => Promise;
+ onDeleteFolderAndContents: () => Promise;
+ folderName: string;
+ itemCount: number;
+};
+
+/**
+ * Modal for deleting a folder with options to keep or delete contents
+ */
+const DeleteFolderModal: React.FC = ({
+ isOpen,
+ onClose,
+ onDeleteFolderOnly,
+ onDeleteFolderAndContents,
+ folderName,
+ itemCount
+}) => {
+ const { t } = useTranslation();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ /**
+ * Handle delete folder only (move items to root)
+ */
+ const handleDeleteFolderOnly = async (): Promise => {
+ setIsSubmitting(true);
+ try {
+ await onDeleteFolderOnly();
+ onClose();
+ } catch (err) {
+ console.error('Error deleting folder:', err);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ /**
+ * Handle delete folder and all contents
+ */
+ const handleDeleteFolderAndContents = async (): Promise => {
+ setIsSubmitting(true);
+ try {
+ await onDeleteFolderAndContents();
+ onClose();
+ } catch (err) {
+ console.error('Error deleting folder with contents:', err);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ /**
+ * Handle escape key press
+ */
+ const handleKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === 'Escape' && !isSubmitting) {
+ onClose();
+ }
+ };
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ {t('items.deleteFolder')}
+
+
+
+ {/* Body */}
+
+
+ {t('items.deleteFolderConfirm', { folderName })}
+
+
+ {itemCount > 0 && (
+
+ {t('items.folderContainsItems', { count: itemCount })}
+
+ )}
+
+ {/* Option buttons */}
+
+ {/* Delete folder only - move items to root */}
+
+
+ {/* Delete folder and contents */}
+ {itemCount > 0 && (
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ );
+};
+
+export default DeleteFolderModal;
diff --git a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemNameInput.tsx b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemNameInput.tsx
index 2d27b5978..93dd894cd 100644
--- a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemNameInput.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemNameInput.tsx
@@ -31,7 +31,6 @@ const ItemNameInput: React.FC = ({
const [showFolderModal, setShowFolderModal] = useState(false);
const selectedFolder = folders.find(f => f.Id === selectedFolderId);
- const hasFolders = folders.length > 0;
/**
* Handle folder selection and close the modal.
@@ -68,7 +67,7 @@ const ItemNameInput: React.FC = ({
-
diff --git a/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx b/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx
index 942ed8be9..ed25ed5a7 100644
--- a/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx
@@ -27,10 +27,16 @@ const BottomNav: React.FC = () => {
/**
* Handle tab change.
+ * For items tab, pass state to signal the list should reset search/filters.
*/
const handleTabChange = (tab: TabName) : void => {
setCurrentTab(tab);
- navigate(`/${tab}`);
+ if (tab === 'items') {
+ // Navigate with state to signal ItemsList to reset search/filters
+ navigate(`/${tab}`, { state: { resetFilters: true } });
+ } else {
+ navigate(`/${tab}`);
+ }
};
// Auth pages that don't show bottom navigation but still show header
diff --git a/apps/browser-extension/src/entrypoints/popup/pages/items/ItemsList.tsx b/apps/browser-extension/src/entrypoints/popup/pages/items/ItemsList.tsx
index 4215fe132..4d39e85fb 100644
--- a/apps/browser-extension/src/entrypoints/popup/pages/items/ItemsList.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/pages/items/ItemsList.tsx
@@ -1,7 +1,8 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import DeleteFolderModal from '@/entrypoints/popup/components/Folders/DeleteFolderModal';
import FolderModal from '@/entrypoints/popup/components/Folders/FolderModal';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
@@ -80,6 +81,7 @@ type FolderWithCount = {
const ItemsList: React.FC = () => {
const { t } = useTranslation();
const { folderId: folderIdParam } = useParams<{ folderId?: string }>();
+ const location = useLocation();
const dbContext = useDb();
const app = useApp();
const navigate = useNavigate();
@@ -91,6 +93,7 @@ const ItemsList: React.FC = () => {
const [filterType, setFilterType] = useState(getStoredFilter());
const [showFilterMenu, setShowFilterMenu] = useState(false);
const [showFolderModal, setShowFolderModal] = useState(false);
+ const [showDeleteFolderModal, setShowDeleteFolderModal] = useState(false);
const [recentlyDeletedCount, setRecentlyDeletedCount] = useState(0);
const { setIsInitialLoading } = useLoading();
@@ -112,6 +115,20 @@ const ItemsList: React.FC = () => {
*/
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
+ /**
+ * Reset search and filter when navigating via the vault tab (with resetFilters state).
+ */
+ useEffect(() => {
+ const state = location.state as { resetFilters?: boolean } | null;
+ if (state?.resetFilters) {
+ setSearchTerm('');
+ setFilterType('all');
+ localStorage.removeItem(FILTER_STORAGE_KEY);
+ // Clear the state to prevent re-triggering on subsequent renders
+ navigate(location.pathname, { replace: true, state: {} });
+ }
+ }, [location.state, location.pathname, navigate]);
+
/**
* Handle add new item.
* Navigate directly to add item page (defaults to Login type).
@@ -145,6 +162,50 @@ const ItemsList: React.FC = () => {
setItems(results);
}, [dbContext, currentFolderId, executeVaultMutationAsync]);
+ /**
+ * Handle delete folder (keep items, move them to root).
+ */
+ const handleDeleteFolderOnly = useCallback(async () : Promise => {
+ if (!dbContext?.sqliteClient || !currentFolderId) {
+ return;
+ }
+
+ await executeVaultMutationAsync(async () => {
+ await dbContext.sqliteClient!.deleteFolder(currentFolderId);
+ });
+
+ // Refresh items list to reflect changes
+ const results = dbContext.sqliteClient!.getAllItems();
+ setItems(results);
+ const deletedCount = dbContext.sqliteClient!.getRecentlyDeletedCount();
+ setRecentlyDeletedCount(deletedCount);
+
+ // Navigate back to root
+ navigate('/items');
+ }, [dbContext, currentFolderId, executeVaultMutationAsync, navigate]);
+
+ /**
+ * Handle delete folder and all its contents.
+ */
+ const handleDeleteFolderAndContents = useCallback(async () : Promise => {
+ if (!dbContext?.sqliteClient || !currentFolderId) {
+ return;
+ }
+
+ await executeVaultMutationAsync(async () => {
+ await dbContext.sqliteClient!.deleteFolderWithContents(currentFolderId);
+ });
+
+ // Refresh items list to reflect changes
+ const results = dbContext.sqliteClient!.getAllItems();
+ setItems(results);
+ const deletedCount = dbContext.sqliteClient!.getRecentlyDeletedCount();
+ setRecentlyDeletedCount(deletedCount);
+
+ // Navigate back to root
+ navigate('/items');
+ }, [dbContext, currentFolderId, executeVaultMutationAsync, navigate]);
+
/**
* Retrieve latest vault and refresh the items list.
*/
@@ -513,11 +574,29 @@ const ItemsList: React.FC = () => {
: t('items.noMatchingItems')
}
+ {/* Show help text and delete button when inside an empty folder */}
+ {currentFolderId && (
+ <>
+
+ {t('items.emptyFolderHint')}
+
+
+ >
+ )}
) : (
<>
{/* Folders as inline pills (only show at root level when not searching) */}
- {!currentFolderId && !searchTerm && folders.length > 0 && (
+ {!currentFolderId && !searchTerm && (
{folders.map(folder => (
{
))}
)}
@@ -552,6 +644,20 @@ const ItemsList: React.FC = () => {
)}
+ {/* Delete folder button (only show when inside a folder) */}
+ {currentFolderId && !searchTerm && (
+
+ )}
+
{/* Recently Deleted link (only show at root level when not searching) */}
{!currentFolderId && !searchTerm && (