(feat:pickers) ux, code refactor

This commit is contained in:
ManishMadan2882
2025-09-20 00:04:27 +05:30
parent 42b83c5994
commit 6574d9cc84
3 changed files with 302 additions and 287 deletions

View File

@@ -286,204 +286,184 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
}
};
// Render authentication UI
if (!isConnected) {
return (
<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('');
if (data.session_token) {
setSessionToken(provider, data.session_token);
loadCloudFiles(data.session_token, null);
}
}}
onError={(error) => {
setAuthError(error);
setIsConnected(false);
}}
/>
</div>
);
}
// Render file browser UI
return (
<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] 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" />
</svg>
<span>Connected as {userEmail}</span>
</div>
<button
onClick={() => {
const sessionToken = getSessionToken(provider);
if (sessionToken) {
const apiHost = import.meta.env.VITE_API_HOST;
fetch(`${apiHost}/api/connectors/disconnect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ provider: provider, session_token: sessionToken })
}).catch(err => console.error(`Error disconnecting from ${getProviderConfig(provider).displayName}:`, err));
}
{authError && (
<div className="text-red-500 text-sm mb-4 text-center">{authError}</div>
)}
removeSessionToken(provider);
setIsConnected(false);
setFiles([]);
setSelectedFiles([]);
onSelectionChange([]);
<ConnectorAuth
provider={provider}
onSuccess={(data) => {
setUserEmail(data.user_email || 'Connected User');
setIsConnected(true);
setAuthError('');
if (onDisconnect) {
onDisconnect();
}
}}
className="text-[#212121] hover:text-gray-700 font-medium text-xs underline"
>
Disconnect
</button>
</div>
</div>
if (data.session_token) {
setSessionToken(provider, data.session_token);
loadCloudFiles(data.session_token, null);
}
}}
onError={(error) => {
setAuthError(error);
setIsConnected(false);
}}
isConnected={isConnected}
userEmail={userEmail}
onDisconnect={() => {
const sessionToken = getSessionToken(provider);
if (sessionToken) {
const apiHost = import.meta.env.VITE_API_HOST;
fetch(`${apiHost}/api/connectors/disconnect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ provider: provider, session_token: sessionToken })
}).catch(err => console.error(`Error disconnecting from ${getProviderConfig(provider).displayName}:`, err));
}
<div className="border border-[#D7D7D7] rounded-lg dark:border-[#6A6A6A] mt-3">
<div className=" border-[#EEE6FF78] dark:border-[#6A6A6A] rounded-t-lg">
{/* Breadcrumb navigation */}
<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}
>
{path.name}
</button>
</div>
))}
removeSessionToken(provider);
setIsConnected(false);
setFiles([]);
setSelectedFiles([]);
onSelectionChange([]);
if (onDisconnect) {
onDisconnect();
}
}}
/>
{isConnected && (
<div className="border border-[#D7D7D7] rounded-lg dark:border-[#6A6A6A] mt-3">
<div className="border-[#EEE6FF78] dark:border-[#6A6A6A] rounded-t-lg">
{/* Breadcrumb navigation */}
<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}
>
{path.name}
</button>
</div>
))}
</div>
<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 + selectedFolders.length} selected
</div>
</div>
<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 + selectedFolders.length} selected
</div>
</div>
<div className="h-72">
<TableContainer
ref={scrollContainerRef}
height="288px"
className="scrollbar-thin md:w-4xl lg:w-5xl"
bordered={false}
>
{(
<>
<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}`}
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 text-sm 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 className='text-xs'>
{formatDate(file.modifiedTime)}
</TableCell>
<TableCell className='text-xs'>
{file.size ? formatBytes(file.size) : '-'}
</TableCell>
<div className="h-72">
<TableContainer
ref={scrollContainerRef}
height="288px"
className="scrollbar-thin md:w-4xl lg:w-5xl"
bordered={false}
>
{(
<>
<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>
))}
</TableBody>
</Table>
</TableHead>
<TableBody>
{files.map((file, index) => (
<TableRow
key={`${file.id}-${index}`}
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 text-sm 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 className='text-xs'>
{formatDate(file.modifiedTime)}
</TableCell>
<TableCell className='text-xs'>
{file.size ? formatBytes(file.size) : '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{isLoading && (
<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...
{isLoading && (
<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>
</div>
)}
</>
)}
</TableContainer>
)}
</>
)}
</TableContainer>
</div>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -17,15 +17,11 @@ interface PickerFile {
interface GoogleDrivePickerProps {
token: string | null;
onSelectionChange: (fileIds: string[], folderIds?: string[]) => void;
initialSelectedFiles?: string[];
initialSelectedFolders?: string[];
}
const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
token,
onSelectionChange,
initialSelectedFiles = [],
initialSelectedFolders = [],
}) => {
const [selectedFiles, setSelectedFiles] = useState<PickerFile[]>([]);
const [selectedFolders, setSelectedFolders] = useState<PickerFile[]>([]);
@@ -34,12 +30,15 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
const [isConnected, setIsConnected] = useState(false);
const [authError, setAuthError] = useState<string>('');
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [openPicker] = useDrivePicker();
useEffect(() => {
const sessionToken = getSessionToken('google_drive');
if (sessionToken) {
setIsValidating(true);
setIsConnected(true); // Optimistically set as connected for skeleton
validateSession(sessionToken);
}
}, [token]);
@@ -59,6 +58,7 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
if (!validateResponse.ok) {
setIsConnected(false);
setAuthError('Session expired. Please reconnect to Google Drive.');
setIsValidating(false);
return false;
}
@@ -68,16 +68,19 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
setIsConnected(true);
setAuthError('');
setAccessToken(validateData.access_token || null);
setIsValidating(false);
return true;
} else {
setIsConnected(false);
setAuthError(validateData.error || 'Session expired. Please reconnect your account.');
setIsValidating(false);
return false;
}
} catch (error) {
console.error('Error validating session:', error);
setAuthError('Failed to validate session. Please reconnect.');
setIsConnected(false);
setIsValidating(false);
return false;
}
};
@@ -100,17 +103,15 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
}
try {
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const developerKey = import.meta.env.VITE_GOOGLE_API_KEY;
const appId = import.meta.env.VITE_GOOGLE_DRIVE_APP_ID;
const clientId: string = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const developerKey : string = import.meta.env.VITE_GOOGLE_API_KEY;
// Derive appId from clientId (extract numeric part before first dash)
const appId = clientId ? clientId.split('-')[0] : null;
if (!clientId || !developerKey || !appId) {
console.error('Missing Google Drive configuration:', {
clientId: !!clientId,
developerKey: !!developerKey,
appId: !!appId
});
setAuthError('Google Drive configuration is incomplete. Please check your environment variables.');
console.error('Missing Google Drive configuration');
setIsLoading(false);
return;
}
@@ -199,105 +200,141 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
setIsConnected(false);
setSelectedFiles([]);
setSelectedFolders([]);
setAccessToken(null);
setUserEmail('');
setAuthError('');
onSelectionChange([], []);
};
const ConnectedStateSkeleton = () => (
<div className="mb-4">
<div className="w-full flex items-center justify-between rounded-[10px] bg-gray-200 dark:bg-gray-700 px-4 py-2 animate-pulse">
<div className="flex items-center gap-2">
<div className="h-4 w-4 bg-gray-300 dark:bg-gray-600 rounded"></div>
<div className="h-4 w-32 bg-gray-300 dark:bg-gray-600 rounded"></div>
</div>
<div className="h-4 w-16 bg-gray-300 dark:bg-gray-600 rounded"></div>
</div>
</div>
);
const FilesSectionSkeleton = () => (
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<div className="h-5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
<div className="h-8 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
</div>
<div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
</div>
</div>
);
return (
<div>
<ConnectorAuth
provider="google_drive"
label="Connect to Google Drive"
onSuccess={(data) => {
setUserEmail(data.user_email || 'Connected User');
setIsConnected(true);
setAuthError('');
{isValidating ? (
<>
<ConnectedStateSkeleton />
<FilesSectionSkeleton />
</>
) : (
<>
<ConnectorAuth
provider="google_drive"
label="Connect to Google Drive"
onSuccess={(data) => {
setUserEmail(data.user_email || 'Connected User');
setIsConnected(true);
setAuthError('');
if (data.session_token) {
setSessionToken('google_drive', data.session_token);
validateSession(data.session_token);
}
}}
onError={(error) => {
setAuthError(error);
setIsConnected(false);
}}
isConnected={isConnected}
userEmail={userEmail}
onDisconnect={handleDisconnect}
errorMessage={authError}
/>
if (data.session_token) {
setSessionToken('google_drive', data.session_token);
validateSession(data.session_token);
}
}}
onError={(error) => {
setAuthError(error);
setIsConnected(false);
}}
isConnected={isConnected}
userEmail={userEmail}
onDisconnect={handleDisconnect}
errorMessage={authError}
/>
{isConnected && (
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-medium">Selected Files</h3>
<button
onClick={() => handleOpenPicker()}
className="bg-[#A076F6] hover:bg-[#8A5FD4] text-white text-sm py-1 px-3 rounded-md"
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Select Files'}
</button>
</div>
{isConnected && (
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-medium">Selected Files</h3>
<button
onClick={() => handleOpenPicker()}
className="bg-[#A076F6] hover:bg-[#8A5FD4] text-white text-sm py-1 px-3 rounded-md"
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Select Files'}
</button>
</div>
{selectedFiles.length === 0 && selectedFolders.length === 0 ? (
<p className="text-gray-600 dark:text-gray-400 text-sm">No files or folders selected</p>
) : (
<div className="max-h-60 overflow-y-auto">
{selectedFolders.length > 0 && (
<div className="mb-2">
<h4 className="text-xs font-medium text-gray-500 mb-1">Folders</h4>
{selectedFolders.map((folder) => (
<div key={folder.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={folder.iconUrl} alt="Folder" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{folder.name}</span>
<button
onClick={() => {
const newSelectedFolders = selectedFolders.filter(f => f.id !== folder.id);
setSelectedFolders(newSelectedFolders);
onSelectionChange(
selectedFiles.map(f => f.id),
newSelectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
{selectedFiles.length === 0 && selectedFolders.length === 0 ? (
<p className="text-gray-600 dark:text-gray-400 text-sm">No files or folders selected</p>
) : (
<div className="max-h-60 overflow-y-auto">
{selectedFolders.length > 0 && (
<div className="mb-2">
<h4 className="text-xs font-medium text-gray-500 mb-1">Folders</h4>
{selectedFolders.map((folder) => (
<div key={folder.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={folder.iconUrl} alt="Folder" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{folder.name}</span>
<button
onClick={() => {
const newSelectedFolders = selectedFolders.filter(f => f.id !== folder.id);
setSelectedFolders(newSelectedFolders);
onSelectionChange(
selectedFiles.map(f => f.id),
newSelectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
</div>
))}
</div>
))}
</div>
)}
)}
{selectedFiles.length > 0 && (
<div>
<h4 className="text-xs font-medium text-gray-500 mb-1">Files</h4>
{selectedFiles.map((file) => (
<div key={file.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={file.iconUrl} alt="File" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{file.name}</span>
<button
onClick={() => {
const newSelectedFiles = selectedFiles.filter(f => f.id !== file.id);
setSelectedFiles(newSelectedFiles);
onSelectionChange(
newSelectedFiles.map(f => f.id),
selectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
{selectedFiles.length > 0 && (
<div>
<h4 className="text-xs font-medium text-gray-500 mb-1">Files</h4>
{selectedFiles.map((file) => (
<div key={file.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={file.iconUrl} alt="File" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{file.name}</span>
<button
onClick={() => {
const newSelectedFiles = selectedFiles.filter(f => f.id !== file.id);
setSelectedFiles(newSelectedFiles);
onSelectionChange(
newSelectedFiles.map(f => f.id),
selectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
</div>
))}
</div>
))}
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
</>
)}
</div>
);

View File

@@ -241,8 +241,6 @@ function Upload({
setSelectedFolders(selectedFolderIds);
}}
token={token}
initialSelectedFiles={selectedFiles}
initialSelectedFolders={selectedFolders}
/>
);
default: