import { useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from "react"; import { ArrowUp, ChevronRight, Copy, Download, File, Folder, Home, KeyRound, Link2, MoveRight, Plus, RefreshCw, Search, Trash2, UploadCloud } from "lucide-react"; import { Button, ConfirmDialog, DismissibleAlert, FieldLabel, FileDropZone, FormField, LoadingIndicator, ToggleSwitch, hasScope, type ApiSettings, type AuthInfo, i18nMessage } from "@govoplan/core-webui"; import { bulkDeleteFiles, bulkRenameFiles, browseFileConnectorProfile, createFileConnectorSpace, createFolder, deleteFolder, downloadFile, downloadFilesAsZip, fetchResourceAccessExplanation, listFilesDelta, listFileConnectorProfiles, listFileSpaces, listManagedFileSnapshot, resolveFilePatterns, syncFileConnectorFile, transferFiles, uploadFiles, virtualFolderResourceId, type ConflictResolution, type ConflictStrategy, type FileConnectorBrowseItem, type FileConnectorProfile, type FileDeltaResponse, type FileFolder, type FileSpace, type ManagedFile, type RenameResponse, type ResourceAccessExplanationResponse } from "../../api/files"; import { EMPTY_FILES, EMPTY_FOLDERS, EMPTY_SPACES, INTERNAL_DRAG_TYPE } from "./constants"; import { FileConflictDialog, FileContextMenu, FileDialog, FolderTree, RenamePreviewList, TransferFolderSelector } from "./components/FileManagerComponents"; import type { ContextMenuState, DialogKind, EntrySelectionKey, ExplorerEntry, FolderEntry, FileActionTarget, FileConflictItem, RenameMode, SelectionSets, SortColumn, SortDirection, TransferMode } from "./types"; import { buildExplorerEntries, buildFolderEntryFromSources, buildFolderTree, candidateRenamedPath, entrySelectionKey, fileIdsForSelection, folderAncestorPaths, folderBreadcrumbs, folderContentLabel, formatBytes, formatDate, isFileInFolder, joinFolder, lastPathSegment, normalizeFilePath, normalizeFolder, parseDragState, parentFolderPath, sortExplorerEntries, treeNodeKey } from "./utils/fileManagerUtils"; import { useFileSelection } from "./hooks/useFileSelection"; import { useFileTreeState } from "./hooks/useFileTreeState"; import { useFileDialogs } from "./hooks/useFileDialogs"; import { useFileDragDropState } from "./hooks/useFileDragDropState"; type UploadPhase = "idle" | "uploading" | "unpacking" | "finalizing"; type FileAccessExplanationTarget = { resourceType: "file" | "folder"; resourceId: string; label: string; action: string; }; const DEFAULT_FILE_LIST_ROW_HEIGHT = 58; const FILE_LIST_OVERSCAN_ROWS = 8; export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) { const canDownload = hasScope(auth, "files:download"); const canUpload = hasScope(auth, "files:upload"); const canOrganize = hasScope(auth, "files:organize"); const canDelete = hasScope(auth, "files:delete"); const [spaces, setSpaces] = useState(EMPTY_SPACES); const [activeSpaceId, setActiveSpaceId] = useState(""); const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? spaces[0] ?? null; const activeSpaceIsConnector = activeSpace?.space_type === "connector"; const [filesBySpace, setFilesBySpace] = useState>({}); const [foldersBySpace, setFoldersBySpace] = useState>({}); const [fileDeltaWatermarksBySpace, setFileDeltaWatermarksBySpace] = useState>({}); const files = activeSpace ? filesBySpace[activeSpace.id] ?? EMPTY_FILES : EMPTY_FILES; const folders = activeSpace ? foldersBySpace[activeSpace.id] ?? EMPTY_FOLDERS : EMPTY_FOLDERS; const [currentFolder, setCurrentFolder] = useState(""); const [searchPattern, setSearchPattern] = useState(""); const [searchCaseSensitive, setSearchCaseSensitive] = useState(false); const [searchActive, setSearchActive] = useState(false); const [searchResults, setSearchResults] = useState(null); const [sortColumn, setSortColumn] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [unmatchedCount, setUnmatchedCount] = useState(null); const [unpackZip, setUnpackZip] = useState(false); const [busy, setBusy] = useState(false); const [uploadActive, setUploadActive] = useState(false); const [uploadPhase, setUploadPhase] = useState("idle"); const [uploadProgress, setUploadProgress] = useState(null); const [connectorProfiles, setConnectorProfiles] = useState([]); const [connectorProfileId, setConnectorProfileId] = useState(""); const [connectorLibraryId, setConnectorLibraryId] = useState(null); const [connectorPath, setConnectorPath] = useState(""); const [connectorItems, setConnectorItems] = useState([]); const [connectorSelectedItem, setConnectorSelectedItem] = useState(null); const [connectorLoading, setConnectorLoading] = useState(false); const [connectorError, setConnectorError] = useState(""); const [connectorSpaceLabel, setConnectorSpaceLabel] = useState(""); const [connectorSpaceOwnerSpaceId, setConnectorSpaceOwnerSpaceId] = useState(""); const [connectorSpaceItemsBySpace, setConnectorSpaceItemsBySpace] = useState>({}); const [connectorSpaceLibraryBySpace, setConnectorSpaceLibraryBySpace] = useState>({}); const [connectorSpaceSelectedItem, setConnectorSpaceSelectedItem] = useState(null); const [connectorSpaceLoading, setConnectorSpaceLoading] = useState(false); const [connectorSpaceError, setConnectorSpaceError] = useState(""); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const [accessExplanationTarget, setAccessExplanationTarget] = useState(null); const [resourceAccessExplanation, setResourceAccessExplanation] = useState(null); const [resourceAccessLoading, setResourceAccessLoading] = useState(false); const { setDragActive, internalDrag, setInternalDrag, dropTargetKey, setDropTargetKey, clearDropState: clearDragDropState } = useFileDragDropState(); const { dialog, setDialog, dialogTarget, setDialogTarget, transferMode, setTransferMode, transferDialogState, setTransferDialogState, contextMenu, setContextMenu, conflictDialog, setConflictDialog, deleteDialog, setDeleteDialog, openDialog: openRawDialog, closeDialog: closeRawDialog } = useFileDialogs(); const [newFolderName, setNewFolderName] = useState(""); const [newFolderError, setNewFolderError] = useState(""); const [renamePreviewVisibleCount, setRenamePreviewVisibleCount] = useState(20); const [singleRenameName, setSingleRenameName] = useState(""); const [renameMode, setRenameMode] = useState("prefix"); const [renameFind, setRenameFind] = useState(""); const [renameReplacement, setRenameReplacement] = useState(""); const [renamePrefix, setRenamePrefix] = useState(""); const [renameSuffix, setRenameSuffix] = useState(""); const [renameRecursive, setRenameRecursive] = useState(false); const [renamePreview, setRenamePreview] = useState(null); const visibleFiles = searchActive && searchResults ? searchResults : files; const explorerEntries = useMemo(() => { const entries = buildExplorerEntries(visibleFiles, folders, currentFolder, searchActive); return sortExplorerEntries(entries, sortColumn, sortDirection); }, [visibleFiles, folders, currentFolder, searchActive, sortColumn, sortDirection]); const fileListViewportRef = useRef(null); const fileListMeasureRowRef = useRef(null); const [fileListViewportHeight, setFileListViewportHeight] = useState(0); const [fileListScrollTop, setFileListScrollTop] = useState(0); const [fileListRowHeight, setFileListRowHeight] = useState(DEFAULT_FILE_LIST_ROW_HEIGHT); const virtualExplorerRows = useMemo(() => { const rowHeight = Math.max(1, fileListRowHeight); const entryScrollTop = Math.max(0, fileListScrollTop - rowHeight); const viewportHeight = Math.max(fileListViewportHeight, rowHeight * FILE_LIST_OVERSCAN_ROWS); const startIndex = Math.max(0, Math.floor(entryScrollTop / rowHeight) - FILE_LIST_OVERSCAN_ROWS); const endIndex = Math.min( explorerEntries.length, Math.ceil((entryScrollTop + viewportHeight) / rowHeight) + FILE_LIST_OVERSCAN_ROWS ); return { startIndex, topSpacerHeight: startIndex * rowHeight, bottomSpacerHeight: Math.max(0, (explorerEntries.length - endIndex) * rowHeight), entries: explorerEntries.slice(startIndex, endIndex) }; }, [explorerEntries, fileListRowHeight, fileListScrollTop, fileListViewportHeight]); const visibleEntryKeys = useMemo(() => explorerEntries.map(entrySelectionKey), [explorerEntries]); const { selectedFileIds, setSelectedFileIds, selectedFolderPaths, setSelectedFolderPaths, selectedEntryCount, hasSelection, selectedSummary, currentSelectionKeys, setSelectionAnchor, applySelectionKeys, clearSelection, currentSelectionSets, setsForEntry, isEntrySelected, preventTextSelectionOnShift, handleEntrySelection } = useFileSelection(visibleEntryKeys, busy); const { expandedTreeNodes, setExpandedTreeNodes, cancelTreeDragExpand, scheduleTreeDragExpand, toggleTreeFolder } = useFileTreeState({ activeSpaceId, currentFolder, onOpenFolder: openFolder }); const selectedFiles = useMemo(() => files.filter((file) => selectedFileIds.has(file.id)), [files, selectedFileIds]); const selectedDownloadFileIds = useMemo(() => fileIdsForSelection(files, selectedFileIds, selectedFolderPaths), [files, selectedFileIds, selectedFolderPaths]); const accessExplainableTarget = useMemo( () => accessTargetForSelection(selectedFileIds, selectedFolderPaths, activeSpaceId), [activeSpaceId, filesBySpace, foldersBySpace, selectedFileIds, selectedFolderPaths] ); const canExplainResourceAccess = hasScope(auth, "admin:users:read") || hasScope(auth, "admin:roles:read") || hasScope(auth, "access:membership:read") || hasScope(auth, "access:role:read"); const folderCrumbs = useMemo(() => folderBreadcrumbs(currentFolder), [currentFolder]); const activeDialogTarget = dialogTarget ?? (activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null); const activeDialogSpace = activeDialogTarget ? spaces.find((space) => space.id === activeDialogTarget.spaceId) ?? null : null; const downloadLabel = selectedDownloadFileIds.length > 1 ? "i18n:govoplan-files.download_zip.7bd0e4b0" : "i18n:govoplan-files.download.a479c9c3"; const activeConnectorProfile = connectorProfiles.find((profile) => profile.id === connectorProfileId) ?? null; const activeConnectorSpaceItems = activeSpaceIsConnector && activeSpace ? connectorSpaceItemsBySpace[activeSpace.id] ?? [] : []; useEffect(() => { const viewport = fileListViewportRef.current; if (!viewport) return undefined; const updateViewportHeight = () => { setFileListViewportHeight(viewport.clientHeight); setFileListScrollTop(viewport.scrollTop); }; updateViewportHeight(); if (typeof ResizeObserver === "undefined") return undefined; const observer = new ResizeObserver(updateViewportHeight); observer.observe(viewport); return () => observer.disconnect(); }, []); useEffect(() => { const row = fileListMeasureRowRef.current; if (!row) return undefined; const updateRowHeight = () => { const measuredHeight = row.getBoundingClientRect().height; if (measuredHeight > 0) setFileListRowHeight(Math.round(measuredHeight)); }; updateRowHeight(); if (typeof ResizeObserver === "undefined") return undefined; const observer = new ResizeObserver(updateRowHeight); observer.observe(row); return () => observer.disconnect(); }, [activeSpaceId, currentFolder]); useEffect(() => { const viewport = fileListViewportRef.current; if (!viewport) return; viewport.scrollTop = 0; setFileListScrollTop(0); }, [activeSpaceId, currentFolder, searchActive, searchResults, sortColumn, sortDirection]); async function loadSpaces() { setBusy(true); setError(""); try { const response = await listFileSpaces(settings); setSpaces(response.spaces); setActiveSpaceId((current) => current || response.spaces[0]?.id || ""); await Promise.all(response.spaces.filter((space) => !isConnectorSpace(space)).map((space) => loadSpaceContents(space, { silent: true }))); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function applyManagedSpaceDelta(spaceId: string, response: FileDeltaResponse) { if (response.full) { setFilesBySpace((current) => ({ ...current, [spaceId]: response.files })); setFoldersBySpace((current) => ({ ...current, [spaceId]: response.folders })); } else { const deletedFileIds = new Set(response.deleted.filter((item) => !item.resource_type || item.resource_type === "file").map((item) => item.id)); const deletedFolderIds = new Set(response.deleted.filter((item) => item.resource_type === "folder").map((item) => item.id)); setFilesBySpace((current) => { const byId = new Map((current[spaceId] ?? EMPTY_FILES).map((file) => [file.id, file])); deletedFileIds.forEach((id) => byId.delete(id)); response.files.forEach((file) => byId.set(file.id, file)); return { ...current, [spaceId]: Array.from(byId.values()) }; }); setFoldersBySpace((current) => { const byId = new Map((current[spaceId] ?? EMPTY_FOLDERS).map((folder) => [folder.id, folder])); deletedFolderIds.forEach((id) => byId.delete(id)); response.folders.forEach((folder) => byId.set(folder.id, folder)); return { ...current, [spaceId]: Array.from(byId.values()) }; }); } if (response.watermark) { setFileDeltaWatermarksBySpace((current) => ({ ...current, [spaceId]: response.watermark || "" })); } } async function loadSpaceContents(space = activeSpace, options: {silent?: boolean;} = {}) { if (!space) return; if (isConnectorSpace(space)) { await loadConnectorSpaceContents(space, { silent: options.silent, folderPath: "" }); return; } if (!options.silent) { setBusy(true); setError(""); setMessage(""); } try { let since = fileDeltaWatermarksBySpace[space.id]; let response: FileDeltaResponse | null = null; if (!since) { const snapshot = await listManagedFileSnapshot(settings, { owner_type: space.owner_type, owner_id: space.owner_id }); setFilesBySpace((current) => ({ ...current, [space.id]: snapshot.files })); setFoldersBySpace((current) => ({ ...current, [space.id]: snapshot.folders })); if (snapshot.watermark) { setFileDeltaWatermarksBySpace((current) => ({ ...current, [space.id]: snapshot.watermark || "" })); } } else { do { response = await listFilesDelta(settings, { owner_type: space.owner_type, owner_id: space.owner_id, since: since || undefined }); applyManagedSpaceDelta(space.id, response); since = response.watermark || ""; } while (response.has_more && response.watermark); } setSelectedFileIds(new Set()); setSelectedFolderPaths(new Set()); setUnmatchedCount(null); setRenamePreview(null); if (!options.silent) { setSearchActive(false); setSearchPattern(""); setSearchResults(null); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { if (!options.silent) setBusy(false); } } useEffect(() => { void loadSpaces(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => { if (!contextMenu) return undefined; const close = () => setContextMenu(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") setContextMenu(null); }; window.addEventListener("click", close); window.addEventListener("resize", close); window.addEventListener("scroll", close, true); window.addEventListener("keydown", closeOnEscape); return () => { window.removeEventListener("click", close); window.removeEventListener("resize", close); window.removeEventListener("scroll", close, true); window.removeEventListener("keydown", closeOnEscape); }; }, [contextMenu]); useEffect(() => { setCurrentFolder(""); setSearchPattern(""); setSearchActive(false); setSearchResults(null); setSelectedFileIds(new Set()); setSelectedFolderPaths(new Set()); setConnectorSpaceSelectedItem(null); setConnectorSpaceError(""); const nextSpace = spaces.find((space) => space.id === activeSpaceId); if (nextSpace && isConnectorSpace(nextSpace)) { void loadConnectorSpaceContents(nextSpace, { folderPath: "", silent: true }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeSpaceId, spaces]); function resetTransientState(options: {keepSearch?: boolean;} = {}) { clearSelection(); if (!options.keepSearch) { setSearchActive(false); setSearchPattern(""); setSearchResults(null); } setUnmatchedCount(null); setRenamePreview(null); } function currentActionTarget(): FileActionTarget | null { return activeDialogTarget; } function openDialog(kind: DialogKind, target: FileActionTarget | null = null) { if ((kind === "upload" || kind === "connector-sync") && !canUpload) return; if (kind === "connector-space" && !canOrganize) return; if (kind && ["create-folder", "rename", "single-rename", "transfer"].includes(kind) && !canOrganize) return; if (target && isConnectorSpace(findSpace(target.spaceId)) && kind !== "connector-sync") return; if (kind === "create-folder") { setNewFolderName(""); setNewFolderError(""); } openRawDialog(kind, target); } function closeDialog() { closeRawDialog(); setNewFolderError(""); setUploadActive(false); setUploadPhase("idle"); setUploadProgress(null); setConnectorError(""); setConnectorSelectedItem(null); setConnectorSpaceLabel(""); } function updateActiveDialogFolder(spaceId: string, folderPath: string) { setDialogTarget({ spaceId, folderPath: normalizeFolder(folderPath) }); } function findSpace(spaceId: string): FileSpace | null { return spaces.find((space) => space.id === spaceId) ?? null; } function isConnectorSpace(space: FileSpace | null | undefined): boolean { return space?.space_type === "connector"; } function connectorSpaceLibrary(space: FileSpace): string | null { return connectorSpaceLibraryBySpace[space.id] ?? space.library_id ?? null; } function connectorSpaceRootPath(space: FileSpace): string { return normalizeFolder(space.remote_path || ""); } function connectorSpaceBrowsePath(space: FileSpace, folderPath: string): string { return normalizeFolder(joinFolder(connectorSpaceRootPath(space), folderPath)); } function connectorSpaceRelativePath(space: FileSpace, path: string): string { const root = connectorSpaceRootPath(space); const normalized = normalizeFolder(path); if (!root) return normalized; if (normalized === root) return ""; if (normalized.startsWith(`${root}/`)) return normalized.slice(root.length + 1); return normalized; } function connectorSpaceLibraryForSync(space: FileSpace, item: FileConnectorBrowseItem): string { return connectorSpaceLibrary(space) || connectorMetadataString(item, "library_id") || connectorMetadataString(item, "share") || space.connector_profile_id || "default"; } async function loadConnectorSpaceContents( space: FileSpace, options: {folderPath?: string;libraryId?: string | null;silent?: boolean;} = {}) { if (!space.connector_profile_id) { setConnectorSpaceError("i18n:govoplan-files.connector_profile_is_not_configured_for_this_spa.669f57b5"); return; } const relativeFolder = normalizeFolder(options.folderPath ?? currentFolder); const libraryId = options.libraryId !== undefined ? options.libraryId : connectorSpaceLibrary(space); if (!options.silent) { setBusy(true); setError(""); setMessage(""); } setConnectorSpaceLoading(true); setConnectorSpaceError(""); try { const response = await browseFileConnectorProfile(settings, space.connector_profile_id, { path: connectorSpaceBrowsePath(space, relativeFolder), library_id: libraryId || undefined }); setConnectorSpaceItemsBySpace((current) => ({ ...current, [space.id]: response.items })); setConnectorSpaceLibraryBySpace((current) => ({ ...current, [space.id]: response.library_id ?? libraryId ?? null })); setConnectorSpaceSelectedItem(null); setCurrentFolder(relativeFolder); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setConnectorSpaceError(detail); setConnectorSpaceItemsBySpace((current) => ({ ...current, [space.id]: [] })); } finally { setConnectorSpaceLoading(false); if (!options.silent) setBusy(false); } } function openConnectorSpaceItem(item: FileConnectorBrowseItem) { if (!activeSpace || !isConnectorSpace(activeSpace)) return; if (item.kind === "file") { setConnectorSpaceSelectedItem(item); return; } if (item.kind === "library") { void loadConnectorSpaceContents(activeSpace, { folderPath: "", libraryId: item.external_id || item.path }); return; } void loadConnectorSpaceContents(activeSpace, { folderPath: connectorSpaceRelativePath(activeSpace, item.path), libraryId: connectorSpaceLibrary(activeSpace) }); } function browseConnectorSpaceParent() { if (!activeSpace || !isConnectorSpace(activeSpace)) return; if (currentFolder) { void loadConnectorSpaceContents(activeSpace, { folderPath: parentFolderPath(currentFolder), libraryId: connectorSpaceLibrary(activeSpace) }); return; } if (connectorSpaceLibrary(activeSpace) && connectorSpaceLibrary(activeSpace) !== (activeSpace.library_id ?? null)) { void loadConnectorSpaceContents(activeSpace, { folderPath: "", libraryId: activeSpace.library_id ?? null }); } } async function syncConnectorSpaceSelection(itemOverride?: FileConnectorBrowseItem) { const space = activeSpace; const selected = itemOverride ?? connectorSpaceSelectedItem; if (!canUpload || !space || !isConnectorSpace(space) || !space.connector_profile_id || !selected || selected.kind !== "file") return; const relativePath = connectorSpaceRelativePath(space, selected.path); const targetFolder = parentFolderPath(relativePath); setBusy(true); setError(""); setMessage(""); setConnectorSpaceError(""); try { const response = await syncFileConnectorFile(settings, space.connector_profile_id, { library_id: connectorSpaceLibraryForSync(space, selected), path: selected.path, owner_type: space.owner_type, owner_id: space.owner_id, target_folder: targetFolder, conflict_strategy: "rename", source_revision: connectorRevision(selected), metadata: { ...selected.metadata, connector_space_id: space.connector_space_id, browse_name: selected.name, browse_path: selected.path, browse_modified_at: selected.modified_at, browse_etag: selected.etag, synced_from_space: true } }); const actionLabel = response.action === "created" ? "i18n:govoplan-files.synced_new_file.d5e8243d" : response.action === "updated" ? "i18n:govoplan-files.synced_new_version.83784264" : "i18n:govoplan-files.connector_source_already_matched.7eaf5be1"; setMessage(i18nMessage("i18n:govoplan-files.value_value.36bc3a40", { value0: actionLabel, value1: response.file.display_path })); setConnectorSpaceSelectedItem(null); const ownerSpace = spaces.find((item) => item.space_type !== "connector" && item.owner_type === space.owner_type && item.owner_id === space.owner_id); if (ownerSpace) await loadSpaceContents(ownerSpace, { silent: ownerSpace.id !== activeSpaceId }); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setError(detail); setConnectorSpaceError(detail); } finally { setBusy(false); } } async function handleFilesUpload(fileList: FileList | File[], options: {conflictStrategy?: ConflictStrategy;conflictResolutions?: ConflictResolution[];bypassConflictDialog?: boolean;target?: FileActionTarget;} = {}) { if (!canUpload) return; const target = options.target ?? currentActionTarget(); const targetSpace = target ? findSpace(target.spaceId) : null; if (!target || !targetSpace || isConnectorSpace(targetSpace)) return; const selected = Array.from(fileList); if (selected.length === 0) return; if (!options.bypassConflictDialog && !unpackZip) { const conflicts = uploadConflicts(selected, target); if (conflicts.length > 0) { closeDialog(); setConflictDialog({ operation: "upload", title: i18nMessage("i18n:govoplan-files.value_upload_conflict_s.3a04f117", { value0: conflicts.length }), message: "i18n:govoplan-files.files_with_the_same_visible_name_already_exist_i.1bb487b0", items: conflicts, pendingUpload: { files: selected, target }, review: false }); return; } } const uploadsZipArchive = unpackZip && selected.some((file) => file.name.toLowerCase().endsWith(".zip")); setBusy(true); setUploadActive(true); setUploadPhase("uploading"); setUploadProgress(0); setError(""); setMessage(i18nMessage("i18n:govoplan-files.uploading_value_file_s.715ba963", { value0: selected.length })); try { const response = await uploadFiles(settings, selected, { owner_type: targetSpace.owner_type, owner_id: targetSpace.owner_id, path: target.folderPath, unpack_zip: unpackZip, conflict_strategy: options.conflictStrategy ?? "reject", conflict_resolutions: options.conflictResolutions, onProgress: ({ percentage }) => { setUploadProgress(percentage); if (percentage !== null && percentage >= 100) { setUploadPhase(uploadsZipArchive ? "unpacking" : "finalizing"); setMessage(uploadsZipArchive ? "i18n:govoplan-files.unpacking_zip_upload.35019691" : "i18n:govoplan-files.finalizing_upload.bcce936d"); } } }); setUploadPhase("finalizing"); setUploadProgress(100); setMessage(i18nMessage("i18n:govoplan-files.uploaded_value_file_s_into_value.d0fa052b", { value0: response.files.length, value1: target.folderPath || "i18n:govoplan-files.root.e96857c5" })); closeDialog(); setConflictDialog(null); resetTransientState(); await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setMessage(""); } finally { setBusy(false); setUploadActive(false); setUploadPhase("idle"); setUploadProgress(null); } } async function uploadExternalFilesToTarget(fileList: FileList | File[], target: FileActionTarget) { const previousTarget = dialogTarget; setDialogTarget(target); try { await handleFilesUpload(fileList); } finally { setDialogTarget(previousTarget); } } async function openConnectorSyncDialog(target: FileActionTarget | null = toolbarTarget()) { if (!canUpload || !target) return; openDialog("connector-sync", target); if (connectorProfiles.length === 0) { await loadConnectorProfilesForDialog(); return; } const profileId = connectorProfileId || connectorProfiles[0]?.id || ""; if (profileId && connectorItems.length === 0) await browseConnector(profileId, "", null); } async function openConnectorSpaceDialog() { if (!canOrganize) return; const defaultOwnerSpace = activeSpace && !activeSpaceIsConnector ? activeSpace : spaces.find((space) => !isConnectorSpace(space)) ?? null; setConnectorSpaceOwnerSpaceId(defaultOwnerSpace?.id || ""); setConnectorSpaceLabel(""); openDialog("connector-space", null); if (connectorProfiles.length === 0) { await loadConnectorProfilesForDialog(); return; } const profileId = connectorProfileId || connectorProfiles[0]?.id || ""; if (profileId && connectorItems.length === 0) await browseConnector(profileId, "", null); } async function loadConnectorProfilesForDialog(preferredProfileId = connectorProfileId) { setConnectorLoading(true); setConnectorError(""); try { const response = await listFileConnectorProfiles(settings); const enabledProfiles = response.profiles.filter((profile) => profile.enabled); setConnectorProfiles(enabledProfiles); const nextProfileId = enabledProfiles.some((profile) => profile.id === preferredProfileId) ? preferredProfileId : enabledProfiles[0]?.id || ""; setConnectorProfileId(nextProfileId); setConnectorLibraryId(null); setConnectorPath(""); setConnectorItems([]); setConnectorSelectedItem(null); if (nextProfileId) { await browseConnector(nextProfileId, "", null); } else { setConnectorError("i18n:govoplan-files.no_connector_profiles_are_available.9be3be28"); } } catch (err) { setConnectorError(err instanceof Error ? err.message : String(err)); } finally { setConnectorLoading(false); } } async function browseConnector(profileId: string, path = "", libraryId: string | null = connectorLibraryId) { if (!profileId) { setConnectorPath(""); setConnectorLibraryId(null); setConnectorItems([]); setConnectorSelectedItem(null); return; } setConnectorLoading(true); setConnectorError(""); try { const response = await browseFileConnectorProfile(settings, profileId, { path: normalizeFolder(path), library_id: libraryId || undefined }); setConnectorPath(response.path || ""); setConnectorLibraryId(response.library_id ?? libraryId ?? null); setConnectorItems(response.items); setConnectorSelectedItem(null); } catch (err) { setConnectorError(err instanceof Error ? err.message : String(err)); setConnectorItems([]); setConnectorSelectedItem(null); } finally { setConnectorLoading(false); } } function handleConnectorProfileChange(profileId: string) { setConnectorProfileId(profileId); setConnectorLibraryId(null); setConnectorPath(""); setConnectorItems([]); setConnectorSelectedItem(null); if (profileId) void browseConnector(profileId, "", null); } function openConnectorItem(item: FileConnectorBrowseItem) { if (!connectorProfileId) return; if (item.kind === "file") { setConnectorSelectedItem(item); return; } if (item.kind === "library") { void browseConnector(connectorProfileId, "", item.external_id || item.path); return; } void browseConnector(connectorProfileId, item.path, connectorLibraryId); } function browseConnectorParent() { if (!connectorProfileId) return; if (connectorPath) { void browseConnector(connectorProfileId, parentFolderPath(connectorPath), connectorLibraryId); return; } if (connectorLibraryId) void browseConnector(connectorProfileId, "", null); } function connectorSpaceSuggestedLabel(): string { const location = connectorPath ? lastPathSegment(connectorPath) : connectorLibraryId || ""; return [activeConnectorProfile?.label, location].filter(Boolean).join(" / ") || "i18n:govoplan-files.linked_file_space.1a614c7d"; } async function createConnectorSpaceFromDialog() { const ownerSpace = findSpace(connectorSpaceOwnerSpaceId) ?? spaces.find((space) => !isConnectorSpace(space)) ?? null; if (!canOrganize || !ownerSpace || isConnectorSpace(ownerSpace) || !connectorProfileId) { setConnectorError("i18n:govoplan-files.choose_a_connector_profile_and_owner_space.4c386b00"); return; } const label = connectorSpaceLabel.trim() || connectorSpaceSuggestedLabel(); const remotePath = normalizeFolder(connectorPath); setBusy(true); setError(""); setMessage(""); setConnectorError(""); try { const created = await createFileConnectorSpace(settings, { owner_type: ownerSpace.owner_type, owner_id: ownerSpace.owner_id, label, connector_profile_id: connectorProfileId, library_id: connectorLibraryId || undefined, remote_path: remotePath, sync_mode: "manual", metadata: { created_from_ui: true, source_label: activeConnectorProfile?.label, source_path: remotePath, source_library_id: connectorLibraryId } }); setMessage(i18nMessage("i18n:govoplan-files.added_connector_space_value.a24725b8", { value0: created.label })); closeDialog(); resetTransientState(); await loadSpaces(); setActiveSpaceId(`connector:${created.id}`); setCurrentFolder(""); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setError(detail); setConnectorError(detail); } finally { setBusy(false); } } function connectorMetadataString(item: FileConnectorBrowseItem, key: string): string | null { const value = item.metadata[key]; if (typeof value === "string" && value.trim()) return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return null; } function connectorLibraryForSync(item: FileConnectorBrowseItem): string { return connectorLibraryId || connectorMetadataString(item, "library_id") || connectorMetadataString(item, "share") || activeConnectorProfile?.id || "default"; } function connectorRevision(item: FileConnectorBrowseItem): string | null { return item.etag || item.modified_at || connectorMetadataString(item, "revision"); } async function syncConnectorSelection() { const target = currentActionTarget(); const targetSpace = target ? findSpace(target.spaceId) : null; const selected = connectorSelectedItem; if (!canUpload || !target || !targetSpace || !connectorProfileId || !selected || selected.kind !== "file") { setConnectorError("i18n:govoplan-files.choose_a_connector_file_and_destination_folder.3c6f3ffb"); return; } setBusy(true); setError(""); setMessage(""); setConnectorError(""); try { const response = await syncFileConnectorFile(settings, connectorProfileId, { library_id: connectorLibraryForSync(selected), path: selected.path, owner_type: targetSpace.owner_type, owner_id: targetSpace.owner_id, target_folder: target.folderPath, conflict_strategy: "rename", source_revision: connectorRevision(selected), metadata: { ...selected.metadata, browse_name: selected.name, browse_path: selected.path, browse_modified_at: selected.modified_at, browse_etag: selected.etag, synced_from_ui: true } }); const actionLabel = response.action === "created" ? "i18n:govoplan-files.synced_new_file.d5e8243d" : response.action === "updated" ? "i18n:govoplan-files.synced_new_version.83784264" : "i18n:govoplan-files.connector_source_already_matched.7eaf5be1"; setMessage(i18nMessage("i18n:govoplan-files.value_value.36bc3a40", { value0: actionLabel, value1: response.file.display_path })); closeDialog(); resetTransientState(); await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId }); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setError(detail); setConnectorError(detail); } finally { setBusy(false); } } async function handleCreateFolder() { const target = currentActionTarget(); const targetSpace = target ? findSpace(target.spaceId) : null; const trimmedName = newFolderName.trim(); if (!target || !targetSpace || isConnectorSpace(targetSpace) || !trimmedName) return; const folderPath = joinFolder(target.folderPath, trimmedName); if (folderExistsInTarget(target, folderPath)) { setNewFolderError(`Folder already exists: ${folderPath}.`); return; } if (pathExistsInTarget(target, folderPath, { includeFolders: false })) { setNewFolderError(`A file already exists at ${folderPath}.`); return; } setBusy(true); setError(""); setNewFolderError(""); setMessage(""); try { await createFolder(settings, { owner_type: targetSpace.owner_type, owner_id: targetSpace.owner_id, path: folderPath }); setMessage(i18nMessage("i18n:govoplan-files.created_folder_value.6c3002f9", { value0: folderPath })); closeDialog(); setNewFolderName(""); resetTransientState(); await loadSpaceContents(targetSpace, { silent: targetSpace.id !== activeSpaceId }); setExpandedTreeNodes((current) => { const next = new Set(current); folderAncestorPaths(target.folderPath, { includeSelf: true }).forEach((path) => next.add(treeNodeKey(target.spaceId, path))); return next; }); } catch (err) { setNewFolderError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function runPatternSearch(patternOverride = searchPattern, caseSensitiveOverride = searchCaseSensitive) { const trimmedPattern = patternOverride.trim(); if (!activeSpace || activeSpaceIsConnector || !trimmedPattern) { await clearSearch(); return; } const caseSensitive = caseSensitiveOverride; setBusy(true); setError(""); setMessage(""); try { const response = await resolveFilePatterns(settings, { patterns: [trimmedPattern], owner_type: activeSpace.owner_type, owner_id: activeSpace.owner_id, path_prefix: currentFolder, include_unmatched: true, case_sensitive: caseSensitive }); setSearchResults(response.patterns.flatMap((pattern) => pattern.matches)); setSearchActive(true); setSearchPattern(trimmedPattern); setSearchCaseSensitive(caseSensitive); setUnmatchedCount(response.unmatched.length); setSelectedFileIds(new Set()); setSelectedFolderPaths(new Set()); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function clearSearch() { setSearchPattern(""); setSearchActive(false); setSearchResults(null); setUnmatchedCount(null); setSelectedFileIds(new Set()); setSelectedFolderPaths(new Set()); } function deleteSelected( fileIds: Set = selectedFileIds, folderPaths: Set = selectedFolderPaths, space: FileSpace | null = activeSpace) { const fileCount = fileIds.size; const folderCount = folderPaths.size; if (!space || isConnectorSpace(space) || fileCount === 0 && folderCount === 0) return; const folderText = folderCount ? `${folderCount} folder(s) and their contents` : ""; const fileText = fileCount ? `${fileCount} file(s)` : ""; const what = [folderText, fileText].filter(Boolean).join(" plus "); setDeleteDialog({ fileIds: new Set(fileIds), folderPaths: new Set(folderPaths), spaceId: space.id, title: "i18n:govoplan-files.delete_selected_item_s.4b6a0023", message: i18nMessage("i18n:govoplan-files.delete_value_audit_relevant_blobs_remain_retaine.290af88f", { value0: what }) }); } async function performConfirmedDelete() { const pending = deleteDialog; if (!pending) return; const space = findSpace(pending.spaceId); if (!space) { setDeleteDialog(null); return; } setBusy(true); setError(""); setMessage(""); try { if (pending.fileIds.size > 0) { await bulkDeleteFiles(settings, Array.from(pending.fileIds)); } if (pending.folderPaths.size > 0) { for (const folderPath of pending.folderPaths) { await deleteFolder(settings, { owner_type: space.owner_type, owner_id: space.owner_id, path: folderPath, recursive: true }); } } setDeleteDialog(null); resetTransientState(); await loadSpaceContents(space, { silent: space.id !== activeSpaceId }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function downloadLabelForSets(sets: SelectionSets, spaceId = activeSpaceId): string { const count = fileIdsForSelection(filesBySpace[spaceId] ?? EMPTY_FILES, sets.fileIds, sets.folderPaths).length; return count > 1 ? "i18n:govoplan-files.download_zip.7bd0e4b0" : "i18n:govoplan-files.download.a479c9c3"; } async function downloadSelection( fileIds: Set = selectedFileIds, folderPaths: Set = selectedFolderPaths, sourceFiles: ManagedFile[] = files) { if (!canDownload) return; const idsToDownload = fileIdsForSelection(sourceFiles, fileIds, folderPaths); if (idsToDownload.length === 0) return; setBusy(true); setError(""); try { if (idsToDownload.length === 1) { const file = sourceFiles.find((item) => item.id === idsToDownload[0]); if (file) await downloadFile(settings, file); } else { await downloadFilesAsZip(settings, idsToDownload, "multi-seal-mail-files.zip"); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function openTransferDialog(mode: TransferMode, sourceSpaceId = activeSpaceId, sets: SelectionSets = currentSelectionSets(), defaultTarget?: FileActionTarget) { if (!canOrganize) return; if (!sourceSpaceId || sets.fileIds.size === 0 && sets.folderPaths.size === 0) return; setTransferMode(mode); setTransferDialogState({ fileIds: new Set(sets.fileIds), folderPaths: new Set(sets.folderPaths), sourceSpaceId, targetSpaceId: defaultTarget?.spaceId ?? sourceSpaceId, targetFolder: defaultTarget?.folderPath ?? currentFolder }); setDialog("transfer"); } function invalidDropReason(sourceSpaceId: string, folderPaths: Iterable, target: FileActionTarget): string | null { if (sourceSpaceId !== target.spaceId) return null; const targetFolder = normalizeFolder(target.folderPath); for (const folderPathRaw of folderPaths) { const folderPath = normalizeFolder(folderPathRaw); if (!folderPath) continue; if (targetFolder === folderPath || targetFolder.startsWith(`${folderPath}/`)) { return "i18n:govoplan-files.cannot_move_or_copy_a_folder_into_itself_or_one_.4adbd5ea"; } } return null; } async function runTransfer(mode: TransferMode, sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget, options: {conflictStrategy?: ConflictStrategy;conflictResolutions?: ConflictResolution[];bypassConflictDialog?: boolean;} = {}) { const sourceSpace = findSpace(sourceSpaceId); const targetSpace = findSpace(target.spaceId); if (!sourceSpace || !targetSpace || isConnectorSpace(sourceSpace) || isConnectorSpace(targetSpace)) return; const reason = invalidDropReason(sourceSpaceId, sets.folderPaths, target); if (reason) { setError(reason); return; } if (sets.fileIds.size === 0 && sets.folderPaths.size === 0) return; if (mode === "move" && transferWouldBeNoop(sourceSpaceId, sets, target)) { setInternalDrag(null); setDropTargetKey(""); return; } if (!options.bypassConflictDialog) { const conflicts = transferConflicts(mode, sourceSpaceId, sets, target); if (conflicts.length > 0) { closeDialog(); setConflictDialog({ operation: "transfer", title: i18nMessage("i18n:govoplan-files.value_conflict_s.b3872a48", { value0: conflicts.length }), message: i18nMessage("i18n:govoplan-files.value_this_selection_would_create_duplicate_visi.26ae34d4", { value0: mode === "copy" ? "i18n:govoplan-files.copying.43b7de98" : "i18n:govoplan-files.moving.65cd4dd8" }), items: conflicts, pendingTransfer: { mode, sourceSpaceId, sets: { fileIds: new Set(sets.fileIds), folderPaths: new Set(sets.folderPaths) }, target }, review: false }); setInternalDrag(null); setDropTargetKey(""); return; } } setBusy(true); setError(""); setMessage(""); try { const response = await transferFiles(settings, { operation: mode, file_ids: Array.from(sets.fileIds), folder_paths: Array.from(sets.folderPaths), source_owner_type: sourceSpace.owner_type, source_owner_id: sourceSpace.owner_id, target_owner_type: targetSpace.owner_type, target_owner_id: targetSpace.owner_id, target_folder: target.folderPath, conflict_strategy: options.conflictStrategy ?? "reject", conflict_resolutions: options.conflictResolutions }); setMessage(i18nMessage("i18n:govoplan-files.value_value_file_s_and_value_folder_s.c160e725", { value0: mode === "copy" ? "i18n:govoplan-files.copied.8e3df45a" : "i18n:govoplan-files.moved.0fc28db0", value1: response.files, value2: response.folders })); setConflictDialog(null); closeDialog(); resetTransientState(); const reloadIds = new Set([sourceSpace.id, targetSpace.id]); await Promise.all(Array.from(reloadIds).map((spaceId) => { const space = findSpace(spaceId); return space ? loadSpaceContents(space, { silent: space.id !== activeSpaceId }) : Promise.resolve(); })); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); setInternalDrag(null); setDropTargetKey(""); } } async function handleTransferDialogSubmit() { if (!transferDialogState) return; const state = transferDialogState; const mode = transferMode; closeDialog(); await runTransfer(mode, state.sourceSpaceId, state, { spaceId: state.targetSpaceId, folderPath: state.targetFolder }); } async function runRenamePreview(dryRun: boolean) { if (!activeSpace || activeSpaceIsConnector || selectedEntryCount === 0) return; setBusy(true); setError(""); try { const response = await bulkRenameFiles(settings, { file_ids: Array.from(selectedFileIds), folder_paths: Array.from(selectedFolderPaths), owner_type: activeSpace.owner_type, owner_id: activeSpace.owner_id, mode: renameMode, find: renameFind, replacement: renameReplacement, prefix: renamePrefix, suffix: renameSuffix, recursive: renameRecursive, dry_run: dryRun }); setRenamePreview(response); setRenamePreviewVisibleCount(20); if (dryRun) { setMessage(""); } else { setMessage(i18nMessage("i18n:govoplan-files.renamed_value_item_s.3c4a40fe", { value0: response.items.length })); } if (!dryRun) { closeDialog(); resetTransientState(); await loadSpaceContents(activeSpace); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function openRenameDialog() { if (!canOrganize || activeSpaceIsConnector || selectedEntryCount === 0) return; setRenamePreview(null); if (selectedEntryCount === 1) { const selectedFile = selectedFiles[0]; const selectedFolderPath = Array.from(selectedFolderPaths)[0]; setSingleRenameName(selectedFile ? selectedFile.filename : lastPathSegment(selectedFolderPath)); openDialog("single-rename"); return; } openDialog("rename"); } async function runSingleRename() { if (!canOrganize || !activeSpace || activeSpaceIsConnector || selectedEntryCount !== 1 || !singleRenameName.trim()) return; setBusy(true); setError(""); try { const response = await bulkRenameFiles(settings, { file_ids: Array.from(selectedFileIds), folder_paths: Array.from(selectedFolderPaths), owner_type: activeSpace.owner_type, owner_id: activeSpace.owner_id, mode: "direct", new_name: singleRenameName, dry_run: false }); setMessage(i18nMessage("i18n:govoplan-files.renamed_value_item_s.3c4a40fe", { value0: response.items.length })); closeDialog(); resetTransientState(); await loadSpaceContents(activeSpace); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } function openFolder(spaceId: string, path: string) { const space = findSpace(spaceId); if (spaceId !== activeSpaceId) setActiveSpaceId(spaceId); resetTransientState(); if (space && isConnectorSpace(space)) { void loadConnectorSpaceContents(space, { folderPath: normalizeFolder(path) }); return; } setCurrentFolder(normalizeFolder(path)); } function handleEntryKeyDown(entry: ExplorerEntry, event: ReactKeyboardEvent) { if (event.key === " " || event.key === "Spacebar") { event.preventDefault(); handleEntrySelection(entry, event as unknown as ReactMouseEvent); } if (event.key === "Enter") { event.preventDefault(); if (entry.kind === "folder") activeSpace && openFolder(activeSpace.id, entry.path);else if (canDownload) void downloadFile(settings, entry.file); } } function dragSetsForEntry(entry: ExplorerEntry): SelectionSets { const key = entrySelectionKey(entry); return currentSelectionKeys().has(key) ? currentSelectionSets() : setsForEntry(entry); } function handleInternalDragStart(entry: ExplorerEntry, event: ReactDragEvent) { if (!canOrganize || !activeSpace || activeSpaceIsConnector) {event.preventDefault();return;} const sets = dragSetsForEntry(entry); if (!currentSelectionKeys().has(entrySelectionKey(entry))) { applySelectionKeys(new Set([entrySelectionKey(entry)])); setSelectionAnchor(entrySelectionKey(entry)); } const state = { sourceSpaceId: activeSpace.id, fileIds: Array.from(sets.fileIds), folderPaths: Array.from(sets.folderPaths) }; setInternalDrag(state); event.dataTransfer.effectAllowed = "i18n:govoplan-files.copymove.d0fa5904"; event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state)); event.dataTransfer.setData("text/plain", `${state.fileIds.length + state.folderPaths.length} item(s)`); } function handleTreeFolderDragStart(spaceId: string, path: string, event: ReactDragEvent) { if (!canOrganize || isConnectorSpace(findSpace(spaceId))) {event.preventDefault();return;} const folderPath = normalizeFolder(path); if (!folderPath) return; const state = { sourceSpaceId: spaceId, fileIds: [], folderPaths: [folderPath] }; setInternalDrag(state); event.dataTransfer.effectAllowed = "i18n:govoplan-files.copymove.d0fa5904"; event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state)); event.dataTransfer.setData("text/plain", folderPath); } function isInternalDrag(event: ReactDragEvent): boolean { return event.dataTransfer.types.includes(INTERNAL_DRAG_TYPE) || Boolean(internalDrag); } function dropTargetId(target: FileActionTarget): string { return `${target.spaceId}:${normalizeFolder(target.folderPath)}`; } function handleDropTargetDragOver(event: ReactDragEvent, target: FileActionTarget) { event.preventDefault(); event.stopPropagation(); if (isConnectorSpace(findSpace(target.spaceId))) { event.dataTransfer.dropEffect = "none"; setDropTargetKey(""); return; } if (isInternalDrag(event)) { if (!canOrganize) {event.dataTransfer.dropEffect = "none";setDropTargetKey("");return;} const state = internalDrag; const mode = event.ctrlKey ? "copy" : "move"; const sets = state ? { fileIds: new Set(state.fileIds), folderPaths: new Set(state.folderPaths) } : null; const reason = state ? invalidDropReason(state.sourceSpaceId, state.folderPaths, target) : null; const noop = state && sets && mode === "move" ? transferWouldBeNoop(state.sourceSpaceId, sets, target) : false; event.dataTransfer.dropEffect = reason || noop ? "none" : mode; setDropTargetKey(reason || noop ? "" : dropTargetId(target)); return; } event.dataTransfer.dropEffect = canUpload ? "copy" : "none"; setDropTargetKey(canUpload ? dropTargetId(target) : ""); } async function handleDropOnTarget(event: ReactDragEvent, target: FileActionTarget) { event.preventDefault(); event.stopPropagation(); setDragActive(false); setDropTargetKey(""); if (isConnectorSpace(findSpace(target.spaceId))) return; if (isInternalDrag(event)) { if (!canOrganize) return; const state = internalDrag ?? parseDragState(event.dataTransfer.getData(INTERNAL_DRAG_TYPE)); if (!state) return; const reason = invalidDropReason(state.sourceSpaceId, state.folderPaths, target); if (reason) { setError(reason); return; } await runTransfer(event.ctrlKey ? "copy" : "move", state.sourceSpaceId, { fileIds: new Set(state.fileIds), folderPaths: new Set(state.folderPaths) }, target); return; } if (canUpload && event.dataTransfer.files.length > 0) { await uploadExternalFilesToTarget(event.dataTransfer.files, target); } } function clearDropState() { clearDragDropState(); cancelTreeDragExpand(); } function spaceForContext(menu: ContextMenuState | null): FileSpace | null { const spaceId = menu?.spaceId ?? activeSpaceId; return findSpace(spaceId) ?? activeSpace; } function filesForContext(menu: ContextMenuState | null): ManagedFile[] { const spaceId = menu?.spaceId ?? activeSpaceId; return filesBySpace[spaceId] ?? files; } function folderEntryForPath(spaceId: string, path: string): FolderEntry { const sourceFiles = filesBySpace[spaceId] ?? EMPTY_FILES; const sourceFolders = foldersBySpace[spaceId] ?? EMPTY_FOLDERS; return buildFolderEntryFromSources(sourceFiles, sourceFolders, path); } function selectedSetsForContext(menu: ContextMenuState | null): SelectionSets { if (!menu) return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) }; if (menu.source === "tree") { if (menu.entry?.kind === "folder") return { fileIds: new Set(), folderPaths: new Set([menu.entry.path]) }; if (menu.folderPath !== undefined) { const sourceFiles = filesForContext(menu); const fileIds = new Set(sourceFiles.filter((file) => isFileInFolder(file, menu.folderPath ?? "")).map((file) => file.id)); return { fileIds, folderPaths: new Set() }; } } if (menu.target === "empty" || !menu.entry) { return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) }; } const key = entrySelectionKey(menu.entry); if (currentSelectionKeys().has(key)) { return { fileIds: new Set(selectedFileIds), folderPaths: new Set(selectedFolderPaths) }; } return setsForEntry(menu.entry); } function openContextMenu(event: ReactMouseEvent, target: "empty" | "folder" | "file", entry?: ExplorerEntry) { event.preventDefault(); event.stopPropagation(); if (entry) { const key = entrySelectionKey(entry); const currentKeys = currentSelectionKeys(); if (!currentKeys.has(key)) { applySelectionKeys(new Set([key])); setSelectionAnchor(key); } } setContextMenu({ x: event.clientX, y: event.clientY, target, entry, source: "list", spaceId: activeSpaceId, folderPath: entry?.kind === "folder" ? entry.path : currentFolder }); } function openTreeContextMenu(event: ReactMouseEvent, spaceId: string, path: string) { event.preventDefault(); event.stopPropagation(); const normalizedPath = normalizeFolder(path); const entry = normalizedPath ? folderEntryForPath(spaceId, normalizedPath) : undefined; setContextMenu({ x: event.clientX, y: event.clientY, target: entry ? "folder" : "empty", entry, source: "tree", spaceId, folderPath: normalizedPath }); } function openTreeEmptyContextMenu(event: ReactMouseEvent) { event.preventDefault(); const target = toolbarTarget(); if (!target) return; setContextMenu({ x: event.clientX, y: event.clientY, target: "empty", source: "tree", spaceId: target.spaceId, folderPath: target.folderPath }); } function contextActionTarget(menu: ContextMenuState | null): FileActionTarget | null { const space = spaceForContext(menu); if (!space) return null; const folderPath = menu?.entry?.kind === "folder" ? menu.entry.path : menu?.folderPath ?? currentFolder; return { spaceId: space.id, folderPath: normalizeFolder(folderPath) }; } function openUploadDialogForContext(menu: ContextMenuState | null) { const target = contextActionTarget(menu); setContextMenu(null); openDialog("upload", target); } function openCreateFolderDialogForContext(menu: ContextMenuState | null) { const target = contextActionTarget(menu); setContextMenu(null); openDialog("create-folder", target); } async function downloadContextSelection(menu: ContextMenuState | null) { const { fileIds, folderPaths } = selectedSetsForContext(menu); const sourceFiles = filesForContext(menu); setContextMenu(null); await downloadSelection(fileIds, folderPaths, sourceFiles); } async function deleteContextSelection(menu: ContextMenuState | null) { const { fileIds, folderPaths } = selectedSetsForContext(menu); const space = spaceForContext(menu); setContextMenu(null); await deleteSelected(fileIds, folderPaths, space); } function accessTargetForSelection(fileIds: Set, folderPaths: Set, spaceId: string): FileAccessExplanationTarget | null { if (fileIds.size === 1 && folderPaths.size === 0) { const fileId = Array.from(fileIds)[0]; const file = filesInSpace(spaceId).find((item) => item.id === fileId); return file ? { resourceType: "file", resourceId: file.id, label: file.display_path || file.filename, action: "files:file:read" } : null; } if (fileIds.size === 0 && folderPaths.size === 1) { const folderPath = normalizeFolder(Array.from(folderPaths)[0]); const folder = foldersInSpace(spaceId).find((item) => normalizeFolder(item.path) === folderPath); if (folder) { return { resourceType: "folder", resourceId: folder.id, label: folder.path, action: "files:file:read" }; } const space = findSpace(spaceId); const tenantId = (auth.active_tenant ?? auth.tenant)?.id; if (!space || !tenantId || !folderPath) return null; return { resourceType: "folder", resourceId: virtualFolderResourceId({ tenantId, ownerType: space.owner_type, ownerId: space.owner_id, path: folderPath }), label: folderPath, action: "files:file:read" }; } return null; } function accessTargetForContext(menu: ContextMenuState | null): FileAccessExplanationTarget | null { const { fileIds, folderPaths } = selectedSetsForContext(menu); return accessTargetForSelection(fileIds, folderPaths, menu?.spaceId ?? activeSpaceId); } function openAccessExplanationForContext(menu: ContextMenuState | null) { const target = accessTargetForContext(menu); setContextMenu(null); if (target) void openAccessExplanation(target); } async function openAccessExplanation(target: FileAccessExplanationTarget): Promise { if (!auth.user?.id) return; setAccessExplanationTarget(target); setResourceAccessExplanation(null); setResourceAccessLoading(true); setError(""); try { setResourceAccessExplanation(await fetchResourceAccessExplanation(settings, { userId: auth.user.id, resourceType: target.resourceType, resourceId: target.resourceId, action: target.action, tenantId: (auth.active_tenant ?? auth.tenant)?.id })); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setAccessExplanationTarget(null); } finally { setResourceAccessLoading(false); } } function openTransferDialogForContext(menu: ContextMenuState | null, mode: TransferMode) { const sets = selectedSetsForContext(menu); const space = spaceForContext(menu); const target = contextActionTarget(menu) ?? (activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null); setContextMenu(null); if (space) openTransferDialog(mode, space.id, sets, target ?? undefined); } function toolbarTarget(): FileActionTarget | null { return activeSpace ? { spaceId: activeSpace.id, folderPath: currentFolder } : null; } function parentFolder(path: string): string { const parts = normalizeFilePath(path).split("/").filter(Boolean); return normalizeFolder(parts.slice(0, -1).join("/")); } function filesInSpace(spaceId: string): ManagedFile[] { return filesBySpace[spaceId] ?? EMPTY_FILES; } function foldersInSpace(spaceId: string): FileFolder[] { return foldersBySpace[spaceId] ?? EMPTY_FOLDERS; } function pathExistsInTarget(target: FileActionTarget, path: string, options: {excludeFileId?: string;includeFolders?: boolean;} = {}): boolean { const normalized = normalizeFilePath(path); const targetFiles = filesInSpace(target.spaceId); if (targetFiles.some((file) => file.id !== options.excludeFileId && normalizeFilePath(file.display_path) === normalized)) return true; if (options.includeFolders && folderExistsInTarget(target, normalized)) return true; return false; } function folderExistsInTarget(target: FileActionTarget, path: string): boolean { const normalized = normalizeFolder(path); return foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === normalized); } function uploadConflicts(uploadedFiles: File[], target: FileActionTarget): FileConflictItem[] { const conflicts: FileConflictItem[] = []; const reserved = new Set(); uploadedFiles.forEach((file, index) => { const targetPath = normalizeFilePath(joinFolder(target.folderPath, file.name)); if (pathExistsInTarget(target, targetPath, { includeFolders: true }) || reserved.has(targetPath)) { conflicts.push({ id: `upload:${index}:${targetPath}`, kind: "file", label: file.name, targetPath, action: "rename", newPath: nextClientAvailablePath(target, targetPath, reserved) }); } reserved.add(targetPath); }); return conflicts; } function nextClientAvailablePath(target: FileActionTarget, desiredPath: string, reserved = new Set()): string { const desired = normalizeFilePath(desiredPath); if (!reserved.has(desired) && !pathExistsInTarget(target, desired, { includeFolders: true })) return desired; for (let counter = 1; counter < 10000; counter += 1) { const candidate = candidateRenamedPath(desired, counter); if (!reserved.has(candidate) && !pathExistsInTarget(target, candidate, { includeFolders: true })) return candidate; } return desired; } function transferWouldBeNoop(sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget): boolean { if (sourceSpaceId !== target.spaceId) return false; const targetFolder = normalizeFolder(target.folderPath); const sourceFiles = filesInSpace(sourceSpaceId); const selectedItems = sets.fileIds.size + sets.folderPaths.size; if (selectedItems === 0) return false; for (const fileId of sets.fileIds) { const file = sourceFiles.find((item) => item.id === fileId); if (!file || parentFolder(file.display_path) !== targetFolder) return false; } for (const folderPath of sets.folderPaths) { if (parentFolder(folderPath) !== targetFolder) return false; } return true; } function transferConflicts(mode: TransferMode, sourceSpaceId: string, sets: SelectionSets, target: FileActionTarget): FileConflictItem[] { const conflicts: FileConflictItem[] = []; const sourceFiles = filesInSpace(sourceSpaceId); const reserved = new Set(); const maybeAddConflict = (id: string, kind: "file" | "folder", label: string, targetPath: string, excludeFileId?: string) => { const normalized = kind === "folder" ? normalizeFolder(targetPath) : normalizeFilePath(targetPath); const exists = kind === "folder" ? foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === normalized) : pathExistsInTarget(target, normalized, { excludeFileId: mode === "move" && sourceSpaceId === target.spaceId ? excludeFileId : undefined }); if (exists || reserved.has(normalized)) { conflicts.push({ id, kind, label, targetPath: normalized, action: "rename", newPath: kind === "folder" ? nextClientAvailableFolderPath(target, normalized, reserved) : nextClientAvailablePath(target, normalized, reserved) }); } reserved.add(normalized); }; for (const fileId of sets.fileIds) { const file = sourceFiles.find((item) => item.id === fileId); if (!file) continue; const targetPath = normalizeFilePath(joinFolder(target.folderPath, file.filename)); if (mode === "move" && sourceSpaceId === target.spaceId && normalizeFilePath(file.display_path) === targetPath) continue; maybeAddConflict(`file:${file.id}`, "file", file.filename, targetPath, file.id); } for (const folderPathRaw of sets.folderPaths) { const folderPath = normalizeFolder(folderPathRaw); const folderName = lastPathSegment(folderPath); const targetFolderPath = normalizeFolder(joinFolder(target.folderPath, folderName)); if (mode === "move" && sourceSpaceId === target.spaceId && folderPath === targetFolderPath) continue; maybeAddConflict(`folder:${folderPath}`, "folder", folderName, targetFolderPath); for (const file of sourceFiles.filter((item) => isFileInFolder(item, folderPath))) { const relative = normalizeFilePath(file.display_path).slice(folderPath.length + 1); const targetPath = normalizeFilePath(joinFolder(targetFolderPath, relative)); if (mode === "move" && sourceSpaceId === target.spaceId && normalizeFilePath(file.display_path) === targetPath) continue; maybeAddConflict(`folder-file:${folderPath}:${file.id}`, "file", relative, targetPath, file.id); } } return conflicts; } function nextClientAvailableFolderPath(target: FileActionTarget, desiredPath: string, reserved = new Set()): string { const desired = normalizeFolder(desiredPath); if (!reserved.has(desired) && !foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === desired)) return desired; for (let counter = 1; counter < 10000; counter += 1) { const candidate = candidateRenamedPath(desired, counter); if (!reserved.has(candidate) && !foldersInSpace(target.spaceId).some((folder) => normalizeFolder(folder.path) === candidate)) return candidate; } return desired; } function updateConflictItem(id: string, patch: Partial) { setConflictDialog((current) => current ? { ...current, items: current.items.map((item) => item.id === id ? { ...item, ...patch } : item) } : current); } async function applyConflictDecision(strategy: ConflictStrategy | "review") { const currentConflict = conflictDialog; if (!currentConflict) return; if (strategy === "review" && !currentConflict.review) { setConflictDialog({ ...currentConflict, review: true }); return; } const reviewed = currentConflict.review || strategy === "review"; const conflictStrategy: ConflictStrategy = reviewed ? "reject" : strategy as ConflictStrategy; const conflictResolutions: ConflictResolution[] | undefined = reviewed ? currentConflict.items.map((item) => ({ target_path: item.targetPath, action: item.action, new_path: item.action === "rename" ? item.newPath : undefined })) : undefined; setConflictDialog(null); if (currentConflict.operation === "upload" && currentConflict.pendingUpload) { const pending = currentConflict.pendingUpload; let filesToUpload = pending.files; let uploadResolutions = conflictResolutions; if (reviewed) { const skippedPaths = new Set(currentConflict.items.filter((item) => item.action === "skip").map((item) => item.targetPath)); filesToUpload = pending.files.filter((file) => !skippedPaths.has(normalizeFilePath(joinFolder(pending.target.folderPath, file.name)))); uploadResolutions = conflictResolutions?.filter((item) => item.action !== "skip"); } if (filesToUpload.length === 0) { closeDialog(); setMessage("i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped.1166f3b5"); return; } await handleFilesUpload(filesToUpload, { target: pending.target, conflictStrategy, conflictResolutions: uploadResolutions, bypassConflictDialog: true }); return; } if (currentConflict.operation === "transfer" && currentConflict.pendingTransfer) { const pending = currentConflict.pendingTransfer; await runTransfer(pending.mode, pending.sourceSpaceId, pending.sets, pending.target, { conflictStrategy, conflictResolutions, bypassConflictDialog: true }); } } function toggleSort(column: SortColumn) { if (sortColumn === column) { setSortDirection((current) => current === "asc" ? "desc" : "asc"); return; } setSortColumn(column); setSortDirection("asc"); } function sortLabel(column: SortColumn, label: string): string { if (sortColumn !== column) return label; return `${label} ${sortDirection === "asc" ? "↑" : "↓"}`; } const noticeTone = message.startsWith("i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped") ? "warning" : message.startsWith("i18n:govoplan-files.uploading_value_file_s") || message === "i18n:govoplan-files.unpacking_zip_upload.35019691" || message === "i18n:govoplan-files.finalizing_upload.bcce936d" ? "info" : "success"; const toolbar =
{hasSelection && } {activeSpaceIsConnector && }
; const uploadBusyLabel = uploadPhase === "unpacking" ? "i18n:govoplan-files.unpacking_zip_archive.698095f4" : "i18n:govoplan-files.uploading_files.6536791d"; const uploadProgressLabel = uploadPhase === "unpacking" ? "i18n:govoplan-files.extracting_files_on_the_server.845a3c1a" : uploadProgress !== null && uploadProgress >= 100 ? "i18n:govoplan-files.finalizing_upload.bcce936d" : undefined; const visibleUploadProgress = uploadPhase === "unpacking" ? null : uploadProgress; const connectorLocationLabel = activeConnectorProfile ? [activeConnectorProfile.label, connectorLibraryId, connectorPath || (connectorLibraryId ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") : "i18n:govoplan-files.connector.ba358306"; const connectorParentDisabled = !connectorProfileId || connectorLoading || busy || !connectorPath && !connectorLibraryId; const activeConnectorSpaceLibrary = activeSpace && activeSpaceIsConnector ? connectorSpaceLibrary(activeSpace) : null; const connectorSpaceLocationLabel = activeSpace && activeSpaceIsConnector ? [activeSpace.label, activeConnectorSpaceLibrary, currentFolder || (activeConnectorSpaceLibrary ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") : ""; const connectorSpaceParentDisabled = !activeSpace || !activeSpaceIsConnector || connectorSpaceLoading || busy || !currentFolder && !activeConnectorSpaceLibrary; function renderExplorerEntry(entry: ExplorerEntry, rowIndex: number) { const entryDropTarget = entry.kind === "folder" && activeSpace ? { spaceId: activeSpace.id, folderPath: entry.path } : null; const isDropTarget = entryDropTarget ? dropTargetKey === dropTargetId(entryDropTarget) : false; return entry.kind === "folder" ?
handleInternalDragStart(entry, event)} onDragEnd={() => {setInternalDrag(null);clearDropState();}} onDragOver={(event) => entryDropTarget && handleDropTargetDragOver(event, entryDropTarget)} onDragLeave={clearDropState} onDrop={(event) => entryDropTarget && void handleDropOnTarget(event, entryDropTarget)} onMouseDown={preventTextSelectionOnShift} onClick={(event) => handleEntrySelection(entry, event)} onDoubleClick={() => {if (!busy && activeSpace) openFolder(activeSpace.id, entry.path);}} onContextMenu={(event) => openContextMenu(event, "folder", entry)} onKeyDown={(event) => handleEntryKeyDown(entry, event)}>
{formatBytes(entry.totalSize)} {formatDate(entry.updatedAt)}
:
handleInternalDragStart(entry, event)} onDragEnd={() => {setInternalDrag(null);clearDropState();}} onMouseDown={preventTextSelectionOnShift} onClick={(event) => handleEntrySelection(entry, event)} onDoubleClick={() => {if (!busy) void downloadFile(settings, entry.file);}} onContextMenu={(event) => openContextMenu(event, "file", entry)} onKeyDown={(event) => handleEntryKeyDown(entry, event)}>
{formatBytes(entry.file.size_bytes)} {formatDate(entry.file.updated_at)}
; } function renderConnectorSpaceContent() { if (!activeSpace || !activeSpaceIsConnector) return null; return (
{connectorSpaceLocationLabel}
{connectorSpaceError &&

{connectorSpaceError}

}
{connectorSpaceLoading &&
{i18nMessage("i18n:govoplan-files.loading_connector_space.356d83c6")}
} {!connectorSpaceLoading && activeConnectorSpaceItems.length === 0 &&
i18n:govoplan-files.no_connector_items.d634e023
} {!connectorSpaceLoading && activeConnectorSpaceItems.map((item) => { const isSelected = connectorSpaceSelectedItem?.kind === "file" && item.kind === "file" && connectorSpaceSelectedItem.path === item.path; const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes); const itemModified = item.modified_at ? formatDate(item.modified_at) : ""; const meta = item.kind === "file" ? [itemSize, itemModified].filter(Boolean).join(" · ") : item.kind === "library" ? "i18n:govoplan-files.library.b8100f5b" : "i18n:govoplan-files.folder.30baa249"; return ( ); })}
{connectorSpaceSelectedItem?.kind === "file" ? connectorSpaceSelectedItem.name : "i18n:govoplan-files.no_file_selected.f76f1c1c"} {connectorSpaceSelectedItem?.kind === "file" ? i18nMessage("i18n:govoplan-files.value_value.598aab59", { value0: connectorSpaceSelectedItem.path, value1: parentFolderPath(connectorSpaceRelativePath(activeSpace, connectorSpaceSelectedItem.path)) || "i18n:govoplan-files.root.e96857c5" }) : "i18n:govoplan-files.select_a_remote_file_to_sync_it_into_this_space_.8e58a5ae"}
); } return (
{error && {error} } {message && !error && {message} }
{toolbar} void runPatternSearch(patternValue, caseSensitiveValue)} onClear={() => void clearSearch()} />
{activeSpaceIsConnector ? connectorSpaceSelectedItem?.kind === "file" ? connectorSpaceSelectedItem.name : "i18n:govoplan-files.remote_connector_space.d8956863" : selectedSummary} {activeSpaceIsConnector ? i18nMessage("i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c", { value0: activeConnectorSpaceItems.filter((item) => item.kind === "folder" || item.kind === "library").length, value1: activeConnectorSpaceItems.filter((item) => item.kind === "file").length }) : i18nMessage("i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c", { value0: explorerEntries.filter((entry) => entry.kind === "folder").length, value1: explorerEntries.filter((entry) => entry.kind === "file").length })} {activeSpaceIsConnector && {[activeSpace?.provider?.toUpperCase() || "i18n:govoplan-files.connector.ba358306", activeSpace?.read_only ? "i18n:govoplan-files.read_only.9b19a5a2" : "i18n:govoplan-files.writable.dd35487a", `${activeSpace?.sync_mode || "manual"} sync`].join(" · ")} } {busy && i18n:govoplan-files.working.13b7bfca} {!activeSpaceIsConnector && unmatchedCount !== null && {unmatchedCount} i18n:govoplan-files.visible_file_s_were_not_matched.dc497e90} {!activeSpaceIsConnector && selectedFiles.some((file) => file.audit_relevant) && i18n:govoplan-files.audit_relevant_files_stay_retained_when_deleted_.2b7b4543}
{!activeSpaceIsConnector &&
}
{activeSpaceIsConnector ? renderConnectorSpaceContent() :
setFileListScrollTop(event.currentTarget.scrollTop)} onContextMenu={(event) => openContextMenu(event, "empty")} onClick={(event) => { if (event.target === event.currentTarget) applySelectionKeys(new Set()); }}>
{(() => { const normalizedCurrentFolder = normalizeFolder(currentFolder); const canOpenParent = Boolean(normalizedCurrentFolder && activeSpace); const targetParentFolder = parentFolderPath(normalizedCurrentFolder); return (
{if (canOpenParent) applySelectionKeys(new Set());}} onDoubleClick={() => { if (canOpenParent && activeSpace && !busy) openFolder(activeSpace.id, targetParentFolder); }} onKeyDown={(event) => { if (event.key === "Enter" && canOpenParent && activeSpace && !busy) { event.preventDefault(); openFolder(activeSpace.id, targetParentFolder); } }}>
); })()} {explorerEntries.length === 0 &&
i18n:govoplan-files.this_folder_is_empty_use_upload_or_create_folder.5977d784
} {virtualExplorerRows.topSpacerHeight > 0 && }
{busy &&
{i18nMessage("i18n:govoplan-files.loading_files.bfd27a7e")}
}
{conflictDialog && setConflictDialog(null)} onCancel={() => setConflictDialog(null)} onOverwrite={() => void applyConflictDecision("overwrite")} onRename={() => void applyConflictDecision("rename")} onReview={() => void applyConflictDecision("review")} onApplyReview={() => void applyConflictDecision("review")} onUpdateItem={updateConflictItem} /> } void performConfirmedDelete()} onCancel={() => setDeleteDialog(null)} /> {contextMenu && 0 || selectedSetsForContext(contextMenu).folderPaths.size > 0} canCreateFolder={canOrganize} canUpload={canUpload} canDownload={canDownload} canOrganize={canOrganize} canDelete={canDelete} canExplainAccess={canExplainResourceAccess && Boolean(accessTargetForContext(contextMenu))} downloadLabel={downloadLabelForSets(selectedSetsForContext(contextMenu), contextMenu.spaceId ?? activeSpaceId)} onCreateFolder={() => openCreateFolderDialogForContext(contextMenu)} onUpload={() => openUploadDialogForContext(contextMenu)} onDownload={() => void downloadContextSelection(contextMenu)} onMove={() => openTransferDialogForContext(contextMenu, "move")} onCopy={() => openTransferDialogForContext(contextMenu, "copy")} onExplainAccess={() => openAccessExplanationForContext(contextMenu)} onDelete={() => void deleteContextSelection(contextMenu)} /> } {accessExplanationTarget && {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}}}>
} {dialog === "upload" && handleFilesUpload(files)} />
i18n:govoplan-files.destination_folder.f8ccb63b activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
} {dialog === "connector-sync" &&
i18n:govoplan-files.destination_folder.f8ccb63b activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
{connectorLocationLabel}
{connectorError &&

{connectorError}

}
{connectorLoading &&
{i18nMessage("i18n:govoplan-files.loading_connector_files.02c876e0")}
} {!connectorLoading && connectorItems.length === 0 &&
i18n:govoplan-files.no_connector_items.d634e023
} {!connectorLoading && connectorItems.map((item) => { const isSelected = connectorSelectedItem?.kind === "file" && item.kind === "file" && connectorSelectedItem.path === item.path; const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes); const itemModified = item.modified_at ? formatDate(item.modified_at) : ""; const meta = item.kind === "file" ? [itemSize, itemModified].filter(Boolean).join(" · ") : item.kind === "library" ? "i18n:govoplan-files.library.b8100f5b" : "i18n:govoplan-files.folder.30baa249"; return ( ); })}
{connectorSelectedItem?.kind === "file" ? connectorSelectedItem.name : "i18n:govoplan-files.no_file_selected.f76f1c1c"} {connectorSelectedItem?.kind === "file" ? i18nMessage("i18n:govoplan-files.value_value.598aab59", { value0: connectorSelectedItem.path, value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" }) : "i18n:govoplan-files.select_a_remote_file_to_sync_into_the_destinatio.80477a47"}
} {dialog === "connector-space" &&
setConnectorSpaceLabel(event.target.value)} disabled={busy} placeholder={connectorSpaceSuggestedLabel()} />
{connectorLocationLabel}
{connectorError &&

{connectorError}

}
{connectorLoading &&
{i18nMessage("i18n:govoplan-files.loading_connector_folders.dde26f3e")}
} {!connectorLoading && connectorItems.length === 0 &&
i18n:govoplan-files.no_connector_items.d634e023
} {!connectorLoading && connectorItems.map((item) => { const itemSize = item.size_bytes === null || item.size_bytes === undefined ? "" : formatBytes(item.size_bytes); const itemModified = item.modified_at ? formatDate(item.modified_at) : ""; const meta = item.kind === "file" ? [itemSize, itemModified].filter(Boolean).join(" · ") : item.kind === "library" ? "i18n:govoplan-files.library.b8100f5b" : "i18n:govoplan-files.folder.30baa249"; return ( ); })}
{connectorSpaceLabel.trim() || connectorSpaceSuggestedLabel()} {[activeConnectorProfile?.label, connectorLibraryId, connectorPath || (connectorLibraryId ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") || "i18n:govoplan-files.connector_root.9027235c"}
} {dialog === "create-folder" && {setNewFolderName(event.target.value);setNewFolderError("");}} onKeyDown={(event) => {if (event.key === "Enter") void handleCreateFolder();}} /> i18n:govoplan-files.created_inside.3426a815 {activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5"}. {newFolderError && {newFolderError}}
i18n:govoplan-files.parent_folder.d8ed4c2a activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
} {dialog === "transfer" && transferDialogState &&

i18n:govoplan-files.choose_a_destination_space_and_folder_folders_ar.45158643

i18n:govoplan-files.destination_folder.f8ccb63b setTransferDialogState((current) => current ? { ...current, targetFolder: folderPath } : current)} />
} {dialog === "single-rename" && setSingleRenameName(event.target.value)} onKeyDown={(event) => {if (event.key === "Enter") void runSingleRename();}} />
} {dialog === "rename" &&

i18n:govoplan-files.bulk_rename_changes_managed_display_paths_only_i.8cf34a6b

{renameMode === "prefix" && setRenamePrefix(event.target.value)} />} {renameMode === "suffix" && setRenameSuffix(event.target.value)} />} {renameMode === "replace" && <> setRenameFind(event.target.value)} /> setRenameReplacement(event.target.value)} />}
{selectedFolderPaths.size > 0 && }
{renamePreview && setRenamePreviewVisibleCount((current) => current + 20)} onShowFewer={() => setRenamePreviewVisibleCount(20)} /> }
}
); } function ResourceAccessExplanationContent({ loading, explanation, fallbackResourceLabel }: {loading: boolean;explanation: ResourceAccessExplanationResponse | null;fallbackResourceLabel: string;}) { if (loading) return

i18n:govoplan-files.loading_access_explanation.04a7c934

; if (!explanation) return null; const userLabel = explanation.user.display_name || explanation.user.email || explanation.user.id; const resourceLabel = explanation.provenance.find((item) => item.kind === "resource")?.label || fallbackResourceLabel || explanation.resource_id; return ( <>
i18n:govoplan-files.user.9f8a2389

{userLabel}

i18n:govoplan-files.resource.d1c626a9

{resourceLabel}

i18n:govoplan-files.action.97c89a4d

{explanation.action}

i18n:govoplan-files.evidence.8487d192

{explanation.provenance.length}

{explanation.provenance.length === 0 ?

i18n:govoplan-files.no_access_evidence_was_returned.84a21e4e

:
{explanation.provenance.map((item, index) =>
{provenanceKindLabel(item.kind)}
{item.source || "i18n:govoplan-files.no_source.6dcf9723"} {item.id && <> · {item.id}}
{item.label &&

{item.label}

} {Object.keys(item.details ?? {}).length > 0 &&

{formatProvenanceDetails(item.details ?? {})}

}
)}
} ); } function provenanceKindLabel(kind: string): string { switch (kind) { case "resource": return "i18n:govoplan-files.resource.d1c626a9"; case "owner": return "i18n:govoplan-files.owner.89ff3122"; case "share": return "i18n:govoplan-files.share.09ca55ca"; case "policy": return "i18n:govoplan-files.policy.0b779a05"; case "role": return "i18n:govoplan-files.role.b5b4a5a2"; case "right": return "i18n:govoplan-files.permission.2f81a22d"; default: return kind; } } function formatProvenanceDetails(details: Record): string { return Object.entries(details).map(([key, value]) => `${key}: ${formatProvenanceValue(value)}`).join("; "); } function formatProvenanceValue(value: unknown): string { if (value === null || value === undefined) return "i18n:govoplan-files.none.6eef6648"; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value); return JSON.stringify(value); } function FileSearchRow({ value, caseSensitive, busy, searchActive, disabled, onSearch, onClear }: {value: string;caseSensitive: boolean;busy: boolean;searchActive: boolean;disabled: boolean;onSearch: (value: string, caseSensitive: boolean) => void;onClear: () => void;}) { const [draft, setDraft] = useState(value); const [caseSensitiveDraft, setCaseSensitiveDraft] = useState(caseSensitive); useEffect(() => { setDraft(value); }, [value]); useEffect(() => { setCaseSensitiveDraft(caseSensitive); }, [caseSensitive]); function clear() { setDraft(""); onClear(); } return (
); } function activeCampaignShares(file: ManagedFile) { return (file.shares ?? []).filter((share) => share.target_type === "campaign" && !share.revoked_at); } function activeCampaignShareCount(file: ManagedFile): number { return activeCampaignShares(file).length; } function campaignShareLabel(file: ManagedFile): string { const count = activeCampaignShareCount(file); if (count === 0) return "i18n:govoplan-files.no_campaign_links.4203c2be"; if (count === 1) return "i18n:govoplan-files.linked_to_1_campaign.5349694f"; return i18nMessage("i18n:govoplan-files.linked_to_value_campaigns.1ad7be94", { value0: count }); } function campaignShareTitle(file: ManagedFile): string | undefined { const shares = activeCampaignShares(file); if (shares.length === 0) return undefined; return shares.map((share) => share.target_id).join(", "); }