Release govoplan-files v0.1.26: speed archive workflows and unify file tools
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
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, ListFilter, MoveRight, Plus, RefreshCw, Search, Share2, Trash2, UploadCloud } from "lucide-react";
|
||||
import { Archive, ArrowUp, ChevronRight, Copy, Download, File, Folder, Home, KeyRound, Link2, ListFilter, MoveRight, Plus, RefreshCw, Search, Settings2, Share2, Trash2, UploadCloud } from "lucide-react";
|
||||
import { FormGrid, ActionToolbar,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
ContentGrid,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
FieldLabel,
|
||||
FileDropZone,
|
||||
FormField,
|
||||
FormSection,
|
||||
LoadingFrame,
|
||||
LoadingIndicator,
|
||||
PasswordField,
|
||||
ResourceAccessExplanation,
|
||||
ToggleSwitch,
|
||||
WorkspaceActionBar,
|
||||
WorkspaceFrame,
|
||||
hasScope,
|
||||
usePlatformLanguage,
|
||||
type ApiSettings,
|
||||
@@ -25,6 +31,7 @@ import {
|
||||
createFileConnectorSpace,
|
||||
createFolder,
|
||||
confirmArchiveUpload,
|
||||
confirmManagedArchive,
|
||||
deleteFolder,
|
||||
deleteFileConnectorSpace,
|
||||
downloadFile,
|
||||
@@ -37,6 +44,8 @@ import {
|
||||
listFileSpaces,
|
||||
listManagedFileSnapshot,
|
||||
previewArchiveUpload,
|
||||
previewManagedArchive,
|
||||
releaseArchivePreview,
|
||||
resolveFilePatterns,
|
||||
syncFileConnectorSpaceFolder,
|
||||
syncFileConnectorFile,
|
||||
@@ -45,6 +54,7 @@ import {
|
||||
virtualFolderResourceId,
|
||||
type ArchivePreviewEntry,
|
||||
type ArchivePreviewResponse,
|
||||
type ArchiveOperationProgress,
|
||||
type ConflictResolution,
|
||||
type ConflictStrategy,
|
||||
type FileConnectorBrowseItem,
|
||||
@@ -103,7 +113,7 @@ import { useFileTreeState } from "./hooks/useFileTreeState";
|
||||
import { useFileDialogs } from "./hooks/useFileDialogs";
|
||||
import { useFileDragDropState } from "./hooks/useFileDragDropState";
|
||||
|
||||
type UploadPhase = "idle" | "uploading" | "unpacking" | "finalizing";
|
||||
type UploadPhase = "idle" | "uploading" | "inspecting" | "unpacking" | "finalizing";
|
||||
type AuditRelevantFilter = "" | "true" | "false";
|
||||
type FileAccessExplanationTarget = {
|
||||
resourceType: "file" | "folder";
|
||||
@@ -155,13 +165,26 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const [unmatchedCount, setUnmatchedCount] = useState<number | null>(null);
|
||||
const [unpackZip, setUnpackZip] = useState(false);
|
||||
const [archiveFile, setArchiveFile] = useState<File | null>(null);
|
||||
const [managedArchiveFile, setManagedArchiveFile] = useState<ManagedFile | null>(null);
|
||||
const [archivePreview, setArchivePreview] = useState<ArchivePreviewResponse | null>(null);
|
||||
const [archivePassword, setArchivePassword] = useState("");
|
||||
const [selectedArchivePaths, setSelectedArchivePaths] = useState<Set<string>>(new Set());
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [operationBusy, setBusy] = useState(false);
|
||||
const [reloadingView, setReloadingView] = useState(false);
|
||||
const busy = operationBusy || reloadingView;
|
||||
const [viewReloadFailed, setViewReloadFailed] = useState(false);
|
||||
const reloadSequenceRef = useRef(0);
|
||||
const reloadContextRef = useRef("");
|
||||
reloadContextRef.current = JSON.stringify([
|
||||
settings.apiBaseUrl, settings.apiKey, settings.accessToken, auth.tenant?.id, auth.user.id,
|
||||
activeSpaceId, currentFolder, searchActive, searchPattern, searchCaseSensitive,
|
||||
propertyFiltersActive, campaignUsageFilter, auditRelevantFilter
|
||||
]);
|
||||
const [toolsPanel, setToolsPanel] = useState<"connections" | "selection" | null>(null);
|
||||
const [uploadActive, setUploadActive] = useState(false);
|
||||
const [uploadPhase, setUploadPhase] = useState<UploadPhase>("idle");
|
||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||
const [archiveProgress, setArchiveProgress] = useState<ArchiveOperationProgress | null>(null);
|
||||
const [connectorProfiles, setConnectorProfiles] = useState<FileConnectorProfile[]>([]);
|
||||
const [connectorProfileId, setConnectorProfileId] = useState("");
|
||||
const [connectorLibraryId, setConnectorLibraryId] = useState<string | null>(null);
|
||||
@@ -282,6 +305,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
onOpenFolder: openFolder
|
||||
});
|
||||
const selectedFiles = useMemo(() => files.filter((file) => selectedFileIds.has(file.id)), [files, selectedFileIds]);
|
||||
const selectedArchive = selectedFiles.length === 1 && selectedFolderPaths.size === 0 && ARCHIVE_FILENAME_PATTERN.test(selectedFiles[0].filename) ? selectedFiles[0] : null;
|
||||
const shareManageableFile = useMemo(() => {
|
||||
if (!canShare || selectedFiles.length !== 1 || selectedFolderPaths.size > 0) return null;
|
||||
const file = selectedFiles[0];
|
||||
@@ -366,6 +390,81 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadCurrentView() {
|
||||
if (busy || reloadingView || connectorSpaceLoading) return;
|
||||
const sequence = ++reloadSequenceRef.current;
|
||||
const context = reloadContextRef.current;
|
||||
const isCurrent = () => sequence === reloadSequenceRef.current && context === reloadContextRef.current;
|
||||
const readOptions = { cache: "no-store" } as const;
|
||||
setReloadingView(true);
|
||||
setViewReloadFailed(false);
|
||||
setError("");
|
||||
if (activeSpaceIsConnector) setConnectorSpaceError("");
|
||||
try {
|
||||
if (!activeSpace) {
|
||||
const response = await listFileSpaces(settings, readOptions);
|
||||
if (!isCurrent()) return;
|
||||
setSpaces(response.spaces);
|
||||
setSpacesLoaded(true);
|
||||
setActiveSpaceId(response.spaces[0]?.id || "");
|
||||
return;
|
||||
}
|
||||
if (activeSpaceIsConnector) {
|
||||
if (!activeSpace.connector_profile_id) throw new Error(translateText("i18n:govoplan-files.connector_profile_is_not_configured_for_this_spa.669f57b5"));
|
||||
const libraryId = connectorSpaceLibrary(activeSpace);
|
||||
const response = await browseFileConnectorProfile(settings, activeSpace.connector_profile_id, {
|
||||
path: connectorSpaceBrowsePath(activeSpace, currentFolder), library_id: libraryId || undefined
|
||||
}, readOptions);
|
||||
if (!isCurrent()) return;
|
||||
setConnectorSpaceItemsBySpace((current) => ({ ...current, [activeSpace.id]: response.items }));
|
||||
setConnectorSpaceLibraryBySpace((current) => ({ ...current, [activeSpace.id]: response.library_id ?? libraryId ?? null }));
|
||||
setConnectorSpaceSelectedItem((current) => current && response.items.find((item) => item.path === current.path && item.kind === current.kind) || null);
|
||||
return;
|
||||
}
|
||||
// Re-read the current projection, including active filters. None of these
|
||||
// calls imports, synchronizes, uploads, or changes a managed resource.
|
||||
// Apply only after all reads succeed so a failed reload keeps usable data.
|
||||
const owner = { owner_type: activeSpace.owner_type, owner_id: activeSpace.owner_id };
|
||||
const [snapshot, matches, properties] = await Promise.all([
|
||||
listManagedFileSnapshot(settings, owner, readOptions),
|
||||
searchActive ? resolveFilePatterns(settings, {
|
||||
...owner, patterns: [searchPattern], path_prefix: currentFolder,
|
||||
include_unmatched: true, case_sensitive: searchCaseSensitive
|
||||
}) : Promise.resolve(null),
|
||||
propertyFiltersActive ? listFilesByProperties(settings, {
|
||||
...owner, path_prefix: currentFolder, campaign_usage: campaignUsageFilter || undefined,
|
||||
audit_relevant: auditRelevantFilter ? auditRelevantFilter === "true" : undefined
|
||||
}, readOptions) : Promise.resolve(null)
|
||||
]);
|
||||
if (!isCurrent()) return;
|
||||
setFilesBySpace((current) => ({ ...current, [activeSpace.id]: snapshot.files }));
|
||||
setFoldersBySpace((current) => ({ ...current, [activeSpace.id]: snapshot.folders }));
|
||||
setFileDeltaWatermarksBySpace((current) => ({ ...current, [activeSpace.id]: snapshot.watermark || "" }));
|
||||
const fileIds = new Set(snapshot.files.map((file) => file.id));
|
||||
const folderPaths = new Set(snapshot.folders.map((folder) => folder.path));
|
||||
setSelectedFileIds((current) => new Set(Array.from(current).filter((id) => fileIds.has(id))));
|
||||
setSelectedFolderPaths((current) => new Set(Array.from(current).filter((path) =>
|
||||
folderPaths.has(path) || snapshot.files.some((file) => file.display_path.startsWith(`${path}/`))
|
||||
)));
|
||||
if (matches) {
|
||||
setSearchResults(matches.patterns.flatMap((pattern) => pattern.matches));
|
||||
setUnmatchedCount(matches.unmatched.length);
|
||||
}
|
||||
if (properties) {
|
||||
setPropertyFilterResults(properties.files);
|
||||
setPropertyFilterTotal(properties.total);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isCurrent()) return;
|
||||
setViewReloadFailed(true);
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
if (activeSpaceIsConnector) setConnectorSpaceError(detail);
|
||||
else setError(detail);
|
||||
} finally {
|
||||
if (sequence === reloadSequenceRef.current) setReloadingView(false);
|
||||
}
|
||||
}
|
||||
|
||||
function applyManagedSpaceDelta(spaceId: string, response: FileDeltaResponse) {
|
||||
if (response.full) {
|
||||
setFilesBySpace((current) => ({ ...current, [spaceId]: response.files }));
|
||||
@@ -450,6 +549,14 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// A late manual reload must not publish a previous tenant/account/folder's
|
||||
// projection, or release a newer operation's busy state.
|
||||
++reloadSequenceRef.current;
|
||||
setReloadingView(false);
|
||||
setViewReloadFailed(false);
|
||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, auth.tenant?.id, auth.user.id, activeSpaceId, currentFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSpaces();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -578,7 +685,10 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
|
||||
function resetArchiveUploadState() {
|
||||
void releaseArchivePreview(settings, archiveFile);
|
||||
setArchiveProgress(null);
|
||||
setArchiveFile(null);
|
||||
setManagedArchiveFile(null);
|
||||
setArchivePreview(null);
|
||||
setArchivePassword("");
|
||||
setSelectedArchivePaths(new Set());
|
||||
@@ -590,6 +700,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
setUploadActive(false);
|
||||
setUploadPhase("idle");
|
||||
setUploadProgress(null);
|
||||
setArchiveProgress(null);
|
||||
setConnectorError("");
|
||||
setConnectorSelectedItem(null);
|
||||
setConnectorSpaceLabel("");
|
||||
@@ -600,6 +711,16 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
|
||||
function updateActiveDialogFolder(spaceId: string, folderPath: string) {
|
||||
setDialogTarget({ spaceId, folderPath: normalizeFolder(folderPath) });
|
||||
setArchivePreview(null);
|
||||
setSelectedArchivePaths(new Set());
|
||||
}
|
||||
|
||||
function openManagedArchive(file: ManagedFile, target: FileActionTarget | null) {
|
||||
if (busy || !canUpload || !canDownload || !target || isConnectorSpace(findSpace(target.spaceId)) || !ARCHIVE_FILENAME_PATTERN.test(file.filename)) return;
|
||||
setContextMenu(null);
|
||||
openDialog("upload", target);
|
||||
setManagedArchiveFile(file);
|
||||
setUnpackZip(true);
|
||||
}
|
||||
|
||||
function findSpace(spaceId: string): FileSpace | null {
|
||||
@@ -801,29 +922,43 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
|
||||
|
||||
async function loadArchivePreview(
|
||||
file: File,
|
||||
file: File | ManagedFile,
|
||||
target: FileActionTarget,
|
||||
options: { preserveSelection?: boolean } = {}
|
||||
) {
|
||||
if (busy || uploadActive) return;
|
||||
const targetSpace = findSpace(target.spaceId);
|
||||
if (!targetSpace || isConnectorSpace(targetSpace)) {
|
||||
setError(uploadRejectedReason(target));
|
||||
return;
|
||||
}
|
||||
setArchiveFile(file);
|
||||
const managed = "version_id" in file;
|
||||
if (archiveFile && archiveFile !== file) void releaseArchivePreview(settings, archiveFile);
|
||||
setArchiveFile(managed ? null : file);
|
||||
setManagedArchiveFile(managed ? file : null);
|
||||
setBusy(true);
|
||||
setUploadActive(true);
|
||||
setUploadPhase("uploading");
|
||||
setUploadPhase(managed ? "inspecting" : "uploading");
|
||||
setUploadProgress(null);
|
||||
setArchiveProgress(null);
|
||||
setError("");
|
||||
setMessage("");
|
||||
try {
|
||||
const response = await previewArchiveUpload(settings, file, {
|
||||
const previewOptions = {
|
||||
owner_type: targetSpace.owner_type,
|
||||
owner_id: targetSpace.owner_id,
|
||||
path: target.folderPath,
|
||||
password: archivePassword || undefined
|
||||
});
|
||||
};
|
||||
const response = managed
|
||||
? await previewManagedArchive(settings, file.id, { ...previewOptions, source_version_id: file.version_id })
|
||||
: await previewArchiveUpload(settings, file, {
|
||||
...previewOptions,
|
||||
onProgress: ({ percentage }) => {
|
||||
setUploadProgress(percentage);
|
||||
if (percentage !== null && percentage >= 100) setUploadPhase("inspecting");
|
||||
}
|
||||
});
|
||||
const availableFiles = new Set(
|
||||
response.entries
|
||||
.filter((entry) => entry.kind === "file")
|
||||
@@ -889,7 +1024,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const targetSpace = target ? findSpace(target.spaceId) : null;
|
||||
if (
|
||||
busy
|
||||
|| !archiveFile
|
||||
|| (!archiveFile && !managedArchiveFile)
|
||||
|| !archivePreview
|
||||
|| !target
|
||||
|| !targetSpace
|
||||
@@ -907,25 +1042,35 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
setBusy(true);
|
||||
setUploadActive(true);
|
||||
setUploadPhase("uploading");
|
||||
setUploadProgress(0);
|
||||
setUploadPhase("inspecting");
|
||||
setUploadProgress(null);
|
||||
setArchiveProgress(null);
|
||||
setError("");
|
||||
setMessage("Uploading the archive for confirmed extraction.");
|
||||
setMessage("i18n:govoplan-files.archive_progress.inspecting");
|
||||
try {
|
||||
const response = await confirmArchiveUpload(settings, archiveFile, {
|
||||
const confirmOptions = {
|
||||
preview_token: archivePreview.preview_token,
|
||||
selected_paths: Array.from(selectedArchivePaths),
|
||||
owner_type: targetSpace.owner_type,
|
||||
owner_id: targetSpace.owner_id,
|
||||
path: target.folderPath,
|
||||
password: archivePassword || undefined,
|
||||
onArchiveProgress: (progress: ArchiveOperationProgress) => {
|
||||
setArchiveProgress(progress);
|
||||
setUploadPhase(progress.phase === "inspecting" ? "inspecting" : progress.phase === "finalizing" || progress.phase === "complete" ? "finalizing" : "unpacking");
|
||||
}
|
||||
};
|
||||
const response = managedArchiveFile
|
||||
? await confirmManagedArchive(settings, managedArchiveFile.id, { ...confirmOptions, source_version_id: managedArchiveFile.version_id })
|
||||
: await confirmArchiveUpload(settings, archiveFile!, {
|
||||
...confirmOptions,
|
||||
conflict_strategy: "reject",
|
||||
onProgress: ({ percentage }) => {
|
||||
setUploadProgress(percentage);
|
||||
if (percentage !== null && percentage >= 100) {
|
||||
setUploadPhase("unpacking");
|
||||
setMessage("Extracting the selected archive files.");
|
||||
}
|
||||
} else setUploadPhase("uploading");
|
||||
}
|
||||
});
|
||||
setUploadPhase("finalizing");
|
||||
@@ -986,6 +1131,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
setUploadActive(true);
|
||||
setUploadPhase("uploading");
|
||||
setUploadProgress(0);
|
||||
setArchiveProgress(null);
|
||||
setError("");
|
||||
setMessage(i18nMessage("i18n:govoplan-files.uploading_value_file_s.715ba963", { value0: selected.length }));
|
||||
try {
|
||||
@@ -1708,7 +1854,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
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.effectAllowed = "copyMove";
|
||||
event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state));
|
||||
event.dataTransfer.setData("text/plain", `${state.fileIds.length + state.folderPaths.length} item(s)`);
|
||||
}
|
||||
@@ -1719,7 +1865,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
if (!folderPath) return;
|
||||
const state = { sourceSpaceId: spaceId, fileIds: [], folderPaths: [folderPath] };
|
||||
setInternalDrag(state);
|
||||
event.dataTransfer.effectAllowed = "i18n:govoplan-files.copymove.d0fa5904";
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
event.dataTransfer.setData(INTERNAL_DRAG_TYPE, JSON.stringify(state));
|
||||
event.dataTransfer.setData("text/plain", folderPath);
|
||||
}
|
||||
@@ -2240,7 +2386,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
return `${label} ${sortDirection === "asc" ? "↑" : "↓"}`;
|
||||
}
|
||||
|
||||
const noticeTone = message.startsWith("i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped") ? "warning" :
|
||||
const noticeTone = uploadActive ? "info" : 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";
|
||||
@@ -2249,6 +2395,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
activeSpaceIsConnector ? "This action writes managed storage and is unavailable in a read-only connector space." : "";
|
||||
const workingBlocker = busy ? "Wait for the current file operation to finish." : "";
|
||||
const uploadBlocker = workingBlocker || managedSpaceBlocker || (!canUpload ? "File upload permission is required." : "");
|
||||
const unpackBlocker = uploadBlocker || (!canDownload ? "i18n:govoplan-files.managed_archive.download_required" : "") || (!selectedArchive ? "i18n:govoplan-files.managed_archive.select_one" : "");
|
||||
const organizeBlocker = workingBlocker || managedSpaceBlocker || (!canOrganize ? "File organization permission is required." : "");
|
||||
const selectionBlocker = !hasSelection ? "Select at least one file or folder first." : "";
|
||||
const downloadBlocker = workingBlocker || managedSpaceBlocker || (!canDownload ? "File download permission is required." : "") || (selectedDownloadFileIds.length === 0 ? "Select at least one downloadable file first." : "");
|
||||
@@ -2268,57 +2415,61 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
["i18n:govoplan-files.folder_sync.summary.failed", folderSyncResult.summary.failed]
|
||||
] as const : [];
|
||||
|
||||
const toolbar =
|
||||
<ActionToolbar className="file-manager-toolbar" aria-label="i18n:govoplan-files.file_actions.9e1b94c5">
|
||||
<Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={Boolean(uploadBlocker)} disabledReason={uploadBlocker}><UploadCloud size={16} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</Button>
|
||||
<Button
|
||||
onClick={() => activeSpaceIsConnector ? void syncConnectorSpaceSelection() : void openConnectorSyncDialog(toolbarTarget())}
|
||||
disabled={Boolean(syncBlocker)}
|
||||
disabledReason={syncBlocker}>
|
||||
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.sync.905f6309
|
||||
</Button>
|
||||
{activeSpaceIsConnector &&
|
||||
<Button onClick={openConnectorFolderSyncDialog} disabled={Boolean(folderSyncBlocker)} disabledReason={folderSyncBlocker}>
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.folder_sync.button
|
||||
</Button>
|
||||
}
|
||||
<Button onClick={() => void openConnectorSpaceDialog()} disabled={busy || !canOrganize} disabledReason={workingBlocker || (!canOrganize ? "File organization permission is required to add a connector space." : "")}>
|
||||
<Link2 size={16} aria-hidden="true" /> i18n:govoplan-files.add_space.e4d674d4
|
||||
</Button>
|
||||
<Button onClick={() => void downloadSelection()} disabled={Boolean(downloadBlocker)} disabledReason={downloadBlocker}><Download size={16} aria-hidden="true" /> {downloadLabel}</Button>
|
||||
<Button onClick={() => shareManageableFile && setShareDialogFile(shareManageableFile)} disabled={Boolean(shareBlocker)} disabledReason={shareBlocker}><Share2 size={16} aria-hidden="true" /> Manage shares</Button>
|
||||
function chooseTool(action: () => void) {
|
||||
setToolsPanel(null);
|
||||
action();
|
||||
}
|
||||
|
||||
const toolbar = <WorkspaceActionBar
|
||||
scope="workspace"
|
||||
variant="collection"
|
||||
refreshable
|
||||
label="i18n:govoplan-files.file_actions.9e1b94c5"
|
||||
interfaceId="files.workspace.actions"
|
||||
helpContextId="files.list"
|
||||
helpModuleId="files"
|
||||
reloadAction={{ onReload: () => void reloadCurrentView(), loading: reloadingView, state: viewReloadFailed ? "reload-failed" : "current", disabled: busy || connectorSpaceLoading, disabledReason: workingBlocker || (connectorSpaceLoading ? "This connector space is already refreshing." : undefined) }}
|
||||
contextActions={<Button onClick={() => setToolsPanel("connections")} disabled={busy} disabledReason={workingBlocker}><Settings2 size={16} aria-hidden="true" /> i18n:govoplan-files.tools.connections</Button>}
|
||||
helpAction={<DocumentationHelpLink reference={FILES_WORKFLOW_DOCUMENTATION} />}
|
||||
createAction={<>
|
||||
<Button onClick={() => openDialog("create-folder", toolbarTarget())} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.create_folder.97bafaba</Button>
|
||||
<Button onClick={() => openTransferDialog("move")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><MoveRight size={16} aria-hidden="true" /> i18n:govoplan-files.move.76cdb950</Button>
|
||||
<Button onClick={() => openTransferDialog("copy")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
|
||||
{hasSelection && <Button onClick={openRenameDialog} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
|
||||
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
|
||||
<Button variant="danger" helpContextId="files.list" helpModuleId="files" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
|
||||
{activeSpaceIsConnector &&
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => activeSpace && setConnectorSpaceRemovalTarget(activeSpace)}
|
||||
disabled={busy || !canOrganize || !activeSpace?.connector_space_id}
|
||||
disabledReason={workingBlocker || (!canOrganize ? "File organization permission is required to remove a connector space." : !activeSpace?.connector_space_id ? "This space is not a removable connector space." : "")}>
|
||||
<Trash2 size={16} aria-hidden="true" /> Remove space
|
||||
</Button>
|
||||
}
|
||||
{activeSpaceIsConnector &&
|
||||
<Button onClick={() => activeSpace && void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading || !activeSpace} disabledReason={workingBlocker || (connectorSpaceLoading ? "This connector space is already refreshing." : !activeSpace ? "Select a connector space first." : "")}>
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc
|
||||
</Button>
|
||||
}
|
||||
<DocumentationHelpLink reference={FILES_WORKFLOW_DOCUMENTATION} />
|
||||
</ActionToolbar>;
|
||||
<Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={Boolean(uploadBlocker)} disabledReason={uploadBlocker}><UploadCloud size={16} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</Button>
|
||||
</>}
|
||||
/>;
|
||||
|
||||
const selectionToolbar = <WorkspaceActionBar
|
||||
scope="detail-pane"
|
||||
variant="detail"
|
||||
label="i18n:govoplan-files.tools.selection"
|
||||
contextActions={<span>{activeSpaceIsConnector ? connectorSpaceSelectedItem?.name || translateText("i18n:govoplan-files.no_file_selected.f76f1c1c") : selectedSummary}</span>}
|
||||
primaryActions={<>
|
||||
<Button onClick={() => void downloadSelection()} disabled={Boolean(downloadBlocker)} disabledReason={downloadBlocker}><Download size={16} aria-hidden="true" /> {downloadLabel}</Button>
|
||||
{selectedArchive && <Button onClick={() => openManagedArchive(selectedArchive, toolbarTarget())} disabled={Boolean(unpackBlocker)} disabledReason={unpackBlocker}><Archive size={16} aria-hidden="true" /> i18n:govoplan-files.managed_archive.unpack</Button>}
|
||||
<Button onClick={() => setToolsPanel("selection")} disabled={Boolean(workingBlocker || managedSpaceBlocker || selectionBlocker)} disabledReason={workingBlocker || managedSpaceBlocker || selectionBlocker}><Settings2 size={16} aria-hidden="true" /> i18n:govoplan-files.tools.selection</Button>
|
||||
</>}
|
||||
/>;
|
||||
|
||||
|
||||
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 selectedArchiveBytes = archivePreview?.entries.reduce((total, entry) => total + (entry.kind === "file" && selectedArchivePaths.has(entry.path) ? entry.size_bytes : 0), 0) ?? 0;
|
||||
const operationBusyLabel = uploadPhase === "inspecting" ? "i18n:govoplan-files.archive_progress.inspecting"
|
||||
: uploadPhase === "finalizing" ? "i18n:govoplan-files.archive_progress.finalizing"
|
||||
: uploadPhase === "unpacking" ? (archiveProgress?.phase === "storing" ? "i18n:govoplan-files.archive_progress.storing" : "i18n:govoplan-files.archive_progress.extracting")
|
||||
: "i18n:govoplan-files.uploading_files.6536791d";
|
||||
const serverProgressRatio = archiveProgress && (archiveProgress.total_bytes > 0
|
||||
? archiveProgress.completed_bytes / archiveProgress.total_bytes
|
||||
: archiveProgress.total_files > 0 ? archiveProgress.completed_files / archiveProgress.total_files : null);
|
||||
// Complete bytes/files do not imply a committed transaction. Keep finalization indeterminate.
|
||||
const operationProgressValue = archiveProgress?.status === "complete" ? 100
|
||||
: archiveProgress && archiveProgress.phase !== "finalizing" && serverProgressRatio !== null && serverProgressRatio < 1 ? serverProgressRatio * 100
|
||||
: uploadPhase === "uploading" && uploadProgress !== null && uploadProgress < 100 ? uploadProgress
|
||||
: null;
|
||||
const operationProgressLabel = archiveProgress && archiveProgress.total_files > 0
|
||||
? i18nMessage("i18n:govoplan-files.archive_progress.processed", { completed: archiveProgress.completed_files, total: archiveProgress.total_files, bytes: formatBytes(archiveProgress.completed_bytes), totalBytes: formatBytes(archiveProgress.total_bytes) })
|
||||
: uploadPhase === "uploading" && uploadProgress !== null && uploadProgress < 100
|
||||
? i18nMessage("i18n:govoplan-files.archive_progress.transferred", { percentage: Math.floor(uploadProgress) })
|
||||
: archivePreview && selectedArchivePaths.size > 0
|
||||
? i18nMessage("i18n:govoplan-files.archive_progress.selected", { total: selectedArchivePaths.size, bytes: formatBytes(selectedArchiveBytes) })
|
||||
: "i18n:govoplan-files.archive_progress.waiting";
|
||||
const connectorLocationLabel = activeConnectorProfile ?
|
||||
[activeConnectorProfile.label, connectorLibraryId, connectorPath || (connectorLibraryId ? "i18n:govoplan-files.root.e96857c5" : "")].filter(Boolean).join(" / ") :
|
||||
"i18n:govoplan-files.connector.ba358306";
|
||||
@@ -2406,7 +2557,6 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
<ActionToolbar className="connector-browser-toolbar">
|
||||
<Button onClick={browseConnectorSpaceParent} disabled={connectorSpaceParentDisabled}><ArrowUp size={15} aria-hidden="true" /> i18n:govoplan-files.up.2038bdec</Button>
|
||||
<span className="connector-browser-path" title={connectorSpaceLocationLabel}>{connectorSpaceLocationLabel}</span>
|
||||
<Button onClick={() => void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading}><RefreshCw size={15} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc</Button>
|
||||
</ActionToolbar>
|
||||
{connectorSpaceError && <p className="field-error connector-browser-error">{connectorSpaceError}</p>}
|
||||
<div className="connector-browser-list" role="list" aria-label="i18n:govoplan-files.connector_space_files.dbb0ab24">
|
||||
@@ -2462,7 +2612,49 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const currentFolderDropActive = currentFolderDropTarget ? dropTargetKey === dropTargetId(currentFolderDropTarget) : false;
|
||||
|
||||
return (
|
||||
<div className="workspace-data-page module-entry-page file-manager-page file-manager-fullscreen files-page">
|
||||
<WorkspaceFrame as="main" height="viewport" surface="plain" className="file-manager-page files-page" label="i18n:govoplan-files.files.6ce6c512" interfaceId="files.workspace" helpContextId="files.list" helpModuleId="files">
|
||||
{toolbar}
|
||||
<Dialog
|
||||
open={toolsPanel !== null}
|
||||
title={toolsPanel === "selection" ? "i18n:govoplan-files.tools.selection" : "i18n:govoplan-files.tools.connections"}
|
||||
description={toolsPanel === "selection" ? selectedSummary : "i18n:govoplan-files.tools.connections_description"}
|
||||
size="wide"
|
||||
onClose={() => setToolsPanel(null)}
|
||||
closeDisabled={busy}
|
||||
footer={<Button onClick={() => setToolsPanel(null)} disabled={busy}>i18n:govoplan-files.close.bbfa773e</Button>}
|
||||
>
|
||||
{toolsPanel === "selection" ? <ContentGrid columns={1}>
|
||||
<FormSection title="i18n:govoplan-files.tools.organize" description="i18n:govoplan-files.tools.organize_description">
|
||||
<ActionToolbar>
|
||||
<Button onClick={() => chooseTool(() => openTransferDialog("move"))} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><MoveRight size={16} aria-hidden="true" /> i18n:govoplan-files.move.76cdb950</Button>
|
||||
<Button onClick={() => chooseTool(() => openTransferDialog("copy"))} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
|
||||
<Button onClick={() => chooseTool(openRenameDialog)} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>
|
||||
</ActionToolbar>
|
||||
</FormSection>
|
||||
<FormSection title="i18n:govoplan-files.tools.sharing_access" variant="separated">
|
||||
<ActionToolbar>
|
||||
<Button onClick={() => chooseTool(() => { if (shareManageableFile) setShareDialogFile(shareManageableFile); })} disabled={Boolean(shareBlocker)} disabledReason={shareBlocker}><Share2 size={16} aria-hidden="true" /> Manage shares</Button>
|
||||
<Button onClick={() => chooseTool(() => { if (accessExplainableTarget) void openAccessExplanation(accessExplainableTarget); })} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
|
||||
</ActionToolbar>
|
||||
</FormSection>
|
||||
<FormSection title="i18n:govoplan-files.tools.destructive" description="i18n:govoplan-files.tools.delete_description" variant="separated">
|
||||
<WorkspaceActionBar scope="detail-pane" variant="detail" label="i18n:govoplan-files.tools.destructive" destructiveActions={<Button variant="danger" helpContextId="files.list" helpModuleId="files" onClick={() => chooseTool(() => void deleteSelected())} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>} />
|
||||
</FormSection>
|
||||
</ContentGrid> : <ContentGrid columns={1}>
|
||||
<FormSection title="i18n:govoplan-files.tools.import_sync" description="i18n:govoplan-files.tools.import_sync_description">
|
||||
<ActionToolbar>
|
||||
<Button onClick={() => chooseTool(() => { if (activeSpaceIsConnector) void syncConnectorSpaceSelection(); else void openConnectorSyncDialog(toolbarTarget()); })} disabled={Boolean(syncBlocker)} disabledReason={syncBlocker}><RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.sync.905f6309</Button>
|
||||
{activeSpaceIsConnector && <Button onClick={() => chooseTool(openConnectorFolderSyncDialog)} disabled={Boolean(folderSyncBlocker)} disabledReason={folderSyncBlocker}><RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.folder_sync.button</Button>}
|
||||
</ActionToolbar>
|
||||
</FormSection>
|
||||
<FormSection title="i18n:govoplan-files.tools.spaces" variant="separated">
|
||||
<ActionToolbar><Button onClick={() => chooseTool(() => void openConnectorSpaceDialog())} disabled={busy || !canOrganize} disabledReason={workingBlocker || (!canOrganize ? "File organization permission is required to add a connector space." : "")}><Link2 size={16} aria-hidden="true" /> i18n:govoplan-files.add_space.e4d674d4</Button></ActionToolbar>
|
||||
</FormSection>
|
||||
{activeSpaceIsConnector && <FormSection title="i18n:govoplan-files.tools.destructive" description="i18n:govoplan-files.tools.remove_space_description" variant="separated">
|
||||
<WorkspaceActionBar scope="detail-pane" variant="detail" label="i18n:govoplan-files.tools.destructive" destructiveActions={<Button variant="danger" onClick={() => chooseTool(() => { if (activeSpace) setConnectorSpaceRemovalTarget(activeSpace); })} disabled={busy || !canOrganize || !activeSpace?.connector_space_id} disabledReason={workingBlocker || (!canOrganize ? "File organization permission is required to remove a connector space." : !activeSpace?.connector_space_id ? "This space is not a removable connector space." : "")}><Trash2 size={16} aria-hidden="true" /> Remove space</Button>} />
|
||||
</FormSection>}
|
||||
</ContentGrid>}
|
||||
</Dialog>
|
||||
{error &&
|
||||
<DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>
|
||||
}
|
||||
@@ -2524,7 +2716,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
|
||||
<section className="file-list-panel" aria-label="i18n:govoplan-files.current_folder_contents.f9a24fa8">
|
||||
<div className="file-list-sticky">
|
||||
{toolbar}
|
||||
{selectionToolbar}
|
||||
<nav className="file-breadcrumbs" aria-label="i18n:govoplan-files.current_folder.5aeab2f0">
|
||||
<button type="button" className="file-breadcrumb" onClick={() => activeSpace && openFolder(activeSpace.id, "")} disabled={busy || !activeSpace}>
|
||||
<Home size={15} aria-hidden="true" /> {activeSpace?.label || "i18n:govoplan-files.files.6ce6c512"}
|
||||
@@ -2555,7 +2747,6 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
onClear={clearPropertyFilters} />
|
||||
|
||||
<div className="file-list-meta">
|
||||
<span>{activeSpaceIsConnector ? connectorSpaceSelectedItem?.kind === "file" ? connectorSpaceSelectedItem.name : "i18n:govoplan-files.remote_connector_space.d8956863" : selectedSummary}</span>
|
||||
<span>
|
||||
{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:
|
||||
@@ -2700,6 +2891,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
downloadLabel={downloadLabelForSets(selectedSetsForContext(contextMenu), contextMenu.spaceId ?? activeSpaceId)}
|
||||
onCreateFolder={() => openCreateFolderDialogForContext(contextMenu)}
|
||||
onUpload={() => openUploadDialogForContext(contextMenu)}
|
||||
canUnpackArchive={!busy && canUpload && canDownload && !isConnectorSpace(findSpace(contextMenu.spaceId ?? activeSpaceId)) && contextMenu.entry?.kind === "file" && ARCHIVE_FILENAME_PATTERN.test(contextMenu.entry.file.filename)}
|
||||
onUnpackArchive={() => contextMenu.entry?.kind === "file" && openManagedArchive(contextMenu.entry.file, contextActionTarget(contextMenu))}
|
||||
onDownload={() => void downloadContextSelection(contextMenu)}
|
||||
onMove={() => openTransferDialogForContext(contextMenu, "move")}
|
||||
onCopy={() => openTransferDialogForContext(contextMenu, "copy")}
|
||||
@@ -2735,14 +2928,30 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
|
||||
{dialog === "upload" &&
|
||||
<FileDialog title={i18nMessage("i18n:govoplan-files.upload_to_value_value.b83a34b4", { value0: activeDialogSpace?.label || "i18n:govoplan-files.files.6ce6c512", value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" })} onClose={closeDialog}>
|
||||
<FileDialog title={managedArchiveFile ? "i18n:govoplan-files.managed_archive.unpack" : i18nMessage("i18n:govoplan-files.upload_to_value_value.b83a34b4", { value0: activeDialogSpace?.label || "i18n:govoplan-files.files.6ce6c512", value1: activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5" })} busy={busy || uploadActive} onClose={() => { if (!busy) closeDialog(); }}>
|
||||
<LoadingFrame loading={uploadActive} label={operationBusyLabel} indicator="none" progress={operationProgressValue} progressLabel={operationProgressLabel}>
|
||||
<div inert={uploadActive}>
|
||||
{managedArchiveFile && <p className="form-help">{i18nMessage("i18n:govoplan-files.managed_archive.source", { value0: managedArchiveFile.filename })}</p>}
|
||||
{managedArchiveFile && <p className="form-help">i18n:govoplan-files.managed_archive.protection</p>}
|
||||
{managedArchiveFile && <DocumentationHelpLink reference={{ topicId: "files.workflow.unpack-managed-archive", documentationType: "user" }} />}
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{!archivePreview &&
|
||||
<>
|
||||
<ToggleSwitch
|
||||
{!managedArchiveFile && <ToggleSwitch
|
||||
label="Preview and unpack archive"
|
||||
checked={unpackZip}
|
||||
onChange={setUnpackZip}
|
||||
disabled={busy} />
|
||||
disabled={busy} />}
|
||||
|
||||
{managedArchiveFile && <FormField label="i18n:govoplan-files.destination_space.92b63970">
|
||||
<select value={activeDialogSpace?.id || ""} disabled={busy} onChange={(event) => {
|
||||
updateActiveDialogFolder(event.target.value, "");
|
||||
const space = findSpace(event.target.value);
|
||||
if (space) void loadSpaceContents(space, { silent: true });
|
||||
}}>
|
||||
{spaces.filter((space) => !isConnectorSpace(space)).map((space) => <option key={space.id} value={space.id}>{space.label}</option>)}
|
||||
</select>
|
||||
</FormField>}
|
||||
|
||||
{unpackZip &&
|
||||
<p className="form-help archive-upload-help">
|
||||
@@ -2759,25 +2968,22 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
onSelect={(folderPath) => activeDialogTarget && updateActiveDialogFolder(activeDialogTarget.spaceId, folderPath)} />
|
||||
|
||||
</div>
|
||||
<FileDropZone
|
||||
{!managedArchiveFile && <FileDropZone
|
||||
disabled={busy || !activeDialogTarget || !activeDialogSpace || isConnectorSpace(activeDialogSpace) || !canUpload}
|
||||
busy={uploadActive}
|
||||
progress={visibleUploadProgress}
|
||||
busyLabel={uploadBusyLabel}
|
||||
progressLabel={uploadProgressLabel}
|
||||
note={`Files are uploaded into ${activeDialogTarget?.folderPath || "i18n:govoplan-files.root.e96857c5"}.`}
|
||||
onRejectedDrop={(reason) => setError(reason === "disabled" ? uploadRejectedReason(activeDialogTarget) : "The browser did not provide readable file data for this drop. Use the file picker, or drag local files from a file manager that exposes file contents to the browser.")}
|
||||
onFiles={(files) => handleFilesUpload(files, { target: activeDialogTarget || undefined })} />
|
||||
onFiles={(files) => handleFilesUpload(files, { target: activeDialogTarget || undefined })} />}
|
||||
<div className="button-row compact-actions align-end">
|
||||
<Button onClick={closeDialog} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
|
||||
{managedArchiveFile && <Button variant="primary" disabled={busy || !activeDialogTarget || !activeDialogSpace || isConnectorSpace(activeDialogSpace)} onClick={() => activeDialogTarget && void loadArchivePreview(managedArchiveFile, activeDialogTarget)}>i18n:govoplan-files.managed_archive.preview</Button>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{archivePreview && archiveFile &&
|
||||
{archivePreview && (archiveFile || managedArchiveFile) &&
|
||||
<div className="archive-preview">
|
||||
<div className="archive-preview-summary">
|
||||
<div>
|
||||
<strong>{archiveFile.name}</strong>
|
||||
<strong>{managedArchiveFile?.filename || archiveFile?.name}</strong>
|
||||
<span>{archivePreview.archive_format.toUpperCase()} · {formatBytes(archivePreview.compressed_size_bytes)} compressed</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2840,30 +3046,33 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
<Folder size={16} aria-hidden="true" /> :
|
||||
<File size={16} aria-hidden="true" />
|
||||
}
|
||||
<span className="archive-entry-path">{entry.path.split("/").at(-1)}</span>
|
||||
<span className="archive-entry-path">{entry.path.split("/").slice(-1)[0]}</span>
|
||||
<span className="archive-entry-size">{entry.kind === "file" ? formatBytes(entry.size_bytes) : `${selectableFiles.length} files`}</span>
|
||||
</label>);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="form-help">
|
||||
Preview expires {formatDate(archivePreview.expires_at)}. The original archive is uploaded again only when you confirm.
|
||||
{managedArchiveFile
|
||||
? i18nMessage("i18n:govoplan-files.managed_archive.expires", { value0: formatDate(archivePreview.expires_at) })
|
||||
: i18nMessage("i18n:govoplan-files.archive_progress.preview_expires", { expires: formatDate(archivePreview.expires_at) })}
|
||||
</p>
|
||||
|
||||
<div className="button-row compact-actions archive-preview-actions">
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetArchiveUploadState();
|
||||
if (managedArchiveFile) { setArchivePreview(null); setSelectedArchivePaths(new Set()); }
|
||||
else resetArchiveUploadState();
|
||||
setError("");
|
||||
}}
|
||||
disabled={busy}>
|
||||
Choose another file
|
||||
{managedArchiveFile ? "i18n:govoplan-files.managed_archive.change_destination" : "Choose another file"}
|
||||
</Button>
|
||||
{archivePreview.requires_password &&
|
||||
<Button
|
||||
helpContextId="files.list"
|
||||
helpModuleId="files"
|
||||
onClick={() => activeDialogTarget && void loadArchivePreview(archiveFile, activeDialogTarget, { preserveSelection: true })}
|
||||
onClick={() => activeDialogTarget && void loadArchivePreview((managedArchiveFile || archiveFile)!, activeDialogTarget, { preserveSelection: true })}
|
||||
disabled={busy || !archivePassword}>
|
||||
<RefreshCw size={15} aria-hidden="true" /> Verify password
|
||||
</Button>
|
||||
@@ -2883,6 +3092,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
</FileDialog>
|
||||
}
|
||||
|
||||
@@ -3243,7 +3454,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
</FileDialog>
|
||||
}
|
||||
</div>);
|
||||
</WorkspaceFrame>);
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user