| | 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, |
| | }; |
| | } |
| |
|