| import { useRef, useCallback } from 'react'; |
| import { useRecoilState } from 'recoil'; |
| import { useToastContext } from '@librechat/client'; |
| import type { SPPickerConfig } from '~/components/SidePanel/Agents/config'; |
| import { useLocalize, useAuthContext } from '~/hooks'; |
| import { useGetStartupConfig } from '~/data-provider'; |
| import useSharePointToken from './useSharePointToken'; |
| import store from '~/store'; |
|
|
| interface UseSharePointPickerProps { |
| containerNode: HTMLDivElement | null; |
| onFilesSelected?: (files: any[]) => void; |
| onClose?: () => void; |
| disabled?: boolean; |
| maxSelectionCount?: number; |
| } |
|
|
| interface UseSharePointPickerReturn { |
| openSharePointPicker: () => void; |
| closeSharePointPicker: () => void; |
| error: string | null; |
| cleanup: () => void; |
| isTokenLoading: boolean; |
| } |
|
|
| export default function useSharePointPicker({ |
| containerNode, |
| onFilesSelected, |
| onClose, |
| disabled = false, |
| maxSelectionCount = 10, |
| }: UseSharePointPickerProps): UseSharePointPickerReturn { |
| const [langcode] = useRecoilState(store.lang); |
| const { user } = useAuthContext(); |
| const { showToast } = useToastContext(); |
| const localize = useLocalize(); |
| const iframeRef = useRef<HTMLIFrameElement | null>(null); |
| const portRef = useRef<MessagePort | null>(null); |
| const channelIdRef = useRef<string>(''); |
|
|
| const { data: startupConfig } = useGetStartupConfig(); |
|
|
| const sharePointBaseUrl = startupConfig?.sharePointBaseUrl; |
| const isEntraIdUser = user?.provider === 'openid'; |
|
|
| const { |
| token, |
| isLoading: isTokenLoading, |
| error: tokenError, |
| } = useSharePointToken({ |
| enabled: isEntraIdUser && !disabled && !!sharePointBaseUrl, |
| purpose: 'Pick', |
| }); |
|
|
| const generateChannelId = useCallback(() => { |
| return `sharepoint-picker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; |
| }, []); |
|
|
| const portMessageHandler = useCallback( |
| async (message: MessageEvent) => { |
| const port = portRef.current; |
| if (!port) { |
| console.error('No port available for communication'); |
| return; |
| } |
|
|
| try { |
| switch (message.data.type) { |
| case 'notification': |
| console.log('SharePoint picker notification:', message.data); |
| break; |
|
|
| case 'command': { |
| |
| port.postMessage({ |
| type: 'acknowledge', |
| id: message.data.id, |
| }); |
|
|
| const command = message.data.data; |
| console.log('SharePoint picker command:', command); |
|
|
| switch (command.command) { |
| case 'authenticate': |
| console.log('Authentication requested, providing token'); |
| console.log('Command details:', command); |
| console.log('Token available:', !!token?.access_token); |
| if (token?.access_token) { |
| port.postMessage({ |
| type: 'result', |
| id: message.data.id, |
| data: { |
| result: 'token', |
| token: token.access_token, |
| }, |
| }); |
| } else { |
| console.error('No token available for authentication'); |
| port.postMessage({ |
| type: 'result', |
| id: message.data.id, |
| data: { |
| result: 'error', |
| error: { |
| code: 'noToken', |
| message: 'No authentication token available', |
| }, |
| }, |
| }); |
| } |
| break; |
|
|
| case 'close': |
| console.log('Close command received'); |
| port.postMessage({ |
| type: 'result', |
| id: message.data.id, |
| data: { |
| result: 'success', |
| }, |
| }); |
| onClose?.(); |
| break; |
|
|
| case 'pick': { |
| console.log('Files picked from SharePoint:', command); |
|
|
| const items = command.items || command.files || []; |
| console.log('Extracted items:', items); |
|
|
| if (items && items.length > 0) { |
| const selectedFiles = items.map((item: any) => ({ |
| id: item.id || item.shareId || item.driveItem?.id, |
| name: item.name || item.driveItem?.name, |
| size: item.size || item.driveItem?.size, |
| webUrl: item.webUrl || item.driveItem?.webUrl, |
| downloadUrl: |
| item.downloadUrl || item.driveItem?.['@microsoft.graph.downloadUrl'], |
| driveId: |
| item.driveId || |
| item.parentReference?.driveId || |
| item.driveItem?.parentReference?.driveId, |
| itemId: item.id || item.driveItem?.id, |
| sharePointItem: item, |
| })); |
|
|
| console.log('Processed SharePoint files:', selectedFiles); |
|
|
| if (onFilesSelected) { |
| onFilesSelected(selectedFiles); |
| } |
|
|
| showToast({ |
| message: `Selected ${selectedFiles.length} file(s) from SharePoint`, |
| status: 'success', |
| }); |
| } |
|
|
| port.postMessage({ |
| type: 'result', |
| id: message.data.id, |
| data: { |
| result: 'success', |
| }, |
| }); |
| break; |
| } |
|
|
| default: |
| console.warn(`Unsupported command: ${command.command}`); |
| port.postMessage({ |
| type: 'result', |
| id: message.data.id, |
| data: { |
| result: 'error', |
| error: { |
| code: 'unsupportedCommand', |
| message: command.command, |
| }, |
| }, |
| }); |
| break; |
| } |
| break; |
| } |
|
|
| default: |
| console.log('Unknown message type:', message.data.type); |
| break; |
| } |
| } catch (error) { |
| console.error('Error processing port message:', error); |
| } |
| }, |
| [token, onFilesSelected, showToast, onClose], |
| ); |
|
|
| |
| const initMessageHandler = useCallback( |
| (event: MessageEvent) => { |
| console.log('=== SharePoint picker init message received ==='); |
| console.log('Event source:', event.source); |
| console.log('Event data:', event.data); |
| console.log('Expected channelId:', channelIdRef.current); |
|
|
| |
| if (event.source && event.source === iframeRef.current?.contentWindow) { |
| const message = event.data; |
|
|
| if (message.type === 'initialize' && message.channelId === channelIdRef.current) { |
| console.log('Establishing MessagePort communication'); |
|
|
| |
| portRef.current = event.ports[0]; |
|
|
| if (portRef.current) { |
| |
| portRef.current.addEventListener('message', portMessageHandler); |
| portRef.current.start(); |
|
|
| |
| portRef.current.postMessage({ |
| type: 'activate', |
| }); |
|
|
| console.log('MessagePort established and activated'); |
| } else { |
| console.error('No MessagePort found in initialize event'); |
| } |
| } |
| } |
| }, |
| [portMessageHandler], |
| ); |
|
|
| const openSharePointPicker = async () => { |
| if (!token) { |
| showToast({ |
| message: 'Unable to access SharePoint. Please ensure you are logged in with Microsoft.', |
| status: 'error', |
| }); |
| return; |
| } |
|
|
| if (!containerNode) { |
| console.error('No container ref provided for SharePoint picker'); |
| return; |
| } |
|
|
| try { |
| const channelId = generateChannelId(); |
| channelIdRef.current = channelId; |
|
|
| console.log('=== SharePoint File Picker v8 (MessagePort) ==='); |
| console.log('Token available:', { |
| hasToken: !!token.access_token, |
| tokenType: token.token_type, |
| expiresIn: token.expires_in, |
| scopes: token.scope, |
| }); |
| console.log('Channel ID:', channelId); |
|
|
| const pickerOptions: SPPickerConfig = { |
| sdk: '8.0', |
| entry: { |
| sharePoint: {}, |
| }, |
| messaging: { |
| origin: window.location.origin, |
| channelId: channelId, |
| }, |
| authentication: { |
| enabled: false, |
| }, |
| typesAndSources: { |
| mode: 'files', |
| pivots: { |
| oneDrive: true, |
| recent: true, |
| shared: true, |
| sharedLibraries: true, |
| myOrganization: true, |
| site: true, |
| }, |
| }, |
| selection: { |
| mode: 'multiple', |
| maximumCount: maxSelectionCount, |
| }, |
| title: localize('com_files_sharepoint_picker_title'), |
| commands: { |
| upload: { |
| enabled: false, |
| }, |
| createFolder: { |
| enabled: false, |
| }, |
| }, |
| search: { enabled: true }, |
| }; |
|
|
| const iframe = document.createElement('iframe'); |
| iframe.style.width = '100%'; |
| iframe.style.height = '100%'; |
| iframe.style.background = '#F5F5F5'; |
| iframe.style.border = 'none'; |
| iframe.title = 'SharePoint File Picker'; |
| iframe.setAttribute( |
| 'sandbox', |
| 'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox', |
| ); |
| iframeRef.current = iframe; |
|
|
| containerNode.innerHTML = ''; |
| containerNode.appendChild(iframe); |
|
|
| activeEventListenerRef.current = initMessageHandler; |
| window.addEventListener('message', initMessageHandler); |
|
|
| iframe.src = 'about:blank'; |
| iframe.onload = () => { |
| const win = iframe.contentWindow; |
| if (!win) return; |
|
|
| const queryString = new URLSearchParams({ |
| filePicker: JSON.stringify(pickerOptions), |
| locale: langcode || 'en-US', |
| }); |
|
|
| const url = sharePointBaseUrl + `/_layouts/15/FilePicker.aspx?${queryString}`; |
|
|
| const form = win.document.createElement('form'); |
| form.setAttribute('action', url); |
| form.setAttribute('method', 'POST'); |
|
|
| const tokenInput = win.document.createElement('input'); |
| tokenInput.setAttribute('type', 'hidden'); |
| tokenInput.setAttribute('name', 'access_token'); |
| tokenInput.setAttribute('value', token.access_token); |
| form.appendChild(tokenInput); |
|
|
| win.document.body.appendChild(form); |
| form.submit(); |
| }; |
| } catch (error) { |
| console.error('SharePoint file picker error:', error); |
| showToast({ |
| message: 'Failed to open SharePoint file picker.', |
| status: 'error', |
| }); |
| } |
| }; |
| const activeEventListenerRef = useRef<((event: MessageEvent) => void) | null>(null); |
|
|
| const cleanup = useCallback(() => { |
| if (activeEventListenerRef.current) { |
| window.removeEventListener('message', activeEventListenerRef.current); |
| activeEventListenerRef.current = null; |
| } |
| if (portRef.current) { |
| portRef.current.close(); |
| portRef.current = null; |
| } |
| if (containerNode) { |
| containerNode.innerHTML = ''; |
| } |
| channelIdRef.current = ''; |
| }, [containerNode]); |
|
|
| const handleDialogClose = useCallback(() => { |
| cleanup(); |
| }, [cleanup]); |
|
|
| const isAvailable = startupConfig?.sharePointFilePickerEnabled && isEntraIdUser && !tokenError; |
|
|
| return { |
| openSharePointPicker: isAvailable ? openSharePointPicker : () => {}, |
| closeSharePointPicker: handleDialogClose, |
| error: tokenError ? 'Failed to authenticate with SharePoint' : null, |
| cleanup, |
| isTokenLoading, |
| }; |
| } |
|
|