mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
(feat:file picker) table ui
This commit is contained in:
@@ -16,6 +16,15 @@ import EyeView from '../assets/eye-view.svg';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
import CheckmarkIcon from '../assets/checkMark2.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface FileNode {
|
||||
type?: string;
|
||||
@@ -380,39 +389,36 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileTree = (directory: DirectoryStructure) => {
|
||||
if (!directory) return [];
|
||||
|
||||
const renderFileTree = (directory: DirectoryStructure): React.ReactNode[] => {
|
||||
// Create parent directory row
|
||||
const parentRow =
|
||||
currentPath.length > 0
|
||||
? [
|
||||
<tr
|
||||
key="parent-dir"
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4"></td>
|
||||
</tr>,
|
||||
]
|
||||
<TableRow
|
||||
key="parent-dir"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right"></TableCell>
|
||||
</TableRow>,
|
||||
]
|
||||
: [];
|
||||
|
||||
// Sort entries: directories first, then files, both alphabetically
|
||||
@@ -440,36 +446,35 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const dirStats = calculateDirectoryStats(node as DirectoryStructure);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => navigateToDirectory(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.folderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{dirStats.totalTokens > 0
|
||||
? dirStats.totalTokens.toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E] font-medium"
|
||||
aria-label={t('settings.sources.menuAlt')}
|
||||
>
|
||||
<img
|
||||
@@ -489,8 +494,8 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -502,30 +507,29 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const menuRef = getMenuRef(itemId);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => handleFileClick(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FileIcon}
|
||||
alt={t('settings.sources.fileAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm md:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
{node.size_bytes ? formatBytes(node.size_bytes) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -549,8 +553,8 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -708,25 +712,31 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
<div className="mb-2">{renderPathNavigation()}</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<table className="w-full min-w-[600px] table-auto bg-transparent">
|
||||
<thead className="bg-gray-100 dark:bg-[#27282D]">
|
||||
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<th className="min-w-[200px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40%" align="left">
|
||||
{t('settings.sources.fileName')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="30%" align="left">
|
||||
{t('settings.sources.tokens')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="20%" align="left">
|
||||
{t('settings.sources.size')}
|
||||
</th>
|
||||
<th className="w-10 px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{renderFileTree(getCurrentDirectory())}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableHeader width="10%" align="right">
|
||||
<span className="sr-only">
|
||||
{t('settings.sources.actions')}
|
||||
</span>
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderFileTree(getCurrentDirectory())}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,15 @@ import FolderIcon from '../assets/folder.svg';
|
||||
import CheckIcon from '../assets/checkmark.svg';
|
||||
import SearchIcon from '../assets/search.svg';
|
||||
import Input from './Input';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface CloudFile {
|
||||
id: string;
|
||||
@@ -34,6 +43,20 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
token,
|
||||
initialSelectedFiles = [],
|
||||
}) => {
|
||||
const PROVIDER_CONFIG = {
|
||||
google_drive: {
|
||||
displayName: 'Drive',
|
||||
rootName: 'My Drive',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const getProviderConfig = (provider: string) => {
|
||||
return PROVIDER_CONFIG[provider as keyof typeof PROVIDER_CONFIG] || {
|
||||
displayName: provider,
|
||||
rootName: 'Root',
|
||||
};
|
||||
};
|
||||
|
||||
const [files, setFiles] = useState<CloudFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialSelectedFiles);
|
||||
const [selectedFolders, setSelectedFolders] = useState<string[]>([]);
|
||||
@@ -41,29 +64,22 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
const [hasMoreFiles, setHasMoreFiles] = useState(false);
|
||||
const [nextPageToken, setNextPageToken] = useState<string | null>(null);
|
||||
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
|
||||
const [folderPath, setFolderPath] = useState<Array<{id: string | null, name: string}>>([{
|
||||
const [folderPath, setFolderPath] = useState<Array<{ id: string | null, name: string }>>([{
|
||||
id: null,
|
||||
name: 'Drive'
|
||||
name: getProviderConfig(provider).rootName
|
||||
}]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [authError, setAuthError] = useState<string>('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const isFolder = (file: CloudFile) => {
|
||||
return file.isFolder ||
|
||||
file.type === 'application/vnd.google-apps.folder' ||
|
||||
file.type === 'folder';
|
||||
};
|
||||
|
||||
const providerDisplayNames = {
|
||||
google_drive: 'Drive',
|
||||
};
|
||||
|
||||
const getConnectorDisplayName = (provider: string) => {
|
||||
return providerDisplayNames[provider as keyof typeof providerDisplayNames] || provider;
|
||||
file.type === 'application/vnd.google-apps.folder' ||
|
||||
file.type === 'folder';
|
||||
};
|
||||
|
||||
const loadCloudFiles = useCallback(
|
||||
@@ -147,7 +163,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
|
||||
const validateData = await validateResponse.json();
|
||||
if (validateData.success) {
|
||||
|
||||
setUserEmail(validateData.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
@@ -155,9 +171,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
setNextPageToken(null);
|
||||
setHasMoreFiles(false);
|
||||
setCurrentFolderId(null);
|
||||
setFolderPath([{id: null, name: provider === 'google_drive' ? 'My Drive' :
|
||||
provider === 'onedrive' ? 'My OneDrive' :
|
||||
provider === 'sharepoint' ? 'SharePoint' : 'Root'}]);
|
||||
setFolderPath([{
|
||||
id: null, name: getProviderConfig(provider).rootName
|
||||
}]);
|
||||
loadCloudFiles(sessionToken, null, undefined, '');
|
||||
} else {
|
||||
removeSessionToken(provider);
|
||||
@@ -273,13 +289,14 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
// Render authentication UI
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg dark:border-gray-600 p-6">
|
||||
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A] p-6">
|
||||
{authError && (
|
||||
<div className="text-red-500 text-sm mb-4 text-center">{authError}</div>
|
||||
)}
|
||||
<ConnectorAuth
|
||||
provider={provider}
|
||||
onSuccess={(data) => {
|
||||
setUserEmail(data.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
@@ -299,15 +316,15 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
|
||||
// Render file browser UI
|
||||
return (
|
||||
<div>
|
||||
<div className=''>
|
||||
{/* Connected state indicator */}
|
||||
<div className="p-3">
|
||||
<div className="w-full flex items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-[#212121] text-sm">
|
||||
<div className="w-full flex items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-[#212121] font-medium text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
<path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
<span>Connected to {getConnectorDisplayName(provider)}</span>
|
||||
<span>Connected as {userEmail}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -321,7 +338,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ provider: provider, session_token: sessionToken })
|
||||
}).catch(err => console.error(`Error disconnecting from ${getConnectorDisplayName(provider)}:`, err));
|
||||
}).catch(err => console.error(`Error disconnecting from ${getProviderConfig(provider).displayName}:`, err));
|
||||
}
|
||||
|
||||
removeSessionToken(provider);
|
||||
@@ -330,146 +347,153 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
setSelectedFiles([]);
|
||||
onSelectionChange([]);
|
||||
|
||||
// Call the onDisconnect
|
||||
if (onDisconnect) {
|
||||
onDisconnect();
|
||||
}
|
||||
}}
|
||||
className="text-[#212121] hover:text-gray-700 text-xs underline"
|
||||
className="text-[#212121] hover:text-gray-700 font-medium text-xs underline"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg dark:border-gray-600 mt-3">
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-600 rounded-t-lg">
|
||||
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A] mt-3">
|
||||
<div className="border-b border-[#EEE6FF78] dark:border-[#6A6A6A] rounded-t-lg">
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
{folderPath.map((path, index) => (
|
||||
<div key={path.id || 'root'} className="flex items-center gap-1">
|
||||
{index > 0 && <span className="text-gray-400">/</span>}
|
||||
<button
|
||||
onClick={() => navigateBack(index)}
|
||||
className="text-sm text-[#A076F6] hover:text-[#8A5FD4] hover:underline"
|
||||
disabled={index === folderPath.length - 1}
|
||||
>
|
||||
{path.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
colorVariant="silver"
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
leftIcon={<img src={SearchIcon} alt="Search" width={16} height={16} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Select Files from {getConnectorDisplayName(provider)}
|
||||
</h4>
|
||||
<span className="text-xs text-gray-500">
|
||||
{selectedFiles.length + selectedFolders.length > 0
|
||||
? `${selectedFiles.length + selectedFolders.length} item${(selectedFiles.length + selectedFolders.length) !== 1 ? 's' : ''} selected`
|
||||
: ''
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-72 overflow-y-auto" ref={scrollContainerRef}>
|
||||
{isLoading && files.length === 0 ? (
|
||||
<div className="p-4 text-center">
|
||||
<div className="inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
Loading files...
|
||||
</div>
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
No files found in your {getConnectorDisplayName(provider)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-600">
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={`${file.id}-${index}`}
|
||||
className={`transition-colors ${
|
||||
selectedFiles.includes(file.id) ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
<div className="px-4 pt-4 bg-[#EEE6FF78] dark:bg-[#2A262E] rounded-t-lg">
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
{folderPath.map((path, index) => (
|
||||
<div key={path.id || 'root'} className="flex items-center gap-1">
|
||||
{index > 0 && <span className="text-gray-400">/</span>}
|
||||
<button
|
||||
onClick={() => navigateBack(index)}
|
||||
className="text-sm text-[#A076F6] hover:text-[#8A5FD4] hover:underline"
|
||||
disabled={index === folderPath.length - 1}
|
||||
>
|
||||
<div className="flex items-center gap-3 p-3">
|
||||
<div
|
||||
className="flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleFileSelect(file.id, isFolder(file));
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783] cursor-pointer"
|
||||
>
|
||||
{(isFolder(file) ? selectedFolders : selectedFiles).includes(file.id) && (
|
||||
<img
|
||||
src={CheckIcon}
|
||||
alt="Selected"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer hover:text-blue-600"
|
||||
onClick={() => {
|
||||
if (isFolder(file)) {
|
||||
handleFolderClick(file.id, file.name);
|
||||
} else {
|
||||
handleFileSelect(file.id, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={isFolder(file) ? FolderIcon : FileIcon}
|
||||
alt={isFolder(file) ? "Folder" : "File"}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate dark:text-[#ececf1]">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{file.size && `${formatBytes(file.size)} • `}Modified {formatDate(file.modifiedTime)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{path.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="p-4 flex items-center justify-center border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="mb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
Select Files from {getProviderConfig(provider).displayName}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 max-w-md">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
colorVariant="silver"
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-[#EEE6FF78] dark:bg-[#2A262E]"
|
||||
leftIcon={<img src={SearchIcon} alt="Search" width={16} height={16} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected Files Message */}
|
||||
<div className="pb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedFiles.length} file(s) selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-72">
|
||||
<TableContainer
|
||||
ref={scrollContainerRef}
|
||||
height="288px"
|
||||
className="scrollbar-thin md:w-4xl lg:w-5xl"
|
||||
bordered={false}
|
||||
>
|
||||
{isLoading && files.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
Loading more files...
|
||||
Loading files...
|
||||
</div>
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-sm text-gray-500 dark:text-gray-400">
|
||||
No files found in your {getProviderConfig(provider).displayName}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table minWidth="1200px">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40px"></TableHeader>
|
||||
<TableHeader width="60%">Name</TableHeader>
|
||||
<TableHeader width="20%">Last Modified</TableHeader>
|
||||
<TableHeader width="20%">Size</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{files.map((file, index) => (
|
||||
<TableRow
|
||||
key={`${file.id}-${index}`}
|
||||
className={selectedFiles.includes(file.id) ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
|
||||
onClick={() => {
|
||||
if (isFolder(file)) {
|
||||
handleFolderClick(file.id, file.name);
|
||||
} else {
|
||||
handleFileSelect(file.id, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell width="40px" align="center">
|
||||
<div
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center border border-[#EEE6FF78] p-[0.5px] dark:border-[#6A6A6A] cursor-pointer mx-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleFileSelect(file.id, isFolder(file));
|
||||
}}
|
||||
>
|
||||
{(isFolder(file) ? selectedFolders : selectedFiles).includes(file.id) && (
|
||||
<img
|
||||
src={CheckIcon}
|
||||
alt="Selected"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={isFolder(file) ? FolderIcon : FileIcon}
|
||||
alt={isFolder(file) ? "Folder" : "File"}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate">{file.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(file.modifiedTime)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{file.size ? formatBytes(file.size) : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{isLoading && files.length > 0 && (
|
||||
<div className="flex items-center justify-center p-4 border-t border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
Loading more files...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,18 @@ import FolderIcon from '../assets/folder.svg';
|
||||
import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import ThreeDots from '../assets/three-dots.svg';
|
||||
import EyeView from '../assets/eye-view.svg';
|
||||
import OutlineSource from '../assets/outline-source.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import SearchIcon from '../assets/search.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface FileNode {
|
||||
type?: string;
|
||||
@@ -538,31 +545,30 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const parentRow =
|
||||
currentPath.length > 0
|
||||
? [
|
||||
<tr
|
||||
<TableRow
|
||||
key="parent-dir"
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
-
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
-
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4"></td>
|
||||
</tr>,
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right"></TableCell>
|
||||
</TableRow>,
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -575,32 +581,31 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const dirStats = calculateDirectoryStats(node as DirectoryStructure);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => navigateToDirectory(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.folderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
{dirStats.totalTokens > 0
|
||||
? dirStats.totalTokens.toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -624,8 +629,8 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}),
|
||||
...files.map(([name, node]) => {
|
||||
@@ -633,30 +638,29 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const menuRef = getMenuRef(itemId);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => handleFileClick(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FileIcon}
|
||||
alt={t('settings.sources.fileAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm md:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{node.size_bytes ? formatBytes(node.size_bytes) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -680,8 +684,8 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}),
|
||||
];
|
||||
@@ -834,31 +838,31 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
<div className="mb-2">{renderPathNavigation()}</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<table className="w-full min-w-[600px] table-auto bg-transparent">
|
||||
<thead className="bg-gray-100 dark:bg-[#27282D]">
|
||||
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<th className="min-w-[200px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40%" align="left">
|
||||
{t('settings.sources.fileName')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
{t('settings.sources.tokens')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="30%" align="left">
|
||||
{t('settings.sources.size')}
|
||||
</th>
|
||||
<th className="w-[60px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="20%" align="right">
|
||||
{t('settings.sources.tokens')}
|
||||
</TableHeader>
|
||||
<TableHeader width="10%" align="right">
|
||||
<span className="sr-only">
|
||||
{t('settings.sources.actions')}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="[&>tr:last-child]:border-b-0">
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderFileTree(currentDirectory)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
174
frontend/src/components/Table.tsx
Normal file
174
frontend/src/components/Table.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TableProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
minWidth?: string;
|
||||
}
|
||||
|
||||
|
||||
interface TableContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
height?: string;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
interface TableHeadProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TableRowProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface TableCellProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
minWidth?: string;
|
||||
width?: string;
|
||||
align?: 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
const TableContainer = React.forwardRef<HTMLDivElement, TableContainerProps>(({
|
||||
children,
|
||||
className = '',
|
||||
height = 'auto',
|
||||
bordered = true
|
||||
}, ref) => {
|
||||
return (
|
||||
<div className={`relative rounded-[6px] ${className}`}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`w-full overflow-x-auto rounded-[6px] bg-gray-100 dark:bg-[#27282D] ${bordered ? 'border border-[#EEE6FF78] dark:border-[#6A6A6A]' : ''}`}
|
||||
style={{
|
||||
maxHeight: height === 'auto' ? undefined : height,
|
||||
overflowY: height === 'auto' ? 'hidden' : 'auto'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});;
|
||||
const Table: React.FC<TableProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth = 'min-w-[600px]'
|
||||
}) => {
|
||||
return (
|
||||
<table className={`w-full table-auto border-collapse bg-transparent ${minWidth} ${className}`}>
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<thead className={`
|
||||
sticky top-0 z-10
|
||||
bg-gray-100 dark:bg-[#27282D]
|
||||
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-[#EEE6FF78] dark:before:bg-[#6A6A6A]
|
||||
after:content-[''] after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-[#EEE6FF78] dark:after:bg-[#6A6A6A]
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
const TableBody: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<tbody className={`[&>tr:last-child]:border-b-0 ${className}`}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
};
|
||||
|
||||
const TableRow: React.FC<TableRowProps> = ({ children, className = '', onClick }) => {
|
||||
const baseClasses = "border-b border-[#EEE6FF78] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]";
|
||||
const cursorClass = onClick ? "cursor-pointer" : "";
|
||||
|
||||
return (
|
||||
<tr className={`${baseClasses} ${cursorClass} ${className}`} onClick={onClick}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const TableHeader: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth,
|
||||
width,
|
||||
align = 'left'
|
||||
}) => {
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return 'text-right';
|
||||
case 'center':
|
||||
return 'text-center';
|
||||
default:
|
||||
return 'text-left';
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses = `px-2 py-3 text-sm font-medium text-gray-700 lg:px-3 dark:text-[#59636E] border-t border-b border-[#EEE6FF78] dark:border-[#6A6A6A] relative box-border ${getAlignmentClass()}`;
|
||||
const widthClasses = minWidth ? minWidth : '';
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`${baseClasses} ${widthClasses} ${className}`}
|
||||
style={width ? { width, minWidth: width, maxWidth: width } : {}}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
const TableCell: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth,
|
||||
width,
|
||||
align = 'left'
|
||||
}) => {
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return 'text-right';
|
||||
case 'center':
|
||||
return 'text-center';
|
||||
default:
|
||||
return 'text-left';
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses = `px-2 py-2 text-sm lg:px-3 dark:text-[#E0E0E0] box-border ${getAlignmentClass()}`;
|
||||
const widthClasses = minWidth ? minWidth : '';
|
||||
|
||||
return (
|
||||
<td
|
||||
className={`${baseClasses} ${widthClasses} ${className}`}
|
||||
style={width ? { width, minWidth: width, maxWidth: width } : {}}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -272,27 +272,27 @@ export default function Sources({
|
||||
|
||||
return documentToView ? (
|
||||
<div className="mt-8 flex flex-col">
|
||||
{documentToView.isNested ? (
|
||||
documentToView.type === 'connector' ? (
|
||||
<ConnectorTreeComponent
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
{documentToView.isNested ? (
|
||||
documentToView.type === 'connector:file' ? (
|
||||
<ConnectorTreeComponent
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<FileTreeComponent
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<FileTreeComponent
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
<Chunks
|
||||
documentId={documentToView.id || ''}
|
||||
documentName={documentToView.name}
|
||||
handleGoBack={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Chunks
|
||||
documentId={documentToView.id || ''}
|
||||
documentName={documentToView.name}
|
||||
handleGoBack={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 flex w-full max-w-full flex-col overflow-hidden">
|
||||
@@ -352,18 +352,17 @@ export default function Sources({
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 px-2 py-4">
|
||||
{currentDocuments.map((document, index) => {
|
||||
const docId = document.id ? document.id.toString() : '';
|
||||
{currentDocuments.map((document, index) => {
|
||||
const docId = document.id ? document.id.toString() : '';
|
||||
|
||||
return (
|
||||
<div key={docId} className="relative">
|
||||
<div
|
||||
className={`flex h-[130px] w-full flex-col rounded-2xl bg-[#F9F9F9] p-3 transition-all duration-200 dark:bg-[#383838] ${
|
||||
activeMenuId === docId || syncMenuState.docId === docId
|
||||
? 'scale-[1.05]'
|
||||
: 'hover:scale-[1.05]'
|
||||
}`}
|
||||
>
|
||||
return (
|
||||
<div key={docId} className="relative">
|
||||
<div
|
||||
className={`flex h-[130px] w-full flex-col rounded-2xl bg-[#F9F9F9] p-3 transition-all duration-200 dark:bg-[#383838] ${activeMenuId === docId || syncMenuState.docId === docId
|
||||
? 'scale-[1.05]'
|
||||
: 'hover:scale-[1.05]'
|
||||
}`}
|
||||
>
|
||||
<div className="w-full flex-1">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<h3
|
||||
|
||||
Reference in New Issue
Block a user