mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-11 01:57:55 -04:00
Add folderUtils.ts to mobile app and refactor browser extension (#1695)
This commit is contained in:
@@ -24,7 +24,7 @@ import type { Folder } from '@/utils/db/repositories/FolderRepository';
|
||||
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 { canHaveSubfolders, getFolderPath } from '@/utils/folderUtils';
|
||||
import { canHaveSubfolders, getDescendantFolderIds, getFolderPath, getRecursiveItemCount } from '@/utils/folderUtils';
|
||||
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -532,12 +532,12 @@ const ItemsList: React.FC = () => {
|
||||
* @param folderId - The folder ID to count items for
|
||||
* @returns Total count of items in this folder and all descendant folders
|
||||
*/
|
||||
const getRecursiveItemCount = (folderId: string): number => {
|
||||
const getRecursiveItemCountLocal = (folderId: string): number => {
|
||||
// Start with direct items in this folder
|
||||
let count = directFolderCounts.get(folderId) || 0;
|
||||
|
||||
// Add counts from all child folders recursively
|
||||
const childFolderIds = getAllChildFolderIds(folderId);
|
||||
const childFolderIds = getDescendantFolderIds(folderId, allFolders);
|
||||
for (const childId of childFolderIds) {
|
||||
count += directFolderCounts.get(childId) || 0;
|
||||
}
|
||||
@@ -549,41 +549,12 @@ const ItemsList: React.FC = () => {
|
||||
const result = relevantFolders.map((folder: Folder) => ({
|
||||
id: folder.Id,
|
||||
name: folder.Name,
|
||||
itemCount: getRecursiveItemCount(folder.Id)
|
||||
itemCount: getRecursiveItemCountLocal(folder.Id)
|
||||
})).sort((a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all child folder IDs recursively for a given folder.
|
||||
* @param folderId - The parent folder ID to get children for
|
||||
* @returns Array of all descendant folder IDs
|
||||
*/
|
||||
const getAllChildFolderIds = useCallback((folderId: string): string[] => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allFolders = dbContext.sqliteClient.folders.getAll();
|
||||
const childIds: string[] = [];
|
||||
|
||||
/**
|
||||
* Recursively find all child folders.
|
||||
* @param parentId - The parent folder ID
|
||||
*/
|
||||
const findChildren = (parentId: string): void => {
|
||||
const children = allFolders.filter(f => f.ParentFolderId === parentId);
|
||||
for (const child of children) {
|
||||
childIds.push(child.Id);
|
||||
findChildren(child.Id); // Recursively find descendants
|
||||
}
|
||||
};
|
||||
|
||||
findChildren(folderId);
|
||||
return childIds;
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Filter items based on current view (folder, search, filter type)
|
||||
*/
|
||||
@@ -593,7 +564,8 @@ const ItemsList: React.FC = () => {
|
||||
// When searching inside a folder, include items in subfolders too
|
||||
if (searchTerm) {
|
||||
// Get all child folder IDs recursively
|
||||
const childFolderIds = getAllChildFolderIds(currentFolderId);
|
||||
const allFolders = dbContext?.sqliteClient?.folders.getAll() || [];
|
||||
const childFolderIds = getDescendantFolderIds(currentFolderId, allFolders);
|
||||
const allFolderIds = [currentFolderId, ...childFolderIds];
|
||||
|
||||
// Item must be in current folder or any subfolder
|
||||
@@ -689,19 +661,13 @@ const ItemsList: React.FC = () => {
|
||||
* Used for the delete folder modal to show accurate count.
|
||||
*/
|
||||
const totalItemCountInFolderTree = useMemo(() => {
|
||||
if (!currentFolderId) {
|
||||
if (!currentFolderId || !dbContext?.sqliteClient) {
|
||||
return filteredItems.length;
|
||||
}
|
||||
|
||||
// Get all child folder IDs
|
||||
const childFolderIds = getAllChildFolderIds(currentFolderId);
|
||||
const allFolderIds = [currentFolderId, ...childFolderIds];
|
||||
|
||||
// Count items in current folder and all child folders
|
||||
return items.filter(item =>
|
||||
item.FolderId && allFolderIds.includes(item.FolderId)
|
||||
).length;
|
||||
}, [currentFolderId, items, getAllChildFolderIds, filteredItems.length]);
|
||||
const allFolders = dbContext.sqliteClient.folders.getAll();
|
||||
return getRecursiveItemCount(currentFolderId, items, allFolders);
|
||||
}, [currentFolderId, items, dbContext?.sqliteClient, filteredItems.length]);
|
||||
|
||||
/**
|
||||
* Check if the current folder can have subfolders (not at max depth).
|
||||
|
||||
@@ -296,3 +296,23 @@ export function getDescendantFolderIds(folderId: string, folders: Folder[]): str
|
||||
traverse(folderId);
|
||||
return descendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of items in a folder and all its subfolders.
|
||||
* @param folderId - The folder ID to count items for
|
||||
* @param allItems - All items in the vault
|
||||
* @param allFolders - All folders in the vault
|
||||
* @returns Total item count including subfolders
|
||||
*/
|
||||
export function getRecursiveItemCount(
|
||||
folderId: string,
|
||||
allItems: Array<{ FolderId?: string | null }>,
|
||||
allFolders: Folder[]
|
||||
): number {
|
||||
// Get all descendant folder IDs
|
||||
const descendantIds = getDescendantFolderIds(folderId, allFolders);
|
||||
const allFolderIds = [folderId, ...descendantIds];
|
||||
|
||||
// Count items in current folder and all descendants
|
||||
return allItems.filter(item => item.FolderId && allFolderIds.includes(item.FolderId)).length;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
|
||||
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { canHaveSubfolders, getRecursiveItemCount } from '@/utils/folderUtils';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
@@ -89,6 +90,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
const [folder, setFolder] = useState<Folder | null>(null);
|
||||
const [subfolders, setSubfolders] = useState<FolderWithCount[]>([]);
|
||||
const [canCreateSubfolder, setCanCreateSubfolder] = useState(false);
|
||||
const [allFolders, setAllFolders] = useState<Folder[]>([]);
|
||||
// No minimum loading delay for folder view since data is already in memory
|
||||
const [isLoadingItems, setIsLoadingItems] = useState(false);
|
||||
const [refreshing, setRefreshing] = useMinDurationLoading(false, 200);
|
||||
@@ -186,38 +188,16 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
const sortedItems = useSortedItems(filteredItems, sortOrder);
|
||||
|
||||
/**
|
||||
* Get folder depth in the hierarchy.
|
||||
* Calculate total item count including all items in current folder and all child folders recursively.
|
||||
* Used for the delete folder modal to show accurate count.
|
||||
*/
|
||||
function getFolderDepth(folderId: string | null, folders: Folder[]): number | null {
|
||||
if (!folderId) {
|
||||
return null;
|
||||
const totalItemCountInFolderTree = useMemo(() => {
|
||||
if (!folderId || allFolders.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const folderItem = folders.find(f => f.Id === folderId);
|
||||
if (!folderItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let currentId: string | null = folderId;
|
||||
|
||||
// Traverse up to root, counting levels
|
||||
while (currentId) {
|
||||
const current = folders.find(f => f.Id === currentId);
|
||||
if (!current || !current.ParentFolderId) {
|
||||
break;
|
||||
}
|
||||
depth++;
|
||||
currentId = current.ParentFolderId;
|
||||
|
||||
// Prevent infinite loops
|
||||
if (depth > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
return getRecursiveItemCount(folderId, itemsList, allFolders);
|
||||
}, [folderId, allFolders, itemsList]);
|
||||
|
||||
/**
|
||||
* Load items in this folder, subfolders, and folder details.
|
||||
@@ -244,37 +224,17 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
// Get subfolders (direct children only)
|
||||
const childFolders = folders.filter((f: Folder) => f.ParentFolderId === folderId);
|
||||
|
||||
/**
|
||||
* Calculate recursive item count for a folder including all subfolders.
|
||||
* @param parentFolderId - The parent folder ID to count items for
|
||||
* @returns Total count of items in the folder and all subfolders
|
||||
*/
|
||||
const getRecursiveItemCount = (parentFolderId: string): number => {
|
||||
// Get items directly in this folder
|
||||
const directItems = items.filter((item: Item) => item.FolderId === parentFolderId);
|
||||
|
||||
// Get all child folders
|
||||
const children = folders.filter((f: Folder) => f.ParentFolderId === parentFolderId);
|
||||
|
||||
// Recursively count items in child folders
|
||||
const childItemCount = children.reduce((count, child) => {
|
||||
return count + getRecursiveItemCount(child.Id);
|
||||
}, 0);
|
||||
|
||||
return directItems.length + childItemCount;
|
||||
};
|
||||
|
||||
const subfoldersWithCounts: FolderWithCount[] = childFolders.map((f) => ({
|
||||
id: f.Id,
|
||||
name: f.Name,
|
||||
itemCount: getRecursiveItemCount(f.Id),
|
||||
itemCount: getRecursiveItemCount(f.Id, items, folders),
|
||||
}));
|
||||
|
||||
setSubfolders(subfoldersWithCounts);
|
||||
setAllFolders(folders);
|
||||
|
||||
// Calculate if we can create subfolders (check depth)
|
||||
const depth = getFolderDepth(folderId, folders);
|
||||
setCanCreateSubfolder(depth !== null && depth < 4);
|
||||
setCanCreateSubfolder(canHaveSubfolders(folderId, folders));
|
||||
|
||||
setSortOrder(savedSortOrder);
|
||||
setIsLoadingItems(false);
|
||||
@@ -1034,7 +994,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
onClose={() => setShowDeleteFolderModal(false)}
|
||||
onDeleteFolderOnly={handleDeleteFolderOnly}
|
||||
onDeleteFolderAndContents={handleDeleteFolderAndContents}
|
||||
itemCount={itemsList.length}
|
||||
itemCount={totalItemCountInFolderTree}
|
||||
/>
|
||||
</ThemedContainer>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
import type { Folder } from '@/utils/db/repositories/FolderRepository';
|
||||
import { getFolderIdPath, getFolderPath } from '@/utils/folderUtils';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
@@ -32,64 +32,6 @@ type FolderBreadcrumbProps = {
|
||||
excludeCurrentFolder?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the full path of folder names from root to the specified folder.
|
||||
* @param folderId - The folder ID
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of folder names from root to current folder, or empty array if not found
|
||||
*/
|
||||
function getFolderPath(folderId: string | null, folders: Folder[]): string[] {
|
||||
if (!folderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const path: string[] = [];
|
||||
let currentId: string | null = folderId;
|
||||
let iterations = 0;
|
||||
|
||||
// Build path by traversing up to root
|
||||
while (currentId && iterations < 5) {
|
||||
const folder = folders.find(f => f.Id === currentId);
|
||||
if (!folder) {
|
||||
break;
|
||||
}
|
||||
path.unshift(folder.Name); // Add to beginning of array
|
||||
currentId = folder.ParentFolderId;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of folder IDs from root to the specified folder.
|
||||
* @param folderId - The folder ID
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of folder IDs from root to current folder, or empty array if not found
|
||||
*/
|
||||
function getFolderIdPath(folderId: string | null, folders: Folder[]): string[] {
|
||||
if (!folderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const path: string[] = [];
|
||||
let currentId: string | null = folderId;
|
||||
let iterations = 0;
|
||||
|
||||
// Build path by traversing up to root
|
||||
while (currentId && iterations < 5) {
|
||||
const folder = folders.find(f => f.Id === currentId);
|
||||
if (!folder) {
|
||||
break;
|
||||
}
|
||||
path.unshift(folder.Id); // Add to beginning of array
|
||||
currentId = folder.ParentFolderId;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a breadcrumb navigation trail for folder hierarchy.
|
||||
* Shows the path to the current location, with optional exclusion of current folder.
|
||||
|
||||
180
apps/mobile-app/utils/folderUtils.ts
Normal file
180
apps/mobile-app/utils/folderUtils.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { Folder } from './db/repositories/FolderRepository';
|
||||
|
||||
/**
|
||||
* Maximum allowed folder nesting depth.
|
||||
* Structure: Root (0) > Level 1 (1) > Level 2 (2) > Level 3 (3) > Level 4 (4)
|
||||
* Folders at depth 4 cannot have subfolders.
|
||||
*/
|
||||
export const MAX_FOLDER_DEPTH = 4;
|
||||
|
||||
/**
|
||||
* Get folder depth in the hierarchy.
|
||||
* @param folderId - The folder ID to check
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Depth (0 = root, 1 = one level deep, etc.) or null if folder not found
|
||||
*/
|
||||
export function getFolderDepth(folderId: string, folders: Folder[]): number | null {
|
||||
const folder = folders.find(f => f.Id === folderId);
|
||||
if (!folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let currentId: string | null = folderId;
|
||||
|
||||
// Traverse up to root, counting levels
|
||||
while (currentId) {
|
||||
const current = folders.find(f => f.Id === currentId);
|
||||
if (!current || !current.ParentFolderId) {
|
||||
break;
|
||||
}
|
||||
depth++;
|
||||
currentId = current.ParentFolderId;
|
||||
|
||||
// Prevent infinite loops
|
||||
if (depth > MAX_FOLDER_DEPTH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of folder names from root to the specified folder.
|
||||
* @param folderId - The folder ID
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of folder names from root to current folder, or empty array if not found
|
||||
*/
|
||||
export function getFolderPath(folderId: string | null, folders: Folder[]): string[] {
|
||||
if (!folderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const path: string[] = [];
|
||||
let currentId: string | null = folderId;
|
||||
let iterations = 0;
|
||||
|
||||
// Build path by traversing up to root
|
||||
while (currentId && iterations < MAX_FOLDER_DEPTH + 1) {
|
||||
const folder = folders.find(f => f.Id === currentId);
|
||||
if (!folder) {
|
||||
break;
|
||||
}
|
||||
path.unshift(folder.Name); // Add to beginning of array
|
||||
currentId = folder.ParentFolderId;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of folder IDs from root to the specified folder.
|
||||
* @param folderId - The folder ID
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of folder IDs from root to current folder, or empty array if not found
|
||||
*/
|
||||
export function getFolderIdPath(folderId: string | null, folders: Folder[]): string[] {
|
||||
if (!folderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const path: string[] = [];
|
||||
let currentId: string | null = folderId;
|
||||
let iterations = 0;
|
||||
|
||||
// Build path by traversing up to root
|
||||
while (currentId && iterations < MAX_FOLDER_DEPTH + 1) {
|
||||
const folder = folders.find(f => f.Id === currentId);
|
||||
if (!folder) {
|
||||
break;
|
||||
}
|
||||
path.unshift(folder.Id); // Add to beginning of array
|
||||
currentId = folder.ParentFolderId;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format folder path for display with separator.
|
||||
* @param pathSegments - Array of folder names
|
||||
* @param separator - Separator string (default: " > ")
|
||||
* @returns Formatted folder path string
|
||||
*/
|
||||
export function formatFolderPath(
|
||||
pathSegments: string[],
|
||||
separator: string = ' > '
|
||||
): string {
|
||||
return pathSegments.join(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a folder can have subfolders (not at max depth).
|
||||
* @param folderId - The folder ID to check
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns True if folder can have children, false otherwise
|
||||
*/
|
||||
export function canHaveSubfolders(folderId: string, folders: Folder[]): boolean {
|
||||
const depth = getFolderDepth(folderId, folders);
|
||||
return depth !== null && depth < MAX_FOLDER_DEPTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant folder IDs (children, grandchildren, etc.).
|
||||
* @param folderId - The parent folder ID
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of descendant folder IDs
|
||||
*/
|
||||
export function getDescendantFolderIds(folderId: string, folders: Folder[]): string[] {
|
||||
const descendants: string[] = [];
|
||||
|
||||
/**
|
||||
* Traverse a folder tree and get all descendant folder IDs.
|
||||
*/
|
||||
const traverse = (parentId: string): void => {
|
||||
folders
|
||||
.filter(f => f.ParentFolderId === parentId)
|
||||
.forEach(child => {
|
||||
descendants.push(child.Id);
|
||||
traverse(child.Id);
|
||||
});
|
||||
};
|
||||
|
||||
traverse(folderId);
|
||||
return descendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all direct child folder IDs.
|
||||
* @param parentFolderId - The parent folder ID (null for root)
|
||||
* @param folders - Flat array of all folders
|
||||
* @returns Array of direct child folder IDs
|
||||
*/
|
||||
export function getDirectChildFolderIds(parentFolderId: string | null, folders: Folder[]): string[] {
|
||||
return folders
|
||||
.filter(f => f.ParentFolderId === parentFolderId)
|
||||
.map(f => f.Id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of items in a folder and all its subfolders.
|
||||
* @param folderId - The folder ID to count items for
|
||||
* @param allItems - All items in the vault
|
||||
* @param allFolders - All folders in the vault
|
||||
* @returns Total item count including subfolders
|
||||
*/
|
||||
export function getRecursiveItemCount(
|
||||
folderId: string,
|
||||
allItems: Array<{ FolderId?: string | null }>,
|
||||
allFolders: Folder[]
|
||||
): number {
|
||||
// Get all descendant folder IDs
|
||||
const descendantIds = getDescendantFolderIds(folderId, allFolders);
|
||||
const allFolderIds = [folderId, ...descendantIds];
|
||||
|
||||
// Count items in current folder and all descendants
|
||||
return allItems.filter(item => item.FolderId && allFolderIds.includes(item.FolderId)).length;
|
||||
}
|
||||
Reference in New Issue
Block a user