bolt.diy/app/shared/workbench/components/ui/LockManager.tsx
KevIsDev 4d3222ee96 refactor: reorganize project structure by moving files to a more dev friendly setup
- Move stores/utils/types to their relative directories (i.e chat stores in chat directory)
- Move utility files to shared/utils
- Move component files to shared/components
- Move type definitions to shared/types
- Move stores to shared/stores
- Update import paths across the project
2025-06-16 15:33:59 +01:00

263 lines
9.3 KiB
TypeScript

import { useState, useEffect } from 'react';
import { workbenchStore } from '~/shared/workbench/stores/workbench';
import { classNames } from '~/shared/utils/classNames';
import { Checkbox } from '~/shared/components/ui/Checkbox';
import { toast } from '~/shared/components/ui/use-toast';
interface LockedItem {
path: string;
type: 'file' | 'folder';
}
export function LockManager() {
const [lockedItems, setLockedItems] = useState<LockedItem[]>([]);
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [filter, setFilter] = useState<'all' | 'files' | 'folders'>('all');
const [searchTerm, setSearchTerm] = useState('');
// Load locked items
useEffect(() => {
const loadLockedItems = () => {
// We don't need to filter by chat ID here as we want to show all locked files
const items: LockedItem[] = [];
// Get all files and folders from the workbench store
const allFiles = workbenchStore.files.get();
// Check each file/folder for locks
Object.entries(allFiles).forEach(([path, item]) => {
if (!item) {
return;
}
if (item.type === 'file' && item.isLocked) {
items.push({
path,
type: 'file',
});
} else if (item.type === 'folder' && item.isLocked) {
items.push({
path,
type: 'folder',
});
}
});
setLockedItems(items);
};
loadLockedItems();
// Set up an interval to refresh the list periodically
const intervalId = setInterval(loadLockedItems, 5000);
return () => clearInterval(intervalId);
}, []);
// Filter and sort the locked items
const filteredAndSortedItems = lockedItems
.filter((item) => {
// Apply type filter
if (filter === 'files' && item.type !== 'file') {
return false;
}
if (filter === 'folders' && item.type !== 'folder') {
return false;
}
// Apply search filter
if (searchTerm && !item.path.toLowerCase().includes(searchTerm.toLowerCase())) {
return false;
}
return true;
})
.sort((a, b) => {
return a.path.localeCompare(b.path);
});
// Handle selecting/deselecting a single item
const handleSelectItem = (path: string) => {
const newSelectedItems = new Set(selectedItems);
if (newSelectedItems.has(path)) {
newSelectedItems.delete(path);
} else {
newSelectedItems.add(path);
}
setSelectedItems(newSelectedItems);
};
// Handle selecting/deselecting all visible items
const handleSelectAll = (checked: boolean | 'indeterminate') => {
if (checked === true) {
// Select all filtered items
const allVisiblePaths = new Set(filteredAndSortedItems.map((item) => item.path));
setSelectedItems(allVisiblePaths);
} else {
// Deselect all (clear selection)
setSelectedItems(new Set());
}
};
// Handle unlocking selected items
const handleUnlockSelected = () => {
if (selectedItems.size === 0) {
toast.error('No items selected to unlock.');
return;
}
let unlockedCount = 0;
selectedItems.forEach((path) => {
const item = lockedItems.find((i) => i.path === path);
if (item) {
if (item.type === 'file') {
workbenchStore.unlockFile(path);
} else {
workbenchStore.unlockFolder(path);
}
unlockedCount++;
}
});
if (unlockedCount > 0) {
toast.success(`Unlocked ${unlockedCount} selected item(s).`);
setSelectedItems(new Set()); // Clear selection after unlocking
}
};
// Determine the state of the "Select All" checkbox
const isAllSelected = filteredAndSortedItems.length > 0 && selectedItems.size === filteredAndSortedItems.length;
const isSomeSelected = selectedItems.size > 0 && selectedItems.size < filteredAndSortedItems.length;
const selectAllCheckedState: boolean | 'indeterminate' = isAllSelected
? true
: isSomeSelected
? 'indeterminate'
: false;
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Controls */}
<div className="flex items-center gap-1 px-2 py-1 border-b border-bolt-elements-borderColor">
{/* Search Input */}
<div className="relative flex-1">
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary i-ph:magnifying-glass text-xs pointer-events-none" />
<input
type="text"
placeholder="Search..."
className="w-full text-xs pl-6 pr-2 py-0.5 h-6 bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary rounded border border-bolt-elements-borderColor focus:outline-none"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ minWidth: 0 }}
/>
</div>
{/* Filter Select */}
<select
className="text-xs px-1 py-0.5 h-6 bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary rounded border border-bolt-elements-borderColor focus:outline-none"
value={filter}
onChange={(e) => setFilter(e.target.value as any)}
>
<option value="all">All</option>
<option value="files">Files</option>
<option value="folders">Folders</option>
</select>
</div>
{/* Header Row with Select All */}
<div className="flex items-center justify-between px-2 py-1 text-xs text-bolt-elements-textSecondary">
<div>
<Checkbox
checked={selectAllCheckedState}
onCheckedChange={handleSelectAll}
className="w-3 h-3 rounded border-bolt-elements-borderColor mr-2"
aria-label="Select all items"
disabled={filteredAndSortedItems.length === 0} // Disable if no items to select
/>
<span>All</span>
</div>
{selectedItems.size > 0 && (
<button
className="ml-auto px-2 py-0.5 rounded bg-bolt-elements-button-secondary-background hover:bg-bolt-elements-button-secondary-backgroundHover text-bolt-elements-button-secondary-text text-xs flex items-center gap-1"
onClick={handleUnlockSelected}
title="Unlock all selected items"
>
Unlock all
</button>
)}
<div></div>
</div>
{/* List of locked items */}
<div className="flex-1 overflow-auto modern-scrollbar px-1 py-1">
{filteredAndSortedItems.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-bolt-elements-textTertiary text-xs gap-2">
<span className="i-ph:lock-open-duotone text-lg opacity-50" />
<span>No locked items found</span>
</div>
) : (
<ul className="space-y-1">
{filteredAndSortedItems.map((item) => (
<li
key={item.path}
className={classNames(
'text-bolt-elements-textTertiary flex items-center gap-2 px-2 py-1 rounded hover:bg-bolt-elements-background-depth-2 transition-colors group',
selectedItems.has(item.path) ? 'bg-bolt-elements-background-depth-2' : '',
)}
>
<Checkbox
checked={selectedItems.has(item.path)}
onCheckedChange={() => handleSelectItem(item.path)}
className="w-3 h-3 rounded border-bolt-elements-borderColor"
aria-labelledby={`item-label-${item.path}`} // For accessibility
/>
<span
className={classNames(
'shrink-0 text-bolt-elements-textTertiary text-xs',
item.type === 'file' ? 'i-ph:file-text-duotone' : 'i-ph:folder-duotone',
)}
/>
<span id={`item-label-${item.path}`} className="truncate flex-1 text-xs" title={item.path}>
{item.path.replace('/home/project/', '')}
</span>
{/* ... rest of the item details and buttons ... */}
<span
className={classNames(
'inline-flex items-center px-1 rounded-sm text-xs',
'bg-red-500/10 text-red-500',
)}
></span>
<button
className="flex items-center px-1 py-0.5 text-xs rounded bg-transparent hover:bg-bolt-elements-background-depth-3"
onClick={() => {
if (item.type === 'file') {
workbenchStore.unlockFile(item.path);
} else {
workbenchStore.unlockFolder(item.path);
}
toast.success(`${item.path.replace('/home/project/', '')} unlocked`);
}}
title="Unlock"
>
<span className="i-ph:lock-open text-xs" />
</button>
</li>
))}
</ul>
)}
</div>
{/* Footer */}
<div className="px-2 py-1 border-t border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 text-xs text-bolt-elements-textTertiary flex justify-between items-center">
<div>
{filteredAndSortedItems.length} item(s) {selectedItems.size} selected
</div>
</div>
</div>
);
}