📁 feat: Integrate SharePoint File Picker and Download Workflow (#8651)

* feat(sharepoint): integrate SharePoint file picker and download workflow
Introduces end‑to‑end SharePoint import support:
* Token exchange with Microsoft Graph and scope management (`useSharePointToken`)
* Re‑usable hooks: `useSharePointPicker`, `useSharePointDownload`,
  `useSharePointFileHandling`
* FileSearch dropdown now offers **From Local Machine** / **From SharePoint**
  sources and gracefully falls back when SharePoint is disabled
* Agent upload model, `AttachFileMenu`, and `DropdownPopup` extended for
  SharePoint files and sub‑menus
* Blurry overlay with progress indicator and `maxSelectionCount` limit during
  downloads
* Cache‑flush utility (`config/flush-cache.js`) supporting Redis & filesystem,
  with dry‑run and npm script
* Updated `SharePointIcon` (uses `currentColor`) and new i18n keys
* Bug fixes: placeholder syntax in progress message, picker event‑listener
  cleanup
* Misc style and performance optimizations

* Fix ESLint warnings

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
This commit is contained in:
Danny Avila 2025-07-25 00:03:23 -04:00
parent b6413b06bc
commit a955097faf
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
40 changed files with 2500 additions and 123 deletions

View file

@ -0,0 +1,137 @@
import React, { useState, useEffect } from 'react';
import {
OGDialog,
OGDialogTitle,
OGDialogPortal,
OGDialogOverlay,
OGDialogContent,
} from '@librechat/client';
import type { SharePointBatchProgress } from '~/data-provider/Files/sharepoint';
import { useSharePointPicker, useLocalize } from '~/hooks';
interface SharePointPickerDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onFilesSelected?: (files: any[]) => void;
disabled?: boolean;
isDownloading?: boolean;
downloadProgress?: SharePointBatchProgress | null;
maxSelectionCount?: number;
}
export default function SharePointPickerDialog({
isOpen,
onOpenChange,
onFilesSelected,
disabled = false,
isDownloading = false,
downloadProgress = null,
maxSelectionCount,
}: SharePointPickerDialogProps) {
const [containerNode, setContainerNode] = useState<HTMLDivElement | null>(null);
const localize = useLocalize();
const { openSharePointPicker, closeSharePointPicker, cleanup } = useSharePointPicker({
containerNode,
onFilesSelected,
disabled,
onClose: () => handleOpenChange(false),
maxSelectionCount,
});
const handleOpenChange = (open: boolean) => {
if (!open) {
closeSharePointPicker();
}
onOpenChange(open);
};
// Use callback ref to trigger SharePoint picker when container is attached
const containerCallbackRef = React.useCallback((node: HTMLDivElement | null) => {
setContainerNode(node);
}, []);
useEffect(() => {
if (containerNode && isOpen) {
openSharePointPicker();
}
return () => {
if (!isOpen) {
cleanup();
}
};
// we need to run this effect only when the containerNode or isOpen changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerNode, isOpen]);
return (
<OGDialog open={isOpen} onOpenChange={handleOpenChange}>
<OGDialogPortal>
<OGDialogOverlay className="bg-black/50" />
<OGDialogContent
className="bg-#F5F5F5 sharepoint-picker-bg fixed left-1/2 top-1/2 z-50 h-[680px] max-h-[90vh] max-w-[90vw] -translate-x-1/2 -translate-y-1/2 rounded-lg border p-2 shadow-lg focus:outline-none"
showCloseButton={true}
>
<OGDialogTitle className="sr-only">
{localize('com_files_sharepoint_picker_title')}
</OGDialogTitle>
<div ref={containerCallbackRef} className="sharepoint-picker-bg relative flex p-2">
{/* SharePoint iframe will be injected here by the hook */}
{isDownloading && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-black/30 backdrop-blur-sm">
<div className="mx-4 w-full max-w-sm rounded-lg bg-white p-6 shadow-lg">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
<h3 className="mb-2 text-lg font-semibold text-gray-900">
{localize('com_files_downloading')}
</h3>
{downloadProgress && (
<div className="space-y-2">
<p className="text-sm text-gray-600">
{localize('com_files_download_progress', {
0: downloadProgress.completed,
1: downloadProgress.total,
})}
</p>
{downloadProgress.currentFile && (
<p className="truncate text-xs text-gray-500">
{downloadProgress.currentFile}
</p>
)}
<div className="h-2 w-full rounded-full bg-gray-200">
<div
className="h-2 rounded-full bg-blue-600 transition-all duration-300"
style={{
width: `${Math.round((downloadProgress.completed / downloadProgress.total) * 100)}%`,
}}
></div>
</div>
<p className="text-xs text-gray-500">
{localize('com_files_download_percent_complete', {
0: Math.round(
(downloadProgress.completed / downloadProgress.total) * 100,
),
})}
</p>
{downloadProgress.failed.length > 0 && (
<p className="text-xs text-red-500">
{localize('com_files_download_failed', {
0: downloadProgress.failed.length,
})}
</p>
)}
</div>
)}
{!downloadProgress && (
<p className="text-sm text-gray-600">
{localize('com_files_preparing_download')}
</p>
)}
</div>
</div>
</div>
)}
</div>
</OGDialogContent>
</OGDialogPortal>
</OGDialog>
);
}

View file

@ -0,0 +1 @@
export { default as SharePointPickerDialog } from './SharePointPickerDialog';