feat(files): orchestrate connector folder sync
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-21 22:49:47 +02:00
parent a8c3192c3a
commit 378f4d6ac5
19 changed files with 1302 additions and 30 deletions
+54 -1
View File
@@ -426,6 +426,48 @@ export type FileConnectorSyncResponse = {
previous_version_id?: string | null;
current_version_id: string;
};
export type FileConnectorFolderSyncAction = "created" | "updated" | "unchanged" | "skipped" | "conflict" | "policy_denied" | "failed";
export type FileConnectorFolderSyncItem = {
source_path: string;
target_path?: string | null;
action: FileConnectorFolderSyncAction;
file?: ManagedFile | null;
previous_version_id?: string | null;
current_version_id?: string | null;
source_revision?: string | null;
detail?: string | null;
policy_decision?: FileConnectorPolicyDecision | null;
};
export type FileConnectorFolderSyncSummary = {
discovered: number;
created: number;
updated: number;
unchanged: number;
skipped: number;
conflicts: number;
policy_denied: number;
failed: number;
};
export type FileConnectorFolderSyncResponse = {
connector_space_id: string;
connector_profile_id: string;
provider: string;
remote_path: string;
target_folder: string;
recursive: boolean;
truncated: boolean;
summary: FileConnectorFolderSyncSummary;
items: FileConnectorFolderSyncItem[];
};
export type FileConnectorFolderSyncPayload = {
path?: string;
target_folder?: string | null;
recursive?: boolean;
conflict_strategy?: "reject" | "overwrite" | "rename" | "skip";
max_files?: number;
max_depth?: number;
metadata?: Record<string, unknown>;
};
export type FileConnectorWritePayload = {
file_id: string;
remote_path: string;
@@ -502,7 +544,7 @@ export type FileBlobGcResponse = {
export type RenameResponse = {dry_run: boolean;items: {kind: "file" | "folder";id: string;file_id?: string | null;folder_path?: string | null;old_path: string;new_path: string;}[];};
export type TransferResponse = {operation: "move" | "copy";files: number;folders: number;};
export type ConflictAction = "overwrite" | "rename" | "skip";
export type ConflictStrategy = "reject" | "overwrite" | "rename";
export type ConflictStrategy = "reject" | "overwrite" | "rename" | "skip";
export type ConflictResolution = {target_path: string;action: ConflictAction;new_path?: string;};
export type PatternResolveResponse = {
patterns: {pattern: string;matches: ManagedFile[];}[];
@@ -1176,6 +1218,17 @@ payload: FileConnectorFilePayload)
});
}
export function syncFileConnectorSpaceFolder(
settings: ApiSettings,
spaceId: string,
payload: FileConnectorFolderSyncPayload)
: Promise<FileConnectorFolderSyncResponse> {
return apiFetch<FileConnectorFolderSyncResponse>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}/sync`, {
method: "POST",
body: JSON.stringify({ path: "", target_folder: "", recursive: true, conflict_strategy: "skip", max_files: 100, max_depth: 12, metadata: {}, ...payload })
});
}
export function shareFilesWithCampaign(settings: ApiSettings, fileIds: string[], campaignId: string): Promise<FileBulkShareResponse> {
return shareFilesWithTarget(settings, fileIds, { type: "campaign", id: campaignId, label: "campaign" });
}
+181 -2
View File
@@ -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,
+1 -1
View File
@@ -4,7 +4,7 @@ export type RenameMode = "prefix" | "suffix" | "replace";
export type SortColumn = "name" | "size" | "modified";
export type SortDirection = "asc" | "desc";
export type TransferMode = "move" | "copy";
export type DialogKind = "upload" | "connector-sync" | "connector-space" | "create-folder" | "rename" | "single-rename" | "transfer" | null;
export type DialogKind = "upload" | "connector-sync" | "connector-folder-sync" | "connector-space" | "create-folder" | "rename" | "single-rename" | "transfer" | null;
export type EntrySelectionKey = `file:${string}` | `folder:${string}`;
export type ContextMenuState = {
x: number;
+86
View File
@@ -165,6 +165,49 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-files.folder_name.b2ce023b": "Folder name",
"i18n:govoplan-files.folder_s.10e54730": "folder(s)",
"i18n:govoplan-files.folder.30baa249": "Folder",
"i18n:govoplan-files.folder_sync.action.conflict": "Conflict",
"i18n:govoplan-files.folder_sync.action.created": "Created",
"i18n:govoplan-files.folder_sync.action.failed": "Failed",
"i18n:govoplan-files.folder_sync.action.policy_denied": "Policy denied",
"i18n:govoplan-files.folder_sync.action.skipped": "Skipped",
"i18n:govoplan-files.folder_sync.action.unchanged": "Unchanged",
"i18n:govoplan-files.folder_sync.action.updated": "Updated",
"i18n:govoplan-files.folder_sync.blocker.loading": "Wait until the connector folder has finished loading.",
"i18n:govoplan-files.folder_sync.blocker.permission": "File upload permission is required to synchronize a connector folder.",
"i18n:govoplan-files.folder_sync.blocker.select_space": "Select a linked connector space first.",
"i18n:govoplan-files.folder_sync.button": "Sync folder",
"i18n:govoplan-files.folder_sync.conflict.help": "Skip is privacy-safe and leaves unrelated managed files untouched. Rename creates a distinct managed path; reject reports conflicts; overwrite retires a colliding unrelated target.",
"i18n:govoplan-files.folder_sync.conflict.label": "Existing target",
"i18n:govoplan-files.folder_sync.conflict.overwrite": "Replace unrelated target",
"i18n:govoplan-files.folder_sync.conflict.reject": "Report as conflict",
"i18n:govoplan-files.folder_sync.conflict.rename": "Keep both with a new name",
"i18n:govoplan-files.folder_sync.conflict.skip": "Skip unrelated conflicts",
"i18n:govoplan-files.folder_sync.description": "Synchronize the visible remote folder into the linked owner's managed Files space. Existing connector identities become new versions; every skip, conflict, policy denial, and transport failure remains visible below.",
"i18n:govoplan-files.folder_sync.destination.help": "Remote relative paths are preserved in the user or group space that owns this connector link.",
"i18n:govoplan-files.folder_sync.destination.label": "Managed destination",
"i18n:govoplan-files.folder_sync.finished": "Folder sync finished: {value0} created, {value1} updated, {value2} unchanged, {value3} skipped, {value4} need review.",
"i18n:govoplan-files.folder_sync.limit.help": "Bounds one interactive run. If the limit is reached, the result is marked truncated and can be continued with a narrower folder.",
"i18n:govoplan-files.folder_sync.limit.label": "Maximum files",
"i18n:govoplan-files.folder_sync.recursive.help": "Subfolders are traversed to a bounded depth. Disable this to synchronize files in the visible folder only.",
"i18n:govoplan-files.folder_sync.recursive.label": "Include subfolders",
"i18n:govoplan-files.folder_sync.remote.help": "The folder is resolved below the linked connector-space root; parent-directory traversal is rejected.",
"i18n:govoplan-files.folder_sync.remote.label": "Remote folder",
"i18n:govoplan-files.folder_sync.results.aria_label": "Folder sync results",
"i18n:govoplan-files.folder_sync.results.detail": "Managed target or detail",
"i18n:govoplan-files.folder_sync.results.result": "Result",
"i18n:govoplan-files.folder_sync.results.source": "Source",
"i18n:govoplan-files.folder_sync.run_again": "Run again",
"i18n:govoplan-files.folder_sync.summary.aria_label": "Folder sync summary",
"i18n:govoplan-files.folder_sync.summary.conflicts": "conflicts",
"i18n:govoplan-files.folder_sync.summary.created": "created",
"i18n:govoplan-files.folder_sync.summary.discovered": "discovered",
"i18n:govoplan-files.folder_sync.summary.failed": "failed",
"i18n:govoplan-files.folder_sync.summary.policy_denied": "policy denied",
"i18n:govoplan-files.folder_sync.summary.skipped": "skipped",
"i18n:govoplan-files.folder_sync.summary.unchanged": "unchanged",
"i18n:govoplan-files.folder_sync.summary.updated": "updated",
"i18n:govoplan-files.folder_sync.title": "Sync connector folder",
"i18n:govoplan-files.folder_sync.truncated": "The file limit was reached. Select a narrower remote folder or raise the bounded maximum to continue.",
"i18n:govoplan-files.folders_and_files.d107ac99": "Folders and files",
"i18n:govoplan-files.folders.19adc47b": "Folders",
"i18n:govoplan-files.gb.505a44fa": "GB",
@@ -521,6 +564,49 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-files.folder_name.b2ce023b": "Ordnername",
"i18n:govoplan-files.folder_s.10e54730": "folder(s)",
"i18n:govoplan-files.folder.30baa249": "Ordner",
"i18n:govoplan-files.folder_sync.action.conflict": "Konflikt",
"i18n:govoplan-files.folder_sync.action.created": "Erstellt",
"i18n:govoplan-files.folder_sync.action.failed": "Fehlgeschlagen",
"i18n:govoplan-files.folder_sync.action.policy_denied": "Durch Richtlinie abgelehnt",
"i18n:govoplan-files.folder_sync.action.skipped": "Übersprungen",
"i18n:govoplan-files.folder_sync.action.unchanged": "Unverändert",
"i18n:govoplan-files.folder_sync.action.updated": "Aktualisiert",
"i18n:govoplan-files.folder_sync.blocker.loading": "Warten Sie, bis der Connector-Ordner vollständig geladen ist.",
"i18n:govoplan-files.folder_sync.blocker.permission": "Zum Synchronisieren eines Connector-Ordners ist die Berechtigung zum Hochladen von Dateien erforderlich.",
"i18n:govoplan-files.folder_sync.blocker.select_space": "Wählen Sie zuerst einen verknüpften Connector-Bereich aus.",
"i18n:govoplan-files.folder_sync.button": "Ordner synchronisieren",
"i18n:govoplan-files.folder_sync.conflict.help": "Überspringen belässt nicht zugehörige verwaltete Dateien unverändert. Umbenennen erstellt einen eigenen verwalteten Pfad; Ablehnen meldet Konflikte; Ersetzen setzt ein kollidierendes, nicht zugehöriges Ziel außer Kraft.",
"i18n:govoplan-files.folder_sync.conflict.label": "Vorhandenes Ziel",
"i18n:govoplan-files.folder_sync.conflict.overwrite": "Nicht zugehöriges Ziel ersetzen",
"i18n:govoplan-files.folder_sync.conflict.reject": "Als Konflikt melden",
"i18n:govoplan-files.folder_sync.conflict.rename": "Beide unter neuem Namen behalten",
"i18n:govoplan-files.folder_sync.conflict.skip": "Nicht zugehörige Konflikte überspringen",
"i18n:govoplan-files.folder_sync.description": "Synchronisiert den sichtbaren Remote-Ordner in den verwalteten Files-Bereich des verknüpften Eigentümers. Bereits bekannte Connector-Identitäten erhalten neue Versionen; Überspringungen, Konflikte, Richtlinienablehnungen und Transportfehler bleiben unten einzeln prüfbar.",
"i18n:govoplan-files.folder_sync.destination.help": "Relative Remote-Pfade bleiben im Benutzer- oder Gruppenbereich erhalten, dem diese Connector-Verknüpfung gehört.",
"i18n:govoplan-files.folder_sync.destination.label": "Verwaltetes Ziel",
"i18n:govoplan-files.folder_sync.finished": "Ordnersynchronisierung abgeschlossen: {value0} erstellt, {value1} aktualisiert, {value2} unverändert, {value3} übersprungen, {value4} zu prüfen.",
"i18n:govoplan-files.folder_sync.limit.help": "Begrenzt einen interaktiven Lauf. Wird der Grenzwert erreicht, ist das Ergebnis als abgeschnitten markiert und kann mit einem engeren Ordner fortgesetzt werden.",
"i18n:govoplan-files.folder_sync.limit.label": "Maximale Dateien",
"i18n:govoplan-files.folder_sync.recursive.help": "Unterordner werden nur bis zu einer begrenzten Tiefe durchlaufen. Deaktivieren Sie dies, um ausschließlich Dateien im sichtbaren Ordner zu synchronisieren.",
"i18n:govoplan-files.folder_sync.recursive.label": "Unterordner einbeziehen",
"i18n:govoplan-files.folder_sync.remote.help": "Der Ordner wird unterhalb der Wurzel des verknüpften Connector-Bereichs aufgelöst; übergeordnete Pfade werden abgelehnt.",
"i18n:govoplan-files.folder_sync.remote.label": "Remote-Ordner",
"i18n:govoplan-files.folder_sync.results.aria_label": "Ergebnisse der Ordnersynchronisierung",
"i18n:govoplan-files.folder_sync.results.detail": "Verwaltetes Ziel oder Detail",
"i18n:govoplan-files.folder_sync.results.result": "Ergebnis",
"i18n:govoplan-files.folder_sync.results.source": "Quelle",
"i18n:govoplan-files.folder_sync.run_again": "Erneut ausführen",
"i18n:govoplan-files.folder_sync.summary.aria_label": "Zusammenfassung der Ordnersynchronisierung",
"i18n:govoplan-files.folder_sync.summary.conflicts": "Konflikte",
"i18n:govoplan-files.folder_sync.summary.created": "erstellt",
"i18n:govoplan-files.folder_sync.summary.discovered": "gefunden",
"i18n:govoplan-files.folder_sync.summary.failed": "fehlgeschlagen",
"i18n:govoplan-files.folder_sync.summary.policy_denied": "durch Richtlinie abgelehnt",
"i18n:govoplan-files.folder_sync.summary.skipped": "übersprungen",
"i18n:govoplan-files.folder_sync.summary.unchanged": "unverändert",
"i18n:govoplan-files.folder_sync.summary.updated": "aktualisiert",
"i18n:govoplan-files.folder_sync.title": "Connector-Ordner synchronisieren",
"i18n:govoplan-files.folder_sync.truncated": "Der Dateigrenzwert wurde erreicht. Wählen Sie zum Fortsetzen einen engeren Remote-Ordner oder erhöhen Sie den begrenzten Höchstwert.",
"i18n:govoplan-files.folders_and_files.d107ac99": "Folders and files",
"i18n:govoplan-files.folders.19adc47b": "Ordner",
"i18n:govoplan-files.gb.505a44fa": "GB",
+77
View File
@@ -1256,6 +1256,78 @@
color: var(--muted);
}
.connector-folder-sync-review {
display: grid;
gap: 12px;
}
.connector-folder-sync-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
gap: 8px;
}
.connector-folder-sync-summary span {
display: grid;
gap: 2px;
padding: 9px 10px;
border: var(--border-line);
border-radius: var(--radius-control);
background: var(--panel-soft);
color: var(--muted);
font-size: 12px;
text-transform: capitalize;
}
.connector-folder-sync-summary strong {
color: var(--text-strong);
font-size: 18px;
}
.connector-folder-sync-results {
max-height: min(320px, 42vh);
overflow: auto;
border: var(--border-line);
border-radius: var(--radius-control);
}
.connector-folder-sync-result {
display: grid;
grid-template-columns: minmax(180px, 1.2fr) minmax(92px, 0.45fr) minmax(220px, 1.5fr);
gap: 12px;
padding: 9px 11px;
border-bottom: var(--border-line);
font-size: 12px;
}
.connector-folder-sync-result:last-child {
border-bottom: 0;
}
.connector-folder-sync-result > * {
min-width: 0;
overflow-wrap: anywhere;
}
.connector-folder-sync-result.is-header {
position: sticky;
z-index: 1;
top: 0;
background: var(--panel-raised, var(--panel));
}
.connector-folder-sync-result.is-created strong,
.connector-folder-sync-result.is-updated strong,
.connector-folder-sync-result.is-unchanged strong {
color: var(--success, var(--text-strong));
}
.connector-folder-sync-result.is-conflict strong,
.connector-folder-sync-result.is-policy_denied strong,
.connector-folder-sync-result.is-failed strong {
color: var(--danger, var(--text-strong));
}
@media (max-width: 900px) {
.managed-file-entry-head,
.managed-file-entry {
@@ -1269,6 +1341,11 @@
}
@media (max-width: 760px) {
.connector-folder-sync-result {
grid-template-columns: 1fr;
gap: 4px;
}
.managed-file-chooser-dialog {
width: calc(100vw - 20px);
height: calc(100vh - 20px);