mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Update folder functionality (#1404)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type DeleteFolderModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDeleteFolderOnly: () => Promise<void>;
|
||||
onDeleteFolderAndContents: () => Promise<void>;
|
||||
folderName: string;
|
||||
itemCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modal for deleting a folder with options to keep or delete contents
|
||||
*/
|
||||
const DeleteFolderModal: React.FC<DeleteFolderModalProps> = ({
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{t('items.deleteFolder')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{t('items.deleteFolderConfirm', { folderName })}
|
||||
</p>
|
||||
|
||||
{itemCount > 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.folderContainsItems', { count: itemCount })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Option buttons */}
|
||||
<div className="space-y-3 pt-2">
|
||||
{/* Delete folder only - move items to root */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderOnly}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-orange-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{t('items.deleteFolderKeepItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderKeepItemsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Delete folder and contents */}
|
||||
{itemCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderAndContents}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-red-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-red-600 dark:text-red-400">
|
||||
{t('items.deleteFolderAndItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderAndItemsDescription', { count: itemCount })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteFolderModal;
|
||||
@@ -31,7 +31,6 @@ const ItemNameInput: React.FC<ItemNameInputProps> = ({
|
||||
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<ItemNameInputProps> = ({
|
||||
<label htmlFor="itemName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('credentials.itemName')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="itemName"
|
||||
@@ -76,31 +75,29 @@ const ItemNameInput: React.FC<ItemNameInputProps> = ({
|
||||
value={value}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t('credentials.itemName')}
|
||||
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white ${hasFolders ? 'pr-28' : ''}`}
|
||||
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white ${selectedFolderId ? 'pr-28' : 'pr-10'}`}
|
||||
required
|
||||
/>
|
||||
{/* Folder Button inside input */}
|
||||
{hasFolders && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenFolderModal}
|
||||
className={`absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1 px-2 py-1 rounded transition-colors text-xs ${
|
||||
selectedFolderId
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 hover:bg-primary-200 dark:hover:bg-primary-900/50'
|
||||
: 'text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={selectedFolder?.Name || t('items.noFolder')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
{selectedFolderId && (
|
||||
<span className="max-w-16 truncate">
|
||||
{selectedFolder?.Name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Folder Button inside input - always visible, width adjusts based on whether a folder is selected */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenFolderModal}
|
||||
className={`absolute right-1 z-10 flex items-center gap-1 px-2 py-1 rounded transition-colors text-xs ${
|
||||
selectedFolderId
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 hover:bg-primary-200 dark:hover:bg-primary-900/50'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={selectedFolder?.Name || t('items.noFolder')}
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
{selectedFolderId && (
|
||||
<span className="max-w-16 truncate">
|
||||
{selectedFolder?.Name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FilterType>(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<void> => {
|
||||
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<void> => {
|
||||
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')
|
||||
}
|
||||
</p>
|
||||
{/* Show help text and delete button when inside an empty folder */}
|
||||
{currentFolderId && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
{t('items.emptyFolderHint')}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowDeleteFolderModal(true)}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 mt-4 text-sm text-red-600 dark:text-red-400 border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
{t('items.deleteFolder')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Folders as inline pills (only show at root level when not searching) */}
|
||||
{!currentFolderId && !searchTerm && folders.length > 0 && (
|
||||
{!currentFolderId && !searchTerm && (
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||
{folders.map(folder => (
|
||||
<FolderPill
|
||||
@@ -528,13 +607,26 @@ const ItemsList: React.FC = () => {
|
||||
))}
|
||||
<button
|
||||
onClick={handleAddFolder}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700/50 rounded-full transition-colors focus:outline-none"
|
||||
className={`inline-flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-full transition-colors focus:outline-none ${
|
||||
folders.length > 0
|
||||
? 'text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
||||
: 'text-gray-400 dark:text-gray-500 border border-dashed border-gray-300 dark:border-gray-600 hover:border-orange-400 dark:hover:border-orange-500 hover:text-orange-600 dark:hover:text-orange-400'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<svg className="w-3 h-3 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
{t('items.newFolder')}
|
||||
{folders.length === 0 && (
|
||||
/**
|
||||
* Only show text when there are no folders yet
|
||||
* if there are folders we hide the text to save on UI space
|
||||
*/
|
||||
<span>{t('items.newFolder')}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -552,6 +644,20 @@ const ItemsList: React.FC = () => {
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Delete folder button (only show when inside a folder) */}
|
||||
{currentFolderId && !searchTerm && (
|
||||
<button
|
||||
onClick={() => setShowDeleteFolderModal(true)}
|
||||
className="w-full mt-4 p-3 flex items-center gap-2 text-left text-red-600 dark:text-red-400 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 hover:border-red-300 dark:hover:border-red-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
<span>{t('items.deleteFolder')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Recently Deleted link (only show at root level when not searching) */}
|
||||
{!currentFolderId && !searchTerm && (
|
||||
<button
|
||||
@@ -590,6 +696,16 @@ const ItemsList: React.FC = () => {
|
||||
onSave={handleSaveFolder}
|
||||
mode="create"
|
||||
/>
|
||||
|
||||
{/* Delete Folder Modal */}
|
||||
<DeleteFolderModal
|
||||
isOpen={showDeleteFolderModal}
|
||||
onClose={() => setShowDeleteFolderModal(false)}
|
||||
onDeleteFolderOnly={handleDeleteFolderOnly}
|
||||
onDeleteFolderAndContents={handleDeleteFolderAndContents}
|
||||
folderName={currentFolderName || ''}
|
||||
itemCount={filteredItems.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -283,6 +283,14 @@
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No items with attachments found",
|
||||
"noMatchingItems": "No matching items found",
|
||||
"emptyFolderHint": "To move items to this folder, edit the item and tap the folder icon in the name field.",
|
||||
"deleteFolder": "Delete Folder",
|
||||
"deleteFolderConfirm": "How would you like to delete \"{{folderName}}\"?",
|
||||
"folderContainsItems": "This folder contains {{count}} item(s).",
|
||||
"deleteFolderKeepItems": "Delete folder only",
|
||||
"deleteFolderKeepItemsDescription": "Items will be moved back to the main list.",
|
||||
"deleteFolderAndItems": "Delete folder and all items",
|
||||
"deleteFolderAndItemsDescription": "{{count}} item(s) will be moved to Recently Deleted.",
|
||||
"filters": {
|
||||
"all": "(All) Items",
|
||||
"passkeys": "Passkeys",
|
||||
|
||||
@@ -3045,6 +3045,51 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a folder and all items within it (soft delete both folder and items)
|
||||
* Items are moved to "Recently Deleted" (trash)
|
||||
* @param folderId - The ID of the folder to delete
|
||||
* @returns The number of items trashed
|
||||
*/
|
||||
public async deleteFolderWithContents(folderId: string): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
this.beginTransaction();
|
||||
|
||||
const currentDateTime = dateFormatter.now();
|
||||
|
||||
// 1. Move all items in this folder to trash (set DeletedAt) and clear FolderId
|
||||
// so that when restored, items won't reference a deleted folder
|
||||
const itemsQuery = `
|
||||
UPDATE Items
|
||||
SET DeletedAt = ?,
|
||||
UpdatedAt = ?,
|
||||
FolderId = NULL
|
||||
WHERE FolderId = ? AND IsDeleted = 0 AND DeletedAt IS NULL`;
|
||||
|
||||
const itemsDeleted = this.executeUpdate(itemsQuery, [currentDateTime, currentDateTime, folderId]);
|
||||
|
||||
// 2. Soft delete the folder
|
||||
const folderQuery = `
|
||||
UPDATE Folders
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
this.executeUpdate(folderQuery, [currentDateTime, folderId]);
|
||||
|
||||
await this.commitTransaction();
|
||||
return itemsDeleted;
|
||||
} catch (error) {
|
||||
this.rollbackTransaction();
|
||||
console.error('Error deleting folder with contents:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an item to a folder
|
||||
* @param itemId - The ID of the item to move
|
||||
|
||||
Reference in New Issue
Block a user