feat(files): orchestrate connector folder sync
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -13,6 +13,7 @@ import { FormGrid, ActionToolbar,
|
||||
ResourceAccessExplanation,
|
||||
ToggleSwitch,
|
||||
hasScope,
|
||||
usePlatformLanguage,
|
||||
type ApiSettings,
|
||||
type AuthInfo, i18nMessage } from
|
||||
"@govoplan/core-webui";
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
listManagedFileSnapshot,
|
||||
previewArchiveUpload,
|
||||
resolveFilePatterns,
|
||||
syncFileConnectorSpaceFolder,
|
||||
syncFileConnectorFile,
|
||||
transferFiles,
|
||||
uploadFiles,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
type ConflictResolution,
|
||||
type ConflictStrategy,
|
||||
type FileConnectorBrowseItem,
|
||||
type FileConnectorFolderSyncResponse,
|
||||
type FileConnectorProfile,
|
||||
type FileDeltaResponse,
|
||||
type FileCampaignUsageFilter,
|
||||
@@ -120,6 +123,7 @@ const FILES_WORKFLOW_DOCUMENTATION = {
|
||||
export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const canDownload = hasScope(auth, "files:download");
|
||||
const canUpload = hasScope(auth, "files:upload");
|
||||
const canOrganize = hasScope(auth, "files:organize");
|
||||
@@ -175,6 +179,10 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const [connectorSpaceRemovalTarget, setConnectorSpaceRemovalTarget] = useState<FileSpace | null>(null);
|
||||
const [connectorSpaceLoading, setConnectorSpaceLoading] = useState(false);
|
||||
const [connectorSpaceError, setConnectorSpaceError] = useState("");
|
||||
const [folderSyncRecursive, setFolderSyncRecursive] = useState(true);
|
||||
const [folderSyncConflictStrategy, setFolderSyncConflictStrategy] = useState<"skip" | "rename" | "reject" | "overwrite">("skip");
|
||||
const [folderSyncMaxFiles, setFolderSyncMaxFiles] = useState(100);
|
||||
const [folderSyncResult, setFolderSyncResult] = useState<FileConnectorFolderSyncResponse | null>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [accessExplanationTarget, setAccessExplanationTarget] = useState<FileAccessExplanationTarget | null>(null);
|
||||
@@ -555,10 +563,10 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
|
||||
function openDialog(kind: DialogKind, target: FileActionTarget | null = null) {
|
||||
if ((kind === "upload" || kind === "connector-sync") && !canUpload) return;
|
||||
if ((kind === "upload" || kind === "connector-sync" || kind === "connector-folder-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 (target && isConnectorSpace(findSpace(target.spaceId)) && kind !== "connector-sync" && kind !== "connector-folder-sync") return;
|
||||
if (kind === "create-folder") {
|
||||
setNewFolderName("");
|
||||
setNewFolderError("");
|
||||
@@ -586,6 +594,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
setConnectorSelectedItem(null);
|
||||
setConnectorSpaceLabel("");
|
||||
setConnectorSpaceReadOnly(true);
|
||||
setFolderSyncResult(null);
|
||||
resetArchiveUploadState();
|
||||
}
|
||||
|
||||
@@ -739,6 +748,56 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
}
|
||||
|
||||
function openConnectorFolderSyncDialog() {
|
||||
if (!canUpload || !activeSpace || !isConnectorSpace(activeSpace) || !activeSpace.connector_space_id) return;
|
||||
setFolderSyncRecursive(true);
|
||||
setFolderSyncConflictStrategy("skip");
|
||||
setFolderSyncMaxFiles(100);
|
||||
setFolderSyncResult(null);
|
||||
setDialog("connector-folder-sync");
|
||||
}
|
||||
|
||||
async function syncActiveConnectorFolder() {
|
||||
const space = activeSpace;
|
||||
if (!canUpload || !space || !isConnectorSpace(space) || !space.connector_space_id) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setMessage("");
|
||||
setConnectorSpaceError("");
|
||||
try {
|
||||
const response = await syncFileConnectorSpaceFolder(settings, space.connector_space_id, {
|
||||
path: currentFolder,
|
||||
target_folder: "",
|
||||
recursive: folderSyncRecursive,
|
||||
conflict_strategy: folderSyncConflictStrategy,
|
||||
max_files: folderSyncMaxFiles,
|
||||
max_depth: 12,
|
||||
metadata: {
|
||||
initiated_from: "files_connector_space",
|
||||
connector_space_label: space.label
|
||||
}
|
||||
});
|
||||
setFolderSyncResult(response);
|
||||
const summary = response.summary;
|
||||
setMessage(i18nMessage("i18n:govoplan-files.folder_sync.finished", {
|
||||
value0: summary.created,
|
||||
value1: summary.updated,
|
||||
value2: summary.unchanged,
|
||||
value3: summary.skipped,
|
||||
value4: summary.conflicts + summary.policy_denied + summary.failed
|
||||
}));
|
||||
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: true });
|
||||
await loadConnectorSpaceContents(space, { folderPath: currentFolder, silent: true });
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
setError(detail);
|
||||
setConnectorSpaceError(detail);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadArchivePreview(
|
||||
@@ -2197,6 +2256,17 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const accessExplanationBlocker = workingBlocker || (activeSpaceIsConnector ? "Access explanations apply to managed files and folders." : "") || (!canExplainResourceAccess ? "Permission to inspect resource access is required." : "") || (!accessExplainableTarget ? "Select one file or folder first." : "");
|
||||
const deleteBlocker = workingBlocker || managedSpaceBlocker || (!canDelete ? "File deletion permission is required." : "") || selectionBlocker;
|
||||
const syncBlocker = workingBlocker || (!activeSpace ? "Select a file space first." : "") || (!canUpload ? "File upload permission is required to import synchronized content." : "") || (activeSpaceIsConnector && connectorSpaceSelectedItem?.kind !== "file" ? "Select one remote file to synchronize." : "");
|
||||
const folderSyncBlocker = workingBlocker || (!activeSpaceIsConnector || !activeSpace?.connector_space_id ? "i18n:govoplan-files.folder_sync.blocker.select_space" : "") || (!canUpload ? "i18n:govoplan-files.folder_sync.blocker.permission" : "") || (connectorSpaceLoading ? "i18n:govoplan-files.folder_sync.blocker.loading" : "");
|
||||
const folderSyncSummaryRows = folderSyncResult ? [
|
||||
["i18n:govoplan-files.folder_sync.summary.discovered", folderSyncResult.summary.discovered],
|
||||
["i18n:govoplan-files.folder_sync.summary.created", folderSyncResult.summary.created],
|
||||
["i18n:govoplan-files.folder_sync.summary.updated", folderSyncResult.summary.updated],
|
||||
["i18n:govoplan-files.folder_sync.summary.unchanged", folderSyncResult.summary.unchanged],
|
||||
["i18n:govoplan-files.folder_sync.summary.skipped", folderSyncResult.summary.skipped],
|
||||
["i18n:govoplan-files.folder_sync.summary.conflicts", folderSyncResult.summary.conflicts],
|
||||
["i18n:govoplan-files.folder_sync.summary.policy_denied", folderSyncResult.summary.policy_denied],
|
||||
["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">
|
||||
@@ -2208,6 +2278,11 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
|
||||
<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>
|
||||
@@ -2973,6 +3048,97 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
</FileDialog>
|
||||
}
|
||||
|
||||
{dialog === "connector-folder-sync" && activeSpace && activeSpaceIsConnector &&
|
||||
<FileDialog title="i18n:govoplan-files.folder_sync.title" onClose={closeDialog}>
|
||||
<p className="muted">i18n:govoplan-files.folder_sync.description</p>
|
||||
<FormGrid columns={2} collapseAt="standard" className="">
|
||||
<FormField
|
||||
label="i18n:govoplan-files.folder_sync.remote.label"
|
||||
help="i18n:govoplan-files.folder_sync.remote.help"
|
||||
interfaceId="files.connector-folder-sync.remote-path"
|
||||
helpContextId="files.connector-folder-sync.remote-path"
|
||||
helpModuleId="files"
|
||||
helpTopicId="files.governed-connectors-and-provenance">
|
||||
<input value={currentFolder || translateText("i18n:govoplan-files.root.e96857c5")} readOnly />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="i18n:govoplan-files.folder_sync.destination.label"
|
||||
help="i18n:govoplan-files.folder_sync.destination.help"
|
||||
interfaceId="files.connector-folder-sync.target-folder"
|
||||
helpContextId="files.connector-folder-sync.target-folder"
|
||||
helpModuleId="files"
|
||||
helpTopicId="files.governed-connectors-and-provenance">
|
||||
<input value={spaces.find((item) => item.space_type !== "connector" && item.owner_type === activeSpace.owner_type && item.owner_id === activeSpace.owner_id)?.label || `${activeSpace.owner_type}:${activeSpace.owner_id}`} readOnly />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="i18n:govoplan-files.folder_sync.conflict.label"
|
||||
help="i18n:govoplan-files.folder_sync.conflict.help"
|
||||
interfaceId="files.connector-folder-sync.conflict-strategy"
|
||||
helpContextId="files.connector-folder-sync.conflict-strategy"
|
||||
helpModuleId="files"
|
||||
helpTopicId="files.governed-connectors-and-provenance">
|
||||
<select value={folderSyncConflictStrategy} onChange={(event) => setFolderSyncConflictStrategy(event.target.value as typeof folderSyncConflictStrategy)} disabled={busy}>
|
||||
<option value="skip">i18n:govoplan-files.folder_sync.conflict.skip</option>
|
||||
<option value="rename">i18n:govoplan-files.folder_sync.conflict.rename</option>
|
||||
<option value="reject">i18n:govoplan-files.folder_sync.conflict.reject</option>
|
||||
<option value="overwrite">i18n:govoplan-files.folder_sync.conflict.overwrite</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="i18n:govoplan-files.folder_sync.limit.label"
|
||||
help="i18n:govoplan-files.folder_sync.limit.help"
|
||||
interfaceId="files.connector-folder-sync.max-files"
|
||||
helpContextId="files.connector-folder-sync.max-files"
|
||||
helpModuleId="files"
|
||||
helpTopicId="files.governed-connectors-and-provenance">
|
||||
<input type="number" min={1} max={500} value={folderSyncMaxFiles} onChange={(event) => setFolderSyncMaxFiles(Math.min(500, Math.max(1, Number(event.target.value) || 1)))} disabled={busy} />
|
||||
</FormField>
|
||||
</FormGrid>
|
||||
<ToggleSwitch
|
||||
label="i18n:govoplan-files.folder_sync.recursive.label"
|
||||
checked={folderSyncRecursive}
|
||||
onChange={setFolderSyncRecursive}
|
||||
disabled={busy}
|
||||
help="i18n:govoplan-files.folder_sync.recursive.help"
|
||||
interfaceId="files.connector-folder-sync.recursive"
|
||||
helpContextId="files.connector-folder-sync.recursive"
|
||||
helpModuleId="files"
|
||||
helpTopicId="files.governed-connectors-and-provenance" />
|
||||
|
||||
{folderSyncResult &&
|
||||
<div className="connector-folder-sync-review" aria-live="polite">
|
||||
<div className="connector-folder-sync-summary" aria-label="i18n:govoplan-files.folder_sync.summary.aria_label">
|
||||
{folderSyncSummaryRows.map(([label, value]) =>
|
||||
<span key={label}><strong>{value}</strong> {label}</span>
|
||||
)}
|
||||
</div>
|
||||
{folderSyncResult.truncated && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-files.folder_sync.truncated</DismissibleAlert>}
|
||||
<div className="connector-folder-sync-results" role="table" aria-label="i18n:govoplan-files.folder_sync.results.aria_label">
|
||||
<div className="connector-folder-sync-result is-header" role="row">
|
||||
<strong role="columnheader">i18n:govoplan-files.folder_sync.results.source</strong>
|
||||
<strong role="columnheader">i18n:govoplan-files.folder_sync.results.result</strong>
|
||||
<strong role="columnheader">i18n:govoplan-files.folder_sync.results.detail</strong>
|
||||
</div>
|
||||
{folderSyncResult.items.map((item, index) =>
|
||||
<div className={`connector-folder-sync-result is-${item.action}`} role="row" key={`${item.source_path}:${index}`}>
|
||||
<span role="cell">{item.source_path}</span>
|
||||
<strong role="cell">{folderSyncActionLabel(item.action)}</strong>
|
||||
<span role="cell">{item.target_path || item.detail || item.policy_decision?.reason || "—"}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="button-row compact-actions align-end">
|
||||
<Button onClick={closeDialog} disabled={busy}>{folderSyncResult ? "i18n:govoplan-files.close.bbfa773e" : "i18n:govoplan-files.cancel.77dfd213"}</Button>
|
||||
<Button variant="primary" onClick={() => void syncActiveConnectorFolder()} disabled={busy || connectorSpaceLoading}>
|
||||
<RefreshCw size={15} aria-hidden="true" /> {folderSyncResult ? "i18n:govoplan-files.folder_sync.run_again" : "i18n:govoplan-files.folder_sync.button"}
|
||||
</Button>
|
||||
</div>
|
||||
</FileDialog>
|
||||
}
|
||||
|
||||
{dialog === "create-folder" &&
|
||||
<FileDialog title="i18n:govoplan-files.create_folder.e59f63fa" onClose={closeDialog}>
|
||||
<FormField label="i18n:govoplan-files.folder_name.b2ce023b" help="i18n:govoplan-files.create_a_folder_below_the_selected_destination.9041ee76">
|
||||
@@ -3075,6 +3241,19 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
|
||||
}
|
||||
|
||||
function folderSyncActionLabel(action: FileConnectorFolderSyncResponse["items"][number]["action"]): string {
|
||||
const labels: Record<FileConnectorFolderSyncResponse["items"][number]["action"], string> = {
|
||||
created: "i18n:govoplan-files.folder_sync.action.created",
|
||||
updated: "i18n:govoplan-files.folder_sync.action.updated",
|
||||
unchanged: "i18n:govoplan-files.folder_sync.action.unchanged",
|
||||
skipped: "i18n:govoplan-files.folder_sync.action.skipped",
|
||||
conflict: "i18n:govoplan-files.folder_sync.action.conflict",
|
||||
policy_denied: "i18n:govoplan-files.folder_sync.action.policy_denied",
|
||||
failed: "i18n:govoplan-files.folder_sync.action.failed"
|
||||
};
|
||||
return labels[action];
|
||||
}
|
||||
|
||||
function FilePropertyFilterRow({
|
||||
campaignUsage,
|
||||
auditRelevant,
|
||||
|
||||
Reference in New Issue
Block a user