mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
📁 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:
parent
b6413b06bc
commit
a955097faf
40 changed files with 2500 additions and 123 deletions
|
|
@ -5,3 +5,7 @@ export { default as useFileDeletion } from './useFileDeletion';
|
|||
export { default as useUpdateFiles } from './useUpdateFiles';
|
||||
export { default as useDragHelpers } from './useDragHelpers';
|
||||
export { default as useFileMap } from './useFileMap';
|
||||
export { default as useSharePointPicker } from './useSharePointPicker';
|
||||
export { default as useSharePointDownload } from './useSharePointDownload';
|
||||
export { default as useSharePointFileHandling } from './useSharePointFileHandling';
|
||||
export { default as useSharePointToken } from './useSharePointToken';
|
||||
|
|
|
|||
129
client/src/hooks/Files/useSharePointDownload.ts
Normal file
129
client/src/hooks/Files/useSharePointDownload.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import type { SharePointFile, SharePointBatchProgress } from '~/data-provider/Files';
|
||||
import { useSharePointBatchDownload } from '~/data-provider/Files';
|
||||
import useSharePointToken from './useSharePointToken';
|
||||
|
||||
interface UseSharePointDownloadProps {
|
||||
onFilesDownloaded?: (files: File[]) => void | Promise<void>;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface UseSharePointDownloadReturn {
|
||||
downloadSharePointFiles: (files: SharePointFile[]) => Promise<File[]>;
|
||||
isDownloading: boolean;
|
||||
downloadProgress: SharePointBatchProgress | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export default function useSharePointDownload({
|
||||
onFilesDownloaded,
|
||||
onError,
|
||||
}: UseSharePointDownloadProps = {}): UseSharePointDownloadReturn {
|
||||
const { showToast } = useToastContext();
|
||||
const [downloadProgress, setDownloadProgress] = useState<SharePointBatchProgress | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { token, refetch: refetchToken } = useSharePointToken({
|
||||
enabled: false,
|
||||
purpose: 'Download',
|
||||
});
|
||||
|
||||
const batchDownloadMutation = useSharePointBatchDownload();
|
||||
|
||||
const downloadSharePointFiles = useCallback(
|
||||
async (files: SharePointFile[]): Promise<File[]> => {
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('No files provided for download');
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setDownloadProgress({ completed: 0, total: files.length, failed: [] });
|
||||
|
||||
try {
|
||||
let accessToken = token?.access_token;
|
||||
if (!accessToken) {
|
||||
showToast({
|
||||
message: 'Getting SharePoint access token...',
|
||||
status: 'info',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
const tokenResult = await refetchToken();
|
||||
accessToken = tokenResult.data?.access_token;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Failed to obtain SharePoint access token');
|
||||
}
|
||||
}
|
||||
|
||||
showToast({
|
||||
message: `Downloading ${files.length} file(s) from SharePoint...`,
|
||||
status: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
const downloadedFiles = await batchDownloadMutation.mutateAsync({
|
||||
files,
|
||||
accessToken,
|
||||
onProgress: (progress) => {
|
||||
setDownloadProgress(progress);
|
||||
|
||||
if (files.length > 5 && progress.completed % 3 === 0) {
|
||||
showToast({
|
||||
message: `Downloaded ${progress.completed}/${progress.total} files...`,
|
||||
status: 'info',
|
||||
duration: 1000,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (downloadedFiles.length > 0) {
|
||||
const failedCount = files.length - downloadedFiles.length;
|
||||
const successMessage =
|
||||
failedCount > 0
|
||||
? `Downloaded ${downloadedFiles.length}/${files.length} files from SharePoint (${failedCount} failed)`
|
||||
: `Successfully downloaded ${downloadedFiles.length} file(s) from SharePoint`;
|
||||
|
||||
showToast({
|
||||
message: successMessage,
|
||||
status: failedCount > 0 ? 'warning' : 'success',
|
||||
duration: 4000,
|
||||
});
|
||||
|
||||
if (onFilesDownloaded) {
|
||||
await onFilesDownloaded(downloadedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
setDownloadProgress(null);
|
||||
return downloadedFiles;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown download error';
|
||||
setError(errorMessage);
|
||||
|
||||
showToast({
|
||||
message: `SharePoint download failed: ${errorMessage}`,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error : new Error(errorMessage));
|
||||
}
|
||||
|
||||
setDownloadProgress(null);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[token, showToast, batchDownloadMutation, onFilesDownloaded, onError, refetchToken],
|
||||
);
|
||||
|
||||
return {
|
||||
downloadSharePointFiles,
|
||||
isDownloading: batchDownloadMutation.isLoading,
|
||||
downloadProgress,
|
||||
error,
|
||||
};
|
||||
}
|
||||
57
client/src/hooks/Files/useSharePointFileHandling.ts
Normal file
57
client/src/hooks/Files/useSharePointFileHandling.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useCallback } from 'react';
|
||||
import useFileHandling from './useFileHandling';
|
||||
import useSharePointDownload from './useSharePointDownload';
|
||||
import type { SharePointFile } from '~/data-provider/Files/sharepoint';
|
||||
|
||||
interface UseSharePointFileHandlingProps {
|
||||
fileSetter?: any;
|
||||
fileFilter?: (file: File) => boolean;
|
||||
additionalMetadata?: Record<string, string | undefined>;
|
||||
overrideEndpoint?: any;
|
||||
overrideEndpointFileConfig?: any;
|
||||
toolResource?: string;
|
||||
}
|
||||
|
||||
interface UseSharePointFileHandlingReturn {
|
||||
handleSharePointFiles: (files: SharePointFile[]) => Promise<void>;
|
||||
isProcessing: boolean;
|
||||
downloadProgress: any;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export default function useSharePointFileHandling(
|
||||
props?: UseSharePointFileHandlingProps,
|
||||
): UseSharePointFileHandlingReturn {
|
||||
const { handleFiles } = useFileHandling(props);
|
||||
|
||||
const { downloadSharePointFiles, isDownloading, downloadProgress, error } = useSharePointDownload(
|
||||
{
|
||||
onFilesDownloaded: async (downloadedFiles: File[]) => {
|
||||
const fileArray = Array.from(downloadedFiles);
|
||||
await handleFiles(fileArray, props?.toolResource);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('SharePoint download failed:', error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleSharePointFiles = useCallback(
|
||||
async (sharePointFiles: SharePointFile[]) => {
|
||||
try {
|
||||
await downloadSharePointFiles(sharePointFiles);
|
||||
} catch (error) {
|
||||
console.error('SharePoint file handling error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[downloadSharePointFiles],
|
||||
);
|
||||
|
||||
return {
|
||||
handleSharePointFiles,
|
||||
isProcessing: isDownloading,
|
||||
downloadProgress,
|
||||
error,
|
||||
};
|
||||
}
|
||||
383
client/src/hooks/Files/useSharePointPicker.ts
Normal file
383
client/src/hooks/Files/useSharePointPicker.ts
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
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': {
|
||||
// Always acknowledge the command first
|
||||
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); // Add this line
|
||||
console.log('Token available:', !!token?.access_token); // Add this line
|
||||
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],
|
||||
);
|
||||
|
||||
// Initialization message handler - establishes MessagePort communication
|
||||
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);
|
||||
|
||||
// Check if this message is from our iframe
|
||||
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');
|
||||
|
||||
// Get the MessagePort from the event
|
||||
portRef.current = event.ports[0];
|
||||
|
||||
if (portRef.current) {
|
||||
// Set up the port message listener
|
||||
portRef.current.addEventListener('message', portMessageHandler);
|
||||
portRef.current.start();
|
||||
|
||||
// Send activate message to start the picker
|
||||
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, // Host app handles authentication
|
||||
},
|
||||
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,
|
||||
};
|
||||
}
|
||||
46
client/src/hooks/Files/useSharePointToken.ts
Normal file
46
client/src/hooks/Files/useSharePointToken.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useGraphTokenQuery, useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
interface UseSharePointTokenProps {
|
||||
enabled?: boolean;
|
||||
purpose: 'Pick' | 'Download';
|
||||
}
|
||||
|
||||
interface UseSharePointTokenReturn {
|
||||
token: any;
|
||||
isLoading: boolean;
|
||||
error: any;
|
||||
refetch: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function useSharePointToken({
|
||||
enabled = true,
|
||||
purpose,
|
||||
}: UseSharePointTokenProps): UseSharePointTokenReturn {
|
||||
const { user } = useAuthContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const sharePointBaseUrl = startupConfig?.sharePointBaseUrl;
|
||||
const sharePointPickerGraphScope = startupConfig?.sharePointPickerGraphScope;
|
||||
const sharePointPickerSharePointScope = startupConfig?.sharePointPickerSharePointScope;
|
||||
|
||||
const isEntraIdUser = user?.provider === 'openid';
|
||||
const graphScopes =
|
||||
purpose === 'Pick' ? sharePointPickerSharePointScope : sharePointPickerGraphScope;
|
||||
const {
|
||||
data: token,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useGraphTokenQuery({
|
||||
scopes: graphScopes,
|
||||
enabled: enabled && isEntraIdUser && !!sharePointBaseUrl,
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue