fix: sharepoint neutralisierer wieder eingebaut
This commit is contained in:
parent
1ff776c503
commit
39e74110cd
3 changed files with 449 additions and 116 deletions
|
|
@ -25,6 +25,7 @@ export interface NeutralizationConfig {
|
|||
|
||||
export interface NeutralizationResult {
|
||||
neutralized_text?: string;
|
||||
neutralized_bytes?: string | Uint8Array;
|
||||
neutralized_file_base64?: string;
|
||||
neutralized_file_name?: string;
|
||||
mime_type?: string;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
* NeutralizationView
|
||||
*
|
||||
* Combined view for the Neutralization feature with two tabs:
|
||||
* - Configuration: Enable/disable neutralization, configure names to parse, SharePoint paths.
|
||||
* - Playground: Manual text neutralization and placeholder resolution.
|
||||
* - Configuration: Configure names to parse.
|
||||
* - Playground: Upload file, paste text, or neutralize SharePoint files. Resolve placeholders.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import { useToast } from '../../../contexts/ToastContext';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { useConnections, type Connection } from '../../../hooks/useConnections';
|
||||
import {
|
||||
getNeutralizationConfig,
|
||||
saveNeutralizationConfig,
|
||||
|
|
@ -20,8 +21,26 @@ import {
|
|||
type NeutralizationConfig,
|
||||
} from '../../../api/neutralizationApi';
|
||||
import { Tabs } from '../../../components/UiComponents/Tabs';
|
||||
import api from '../../../api';
|
||||
import styles from './NeutralizationViews.module.css';
|
||||
|
||||
interface SiteOption {
|
||||
value: string;
|
||||
label: string;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
webUrl: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface FolderOption {
|
||||
value: string;
|
||||
label: string;
|
||||
siteId: string;
|
||||
folderName: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION TAB
|
||||
// =============================================================================
|
||||
|
|
@ -33,13 +52,10 @@ const ConfigTab: React.FC = () => {
|
|||
const [config, setConfig] = useState<NeutralizationConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [namesToParse, setNamesToParse] = useState('');
|
||||
const [sharepointSourcePath, setSharepointSourcePath] = useState('');
|
||||
const [sharepointTargetPath, setSharepointTargetPath] = useState('');
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -49,8 +65,6 @@ const ConfigTab: React.FC = () => {
|
|||
setConfig(data);
|
||||
setEnabled(data.enabled);
|
||||
setNamesToParse(data.namesToParse || '');
|
||||
setSharepointSourcePath(data.sharepointSourcePath || '');
|
||||
setSharepointTargetPath(data.sharepointTargetPath || '');
|
||||
} catch (err: unknown) {
|
||||
const errObj = err as { response?: { data?: { detail?: string | { msg?: string }[] } }; message?: string };
|
||||
const detail = errObj.response?.data?.detail;
|
||||
|
|
@ -85,8 +99,8 @@ const ConfigTab: React.FC = () => {
|
|||
userId: config?.userId || '',
|
||||
enabled,
|
||||
namesToParse,
|
||||
sharepointSourcePath,
|
||||
sharepointTargetPath,
|
||||
sharepointSourcePath: config?.sharepointSourcePath || '',
|
||||
sharepointTargetPath: config?.sharepointTargetPath || '',
|
||||
});
|
||||
showSuccess('Saved', 'Configuration saved successfully.');
|
||||
await loadConfig();
|
||||
|
|
@ -103,34 +117,6 @@ const ConfigTab: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleProcessSharepoint = async () => {
|
||||
if (!sharepointSourcePath.trim() || !sharepointTargetPath.trim()) {
|
||||
showError('Error', 'Both SharePoint source and target paths are required.');
|
||||
return;
|
||||
}
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await processSharepointFiles(sharepointSourcePath, sharepointTargetPath);
|
||||
if (result.success) {
|
||||
showSuccess('Done', result.message || 'SharePoint files processed successfully.');
|
||||
} else {
|
||||
setError(result.message || 'Processing failed');
|
||||
showError('Error', result.message || 'Processing failed');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errObj = err as { response?: { data?: { detail?: string } }; message?: string };
|
||||
const message =
|
||||
(typeof errObj.response?.data?.detail === 'string' ? errObj.response.data.detail : null) ||
|
||||
errObj.message ||
|
||||
'Failed to process SharePoint files';
|
||||
setError(message);
|
||||
showError('Error', message);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const dismissError = () => setError(null);
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -154,18 +140,6 @@ const ConfigTab: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.checkboxRow}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="neutralization-enabled"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="neutralization-enabled">Enable Neutralization:</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="names-to-parse">Names to Parse (one per line):</label>
|
||||
<textarea
|
||||
|
|
@ -178,48 +152,6 @@ const ConfigTab: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="sharepoint-source">SharePoint Source Path:</label>
|
||||
<div className={styles.inputWithButton}>
|
||||
<input
|
||||
id="sharepoint-source"
|
||||
type="text"
|
||||
value={sharepointSourcePath}
|
||||
onChange={(e) => setSharepointSourcePath(e.target.value)}
|
||||
placeholder="Enter SharePoint source path..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => {}}
|
||||
title="Browse (not yet implemented)"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="sharepoint-target">SharePoint Target Path:</label>
|
||||
<div className={styles.inputWithButton}>
|
||||
<input
|
||||
id="sharepoint-target"
|
||||
type="text"
|
||||
value={sharepointTargetPath}
|
||||
onChange={(e) => setSharepointTargetPath(e.target.value)}
|
||||
placeholder="Enter SharePoint target path..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => {}}
|
||||
title="Browse (not yet implemented)"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonRow}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -229,14 +161,6 @@ const ConfigTab: React.FC = () => {
|
|||
>
|
||||
{saving ? 'Saving...' : 'Save Configuration'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleProcessSharepoint}
|
||||
disabled={processing || !sharepointSourcePath.trim() || !sharepointTargetPath.trim()}
|
||||
>
|
||||
{processing ? 'Processing...' : 'Neutralize Sharepoint Files'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -250,11 +174,140 @@ const ConfigTab: React.FC = () => {
|
|||
const PlaygroundTab: React.FC = () => {
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { refetch: refetchFiles, handleFileDownload } = useFileContext();
|
||||
const { connections } = useConnections();
|
||||
|
||||
const msftConnections = connections.filter(
|
||||
(c: Connection) => (c.authority === 'msft' || c.type === 'msft') && c.status === 'active'
|
||||
);
|
||||
const hasMsftConnection = msftConnections.length > 0;
|
||||
const msftConnection = msftConnections[0] ?? null;
|
||||
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [neutralizedText, setNeutralizedText] = useState('');
|
||||
const [neutralizing, setNeutralizing] = useState(false);
|
||||
const [resolving, setResolving] = useState(false);
|
||||
const [sharepointSourcePath, setSharepointSourcePath] = useState('');
|
||||
const [sharepointTargetPath, setSharepointTargetPath] = useState('');
|
||||
const [processingSharepoint, setProcessingSharepoint] = useState(false);
|
||||
|
||||
const [browseTarget, setBrowseTarget] = useState<'source' | 'target' | null>(null);
|
||||
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
|
||||
const [folderOptions, setFolderOptions] = useState<FolderOption[]>([]);
|
||||
const [selectedSite, setSelectedSite] = useState<SiteOption | null>(null);
|
||||
const [browseCurrentPath, setBrowseCurrentPath] = useState('');
|
||||
const [isLoadingSites, setIsLoadingSites] = useState(false);
|
||||
const [isLoadingFolders, setIsLoadingFolders] = useState(false);
|
||||
const [browseError, setBrowseError] = useState<string | null>(null);
|
||||
|
||||
const getConnectionReference = useCallback((conn: Connection): string => {
|
||||
return conn.connectionReference || `connection:${conn.authority || conn.type}:${conn.externalUsername}`;
|
||||
}, []);
|
||||
|
||||
const loadSiteOptions = useCallback(async () => {
|
||||
if (!msftConnection) return;
|
||||
setIsLoadingSites(true);
|
||||
setBrowseError(null);
|
||||
try {
|
||||
const ref = getConnectionReference(msftConnection);
|
||||
const params = new URLSearchParams({ connectionReference: ref });
|
||||
const response = await api.get(`/api/sharepoint/folder-options?${params}`);
|
||||
setSiteOptions(response.data || []);
|
||||
} catch (err: unknown) {
|
||||
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
|
||||
setBrowseError(typeof detail === 'string' ? detail : 'Failed to load SharePoint sites');
|
||||
setSiteOptions([]);
|
||||
} finally {
|
||||
setIsLoadingSites(false);
|
||||
}
|
||||
}, [msftConnection, getConnectionReference]);
|
||||
|
||||
const loadFolderOptions = useCallback(async (siteId: string, path: string = '') => {
|
||||
if (!msftConnection || !siteId) return;
|
||||
setIsLoadingFolders(true);
|
||||
setBrowseError(null);
|
||||
try {
|
||||
const ref = getConnectionReference(msftConnection);
|
||||
const params = new URLSearchParams({ connectionReference: ref, siteId });
|
||||
if (path) params.append('path', path);
|
||||
const response = await api.get(`/api/sharepoint/folder-options?${params}`);
|
||||
setFolderOptions(response.data || []);
|
||||
} catch (err: unknown) {
|
||||
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
|
||||
setBrowseError(typeof detail === 'string' ? detail : 'Failed to load folders');
|
||||
setFolderOptions([]);
|
||||
} finally {
|
||||
setIsLoadingFolders(false);
|
||||
}
|
||||
}, [msftConnection, getConnectionReference]);
|
||||
|
||||
const handleOpenBrowse = useCallback((target: 'source' | 'target') => {
|
||||
setBrowseTarget(target);
|
||||
setSelectedSite(null);
|
||||
setBrowseCurrentPath('');
|
||||
setFolderOptions([]);
|
||||
setBrowseError(null);
|
||||
loadSiteOptions();
|
||||
}, [loadSiteOptions]);
|
||||
|
||||
const handleCloseBrowse = useCallback(() => {
|
||||
setBrowseTarget(null);
|
||||
setSelectedSite(null);
|
||||
setBrowseCurrentPath('');
|
||||
}, []);
|
||||
|
||||
const handleSiteChange = useCallback((siteId: string) => {
|
||||
const site = siteOptions.find((s) => s.siteId === siteId) || null;
|
||||
setSelectedSite(site);
|
||||
setBrowseCurrentPath('');
|
||||
if (site) {
|
||||
loadFolderOptions(site.siteId, '');
|
||||
} else {
|
||||
setFolderOptions([]);
|
||||
}
|
||||
}, [siteOptions, loadFolderOptions]);
|
||||
|
||||
const handleFolderNavigate = useCallback((folder: FolderOption) => {
|
||||
setBrowseCurrentPath(folder.path);
|
||||
loadFolderOptions(selectedSite!.siteId, folder.path);
|
||||
}, [selectedSite, loadFolderOptions]);
|
||||
|
||||
const handleFolderSelect = useCallback((folder: FolderOption) => {
|
||||
if (!selectedSite) return;
|
||||
const fullPath = selectedSite.webUrl + (folder.path ? '/' + folder.path : '');
|
||||
if (browseTarget === 'source') setSharepointSourcePath(fullPath);
|
||||
else setSharepointTargetPath(fullPath);
|
||||
handleCloseBrowse();
|
||||
}, [selectedSite, browseTarget, handleCloseBrowse]);
|
||||
|
||||
const handleSelectCurrentFolder = useCallback(() => {
|
||||
if (!selectedSite) return;
|
||||
const fullPath = selectedSite.webUrl + (browseCurrentPath ? '/' + browseCurrentPath : '');
|
||||
if (browseTarget === 'source') setSharepointSourcePath(fullPath);
|
||||
else setSharepointTargetPath(fullPath);
|
||||
handleCloseBrowse();
|
||||
}, [selectedSite, browseCurrentPath, browseTarget, handleCloseBrowse]);
|
||||
|
||||
const handleGoUp = useCallback(() => {
|
||||
if (!browseCurrentPath || !selectedSite) return;
|
||||
const parts = browseCurrentPath.split('/');
|
||||
parts.pop();
|
||||
const parentPath = parts.join('/');
|
||||
setBrowseCurrentPath(parentPath);
|
||||
loadFolderOptions(selectedSite.siteId, parentPath);
|
||||
}, [browseCurrentPath, selectedSite, loadFolderOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
getNeutralizationConfig()
|
||||
.then((data) => {
|
||||
if (mounted) {
|
||||
setSharepointSourcePath(data.sharepointSourcePath || '');
|
||||
setSharepointTargetPath(data.sharepointTargetPath || '');
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
const [fileResult, setFileResult] = useState<{
|
||||
base64?: string;
|
||||
fileName?: string;
|
||||
|
|
@ -397,17 +450,42 @@ const PlaygroundTab: React.FC = () => {
|
|||
setFileResult(null);
|
||||
};
|
||||
|
||||
const handleProcessSharepoint = async () => {
|
||||
if (!sharepointSourcePath.trim() || !sharepointTargetPath.trim()) {
|
||||
showError('Error', 'Both SharePoint source and target paths are required.');
|
||||
return;
|
||||
}
|
||||
setProcessingSharepoint(true);
|
||||
try {
|
||||
const result = await processSharepointFiles(sharepointSourcePath, sharepointTargetPath);
|
||||
if (result.success) {
|
||||
showSuccess('Done', result.message || 'SharePoint files processed successfully.');
|
||||
} else {
|
||||
showError('Error', result.message || 'Processing failed');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errObj = err as { response?: { data?: { detail?: string } }; message?: string };
|
||||
const message =
|
||||
(typeof errObj.response?.data?.detail === 'string' ? errObj.response.data.detail : null) ||
|
||||
errObj.message ||
|
||||
'Failed to process SharePoint files';
|
||||
showError('Error', message);
|
||||
} finally {
|
||||
setProcessingSharepoint(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.playgroundSection}>
|
||||
<h3 className={styles.sectionTitle}>Manual Text & File Neutralization</h3>
|
||||
<p className={styles.sectionDescription}>
|
||||
Enter text or upload a file (PDF, DOCX, XLSX, PPTX, TXT, CSV, JSON) to neutralize sensitive data.
|
||||
Use "Resolve Text" to convert placeholders back to the original text.
|
||||
Upload a file, paste text, or neutralize SharePoint files. Use "Resolve Text" to convert placeholders back to the original text.
|
||||
</p>
|
||||
|
||||
<div className={styles.playgroundCard}>
|
||||
<div className={styles.formRow}>
|
||||
<label>Upload file to neutralize:</label>
|
||||
<label>1. Upload file to neutralize:</label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
|
@ -418,7 +496,7 @@ const PlaygroundTab: React.FC = () => {
|
|||
<div className={styles.buttonRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
className={styles.primaryButton}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={neutralizing}
|
||||
>
|
||||
|
|
@ -442,7 +520,7 @@ const PlaygroundTab: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="input-text">Or paste text:</label>
|
||||
<label htmlFor="input-text">2. Or paste text:</label>
|
||||
<textarea
|
||||
id="input-text"
|
||||
className={styles.textareaField}
|
||||
|
|
@ -452,7 +530,6 @@ const PlaygroundTab: React.FC = () => {
|
|||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonRow}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -473,17 +550,81 @@ const PlaygroundTab: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="neutralized-text">Neutralized Text:</label>
|
||||
<textarea
|
||||
id="neutralized-text"
|
||||
className={styles.textareaField}
|
||||
value={neutralizedText}
|
||||
onChange={(e) => setNeutralizedText(e.target.value)}
|
||||
placeholder="Neutralized text will appear here (or use Download for binary files)...."
|
||||
rows={8}
|
||||
readOnly={false}
|
||||
<label>3. Or neutralize SharePoint files:</label>
|
||||
<div className={styles.formRow} style={{ gap: '0.75rem', marginTop: 0 }}>
|
||||
<div>
|
||||
<label htmlFor="sharepoint-source" style={{ fontSize: '0.8125rem', fontWeight: 400 }}>Source:</label>
|
||||
<div className={styles.inputWithButton}>
|
||||
<input
|
||||
id="sharepoint-source"
|
||||
type="text"
|
||||
value={sharepointSourcePath}
|
||||
onChange={(e) => setSharepointSourcePath(e.target.value)}
|
||||
placeholder="SharePoint source path..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => handleOpenBrowse('source')}
|
||||
disabled={!hasMsftConnection}
|
||||
title={hasMsftConnection ? 'Browse SharePoint' : 'Add a Microsoft connection first (Basisdaten → Connections)'}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="sharepoint-target" style={{ fontSize: '0.8125rem', fontWeight: 400 }}>Target:</label>
|
||||
<div className={styles.inputWithButton}>
|
||||
<input
|
||||
id="sharepoint-target"
|
||||
type="text"
|
||||
value={sharepointTargetPath}
|
||||
onChange={(e) => setSharepointTargetPath(e.target.value)}
|
||||
placeholder="SharePoint target path..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => handleOpenBrowse('target')}
|
||||
disabled={!hasMsftConnection}
|
||||
title={hasMsftConnection ? 'Browse SharePoint' : 'Add a Microsoft connection first (Basisdaten → Connections)'}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buttonRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleProcessSharepoint}
|
||||
disabled={processingSharepoint || !sharepointSourcePath.trim() || !sharepointTargetPath.trim()}
|
||||
title={
|
||||
!sharepointSourcePath.trim() || !sharepointTargetPath.trim()
|
||||
? 'Enter source and target SharePoint paths to enable'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{processingSharepoint ? 'Processing...' : 'Neutralize SharePoint Files'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{neutralizedText && !fileResult && (
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="neutralized-text">Neutralized Text:</label>
|
||||
<div
|
||||
id="neutralized-text"
|
||||
className={styles.outputBlock}
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
>
|
||||
{neutralizedText}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(neutralizedText || fileResult) && (
|
||||
<button
|
||||
|
|
@ -496,6 +637,98 @@ const PlaygroundTab: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{browseTarget && (
|
||||
<div className={styles.modalOverlay} onClick={handleCloseBrowse}>
|
||||
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className={styles.modalTitle}>
|
||||
Browse SharePoint {browseTarget === 'source' ? 'Source' : 'Target'} Folder
|
||||
</h3>
|
||||
{browseError && (
|
||||
<div className={styles.errorMessage} style={{ marginBottom: '0.75rem' }}>
|
||||
{browseError}
|
||||
</div>
|
||||
)}
|
||||
{isLoadingSites ? (
|
||||
<div className={styles.loading}>Loading sites...</div>
|
||||
) : (
|
||||
<>
|
||||
<label htmlFor="browse-site" style={{ fontSize: '0.875rem', fontWeight: 500 }}>Site:</label>
|
||||
<select
|
||||
id="browse-site"
|
||||
className={styles.folderSelect}
|
||||
value={selectedSite?.siteId || ''}
|
||||
onChange={(e) => handleSiteChange(e.target.value)}
|
||||
>
|
||||
<option value="">Select a site...</option>
|
||||
{siteOptions.map((site) => (
|
||||
<option key={site.siteId} value={site.siteId}>
|
||||
{site.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{selectedSite && (
|
||||
<>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', margin: '0 0 0.5rem 0' }}>
|
||||
Current path: <strong>{selectedSite.path}{browseCurrentPath ? '/' + browseCurrentPath : ''}</strong>
|
||||
</p>
|
||||
{isLoadingFolders ? (
|
||||
<div className={styles.loading}>Loading folders...</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
{browseCurrentPath && (
|
||||
<button type="button" className={styles.secondaryButton} onClick={handleGoUp}>
|
||||
↑ Go Up
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.primaryButton}
|
||||
onClick={handleSelectCurrentFolder}
|
||||
>
|
||||
Select This Folder
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.folderList}>
|
||||
{folderOptions.map((folder) => (
|
||||
<div key={folder.value} className={styles.folderItem}>
|
||||
<span
|
||||
className={styles.folderName}
|
||||
onClick={() => handleFolderNavigate(folder)}
|
||||
>
|
||||
📁 {folder.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.selectButton}
|
||||
onClick={(e) => { e.stopPropagation(); handleFolderSelect(folder); }}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{folderOptions.length === 0 && (
|
||||
<div style={{ padding: '0.75rem', color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
|
||||
No subfolders in this location
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<button type="button" className={styles.secondaryButton} onClick={handleCloseBrowse}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,21 @@
|
|||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
|
||||
/* Read-only output block */
|
||||
.outputBlock {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
font-family: inherit;
|
||||
background: var(--surface-color, #f8f9fa);
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.buttonRow {
|
||||
display: flex;
|
||||
|
|
@ -259,6 +274,89 @@
|
|||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* SharePoint path picker modal */
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
background: var(--bg-primary, #ffffff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.folderSelect {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
background: var(--bg-primary, #ffffff);
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.folderList {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary, #ffffff);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.folderItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folderItem:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.folderItem:hover {
|
||||
background: var(--surface-color, #f8f9fa);
|
||||
}
|
||||
|
||||
.folderName {
|
||||
flex: 1;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.selectButton {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
border: 1px solid var(--border-color, #d0d0d0);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary, #ffffff);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectButton:hover {
|
||||
background: var(--surface-color, #f5f5f5);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: flex;
|
||||
|
|
@ -297,7 +395,8 @@
|
|||
}
|
||||
|
||||
:global(.dark-theme) .inputWithButton input,
|
||||
:global(.dark-theme) .textareaField {
|
||||
:global(.dark-theme) .textareaField,
|
||||
:global(.dark-theme) .outputBlock {
|
||||
background: var(--surface-dark, #2a2a2a);
|
||||
border-color: var(--border-dark, #444);
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
|
|
|
|||
Loading…
Reference in a new issue