chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:21 +02:00
parent 18e6c3eb9b
commit 13fd7fc3bd
50 changed files with 12678 additions and 1109 deletions

View File

@@ -20,7 +20,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"lucide-react": "^0.555.0",
"lucide-react": "^1.23.0",
"@govoplan/core-webui": "^0.1.6"
},
"peerDependenciesMeta": {

View File

@@ -1,4 +1,4 @@
import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type FilesManagedFileLinkTarget } from "@govoplan/core-webui";
import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type DeltaDeletedItem, type FilesManagedFileLinkTarget } from "@govoplan/core-webui";
export type FileSpace = {
id: string;
@@ -6,6 +6,14 @@ export type FileSpace = {
owner_type: "user" | "group";
owner_id: string;
description?: string | null;
space_type?: "managed" | "connector";
connector_space_id?: string | null;
connector_profile_id?: string | null;
provider?: string | null;
library_id?: string | null;
remote_path?: string | null;
sync_mode?: string | null;
read_only?: boolean;
};
export type FileShare = {
@@ -17,6 +25,250 @@ export type FileShare = {
revoked_at?: string | null;
};
export type FileSourceProvenance = {
source_type?: string | null;
connector_id?: string | null;
provider?: string | null;
external_id?: string | null;
external_path?: string | null;
external_url?: string | null;
revision?: string | null;
revision_label?: string | null;
observed_at?: string | null;
imported_at?: string | null;
metadata?: Record<string, unknown>;
};
export type FileConnectorPolicySource = {
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
label?: string | null;
policy: Record<string, unknown>;
};
export type FileConnectorPolicyDecision = {
allowed: boolean;
reason?: string | null;
source_path: unknown[];
requirements: string[];
details: Record<string, unknown>;
};
export type FileConnectorPolicyStep = {
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
path: string;
label: string;
applied_fields: string[];
policy: Record<string, unknown>;
};
export type FileConnectorPolicyResponse = {
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
policy: Record<string, unknown>;
effective_policy?: Record<string, unknown> | null;
parent_policy?: Record<string, unknown> | null;
effective_policy_sources?: FileConnectorPolicyStep[];
parent_policy_sources?: FileConnectorPolicyStep[];
};
export type FileConnectorProfile = {
id: string;
label: string;
provider: string;
endpoint_url?: string | null;
base_path?: string | null;
enabled: boolean;
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
source_path: string;
credential_profile_id?: string | null;
credential_profile_label?: string | null;
credential_mode: string;
credential_source?: string | null;
credentials_configured: boolean;
username?: string | null;
capabilities: string[];
policy_sources: FileConnectorPolicySource[];
metadata: Record<string, unknown>;
source_kind?: string;
};
export type FileConnectorCredential = {
id: string;
label: string;
provider?: string | null;
enabled: boolean;
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
source_path: string;
credential_mode: string;
credential_secret_source?: string | null;
credentials_configured: boolean;
username?: string | null;
policy_sources: FileConnectorPolicySource[];
metadata: Record<string, unknown>;
source_kind?: string;
};
export type FileConnectorCredentialsPayload = {
username?: string | null;
password?: string | null;
token?: string | null;
password_env?: string | null;
token_env?: string | null;
secret_ref?: string | null;
};
export type FileConnectorDiscoveryCandidate = {
endpoint_url: string;
status: "failed" | "usable" | "found" | "credentials_rejected";
message: string;
};
export type FileConnectorDiscoveryPayload = {
provider: FileConnectorProfilePayload["provider"];
endpoint_url: string;
base_path?: string | null;
credential_mode?: FileConnectorProfilePayload["credential_mode"];
credentials?: FileConnectorCredentialsPayload;
metadata?: Record<string, unknown>;
require_valid_credentials?: boolean;
};
export type FileConnectorDiscoveryResponse = {
provider: string;
endpoint_url?: string | null;
base_path?: string | null;
status: "usable" | "found" | "credentials_rejected" | "not_found" | "unsupported";
message: string;
candidates: FileConnectorDiscoveryCandidate[];
metadata: Record<string, unknown>;
};
export type FileConnectorProfilePayload = {
id: string;
label: string;
provider: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic";
endpoint_url?: string | null;
base_path?: string | null;
enabled?: boolean;
scope_type?: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
credential_profile_id?: string | null;
credential_mode?: "none" | "anonymous" | "basic" | "token" | "secret_ref";
credentials?: FileConnectorCredentialsPayload;
capabilities?: string[];
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type FileConnectorProfileUpdatePayload = Partial<Omit<FileConnectorProfilePayload, "id" | "scope_type" | "scope_id">> & {
clear_password?: boolean;
clear_token?: boolean;
};
export type FileConnectorCredentialPayload = {
id: string;
label: string;
provider?: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic" | null;
enabled?: boolean;
scope_type?: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
credential_mode?: "none" | "anonymous" | "basic" | "token" | "secret_ref";
credentials?: FileConnectorCredentialsPayload;
policy?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export type FileConnectorCredentialUpdatePayload = Partial<Omit<FileConnectorCredentialPayload, "id" | "scope_type" | "scope_id">> & {
clear_password?: boolean;
clear_token?: boolean;
};
export type FileConnectorBrowseItem = {
kind: "library" | "folder" | "file";
name: string;
path: string;
external_id?: string | null;
external_url?: string | null;
size_bytes?: number | null;
content_type?: string | null;
modified_at?: string | null;
etag?: string | null;
metadata: Record<string, unknown>;
};
export type FileConnectorBrowseResponse = {
profile_id: string;
provider: string;
path: string;
library_id?: string | null;
read_only: boolean;
decision: FileConnectorPolicyDecision;
items: FileConnectorBrowseItem[];
};
export type FileConnectorProvider = {
provider: string;
label: string;
protocol: string;
implemented: boolean;
installed: boolean;
browse_supported: boolean;
import_supported: boolean;
optional_dependency?: string | null;
permission_model: string;
sync_strategy: string;
conflict_strategy: string;
preview_strategy: string;
audit_events: string[];
notes?: string | null;
};
export type FileConnectorSpace = {
id: string;
tenant_id: string;
owner_type: "user" | "group";
owner_id: string;
label: string;
connector_profile_id: string;
provider: string;
library_id?: string | null;
remote_path: string;
sync_mode: string;
read_only: boolean;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at?: string | null;
metadata: Record<string, unknown>;
};
export type FileConnectorSettingsDeltaResponse = {
profiles: FileConnectorProfile[];
credentials: FileConnectorCredential[];
spaces: FileConnectorSpace[];
policy?: FileConnectorPolicyResponse | null;
changed_sections?: string[];
deleted: DeltaDeletedItem[];
watermark?: string | null;
has_more: boolean;
full: boolean;
};
export type FileConnectorSpacePayload = {
owner_type?: "user" | "group";
owner_id?: string | null;
label: string;
connector_profile_id: string;
library_id?: string | null;
remote_path?: string;
sync_mode?: "manual";
metadata?: Record<string, unknown>;
};
export type ManagedFile = {
id: string;
tenant_id: string;
@@ -34,13 +286,43 @@ export type ManagedFile = {
deleted_at?: string | null;
audit_relevant: boolean;
metadata?: Record<string, unknown> | null;
source_provenance?: FileSourceProvenance | null;
source_revision?: string | null;
shares?: FileShare[];
};
export type FileListResponse = { files: ManagedFile[] };
export type FileSpacesResponse = { spaces: FileSpace[] };
export type FileUploadResponse = { files: ManagedFile[] };
export type FileUploadProgress = { loaded: number; total?: number; percentage: number | null };
export type FileListResponse = {files: ManagedFile[];cursor?: string | null;next_cursor?: string | null;watermark?: string | null;};
export type FileDeltaDeletedItem = {id: string;resource_type?: string | null;revision?: string | null;deleted_at?: string | null;};
export type FileDeltaResponse = {
files: ManagedFile[];
folders: FileFolder[];
deleted: FileDeltaDeletedItem[];
watermark?: string | null;
has_more: boolean;
full: boolean;
};
export type ManagedFileSnapshotResponse = {files: ManagedFile[];folders: FileFolder[];watermark?: string | null;};
export type FileSpacesResponse = {spaces: FileSpace[];};
export type FileUploadResponse = {files: ManagedFile[];};
export type FileUploadProgress = {loaded: number;total?: number;percentage: number | null;};
export type FileConnectorFilePayload = {
library_id: string;
path: string;
owner_type: "user" | "group";
owner_id?: string | null;
target_folder?: string | null;
target_path?: string | null;
campaign_id?: string | null;
conflict_strategy?: ConflictStrategy;
source_revision?: string | null;
metadata?: Record<string, unknown>;
};
export type FileConnectorSyncResponse = {
file: ManagedFile;
action: "created" | "updated" | "unchanged";
previous_version_id?: string | null;
current_version_id: string;
};
export type FileFolder = {
id: string;
tenant_id: string;
@@ -51,16 +333,18 @@ export type FileFolder = {
updated_at: string;
deleted_at?: string | null;
};
export type FileFoldersResponse = { folders: FileFolder[] };
export type FolderDeleteResponse = { deleted_folders: number; deleted_files: number };
export type BulkDeleteResponse = { deleted_count: number };
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 FileFoldersResponse = {folders: FileFolder[];cursor?: string | null;next_cursor?: string | null;watermark?: string | null;};
const DEFAULT_MANAGED_FILE_WINDOW_SIZE = 500;
export type FolderDeleteResponse = {deleted_folders: number;deleted_files: number;};
export type BulkDeleteResponse = {deleted_count: number;};
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 ConflictResolution = { target_path: string; action: ConflictAction; new_path?: string };
export type ConflictResolution = {target_path: string;action: ConflictAction;new_path?: string;};
export type PatternResolveResponse = {
patterns: { pattern: string; matches: ManagedFile[] }[];
patterns: {pattern: string;matches: ManagedFile[];}[];
unmatched: ManagedFile[];
};
@@ -70,50 +354,141 @@ export function listFileSpaces(settings: ApiSettings): Promise<FileSpacesRespons
}
export function listFolders(settings: ApiSettings, params: { owner_type: "user" | "group"; owner_id: string }): Promise<FileFoldersResponse> {
export function listFolders(settings: ApiSettings, params: {owner_type: "user" | "group";owner_id: string;page_size?: number;cursor?: string | null;}): Promise<FileFoldersResponse> {
const search = new URLSearchParams();
search.set("owner_type", params.owner_type);
search.set("owner_id", params.owner_id);
if (params.page_size) search.set("page_size", String(params.page_size));
if (params.cursor) search.set("cursor", params.cursor);
return apiFetch<FileFoldersResponse>(settings, `/api/v1/files/folders?${search.toString()}`);
}
export function createFolder(
settings: ApiSettings,
payload: { owner_type: "user" | "group"; owner_id: string; path: string }
): Promise<FileFolder> {
settings: ApiSettings,
payload: {owner_type: "user" | "group";owner_id: string;path: string;})
: Promise<FileFolder> {
return apiFetch<FileFolder>(settings, "/api/v1/files/folders", { method: "POST", body: JSON.stringify(payload) });
}
export function deleteFolder(
settings: ApiSettings,
payload: { owner_type: "user" | "group"; owner_id: string; path: string; recursive?: boolean }
): Promise<FolderDeleteResponse> {
settings: ApiSettings,
payload: {owner_type: "user" | "group";owner_id: string;path: string;recursive?: boolean;})
: Promise<FolderDeleteResponse> {
return apiFetch<FolderDeleteResponse>(settings, "/api/v1/files/folders/delete", { method: "POST", body: JSON.stringify({ recursive: true, ...payload }) });
}
export function listFiles(settings: ApiSettings, params: { owner_type?: string; owner_id?: string; campaign_id?: string; path_prefix?: string } = {}): Promise<FileListResponse> {
export function listFiles(settings: ApiSettings, params: {owner_type?: string;owner_id?: string;campaign_id?: string;path_prefix?: string;page_size?: number;cursor?: string | null;} = {}): Promise<FileListResponse> {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value) search.set(key, value);
if (value) search.set(key, String(value));
}
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileListResponse>(settings, `/api/v1/files${suffix}`);
}
export async function uploadFiles(
settings: ApiSettings,
files: File[],
options: {
owner_type: "user" | "group";
owner_id: string;
path?: string;
campaign_id?: string;
unpack_zip?: boolean;
conflict_strategy?: ConflictStrategy;
conflict_resolutions?: ConflictResolution[];
onProgress?: (progress: FileUploadProgress) => void;
export function listFilesDelta(
settings: ApiSettings,
params: {owner_type?: string;owner_id?: string;campaign_id?: string;path_prefix?: string;since?: string;limit?: number;} = {})
: Promise<FileDeltaResponse> {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") search.set(key, String(value));
}
): Promise<FileUploadResponse> {
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileDeltaResponse>(settings, `/api/v1/files/delta${suffix}`);
}
function applyDeltaToSnapshot(
files: ManagedFile[],
folders: FileFolder[],
response: FileDeltaResponse)
: {files: ManagedFile[];folders: FileFolder[];} {
if (response.full) return { files: response.files, folders: response.folders };
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));
const filesById = new Map(files.map((file) => [file.id, file]));
const foldersById = new Map(folders.map((folder) => [folder.id, folder]));
deletedFileIds.forEach((id) => filesById.delete(id));
deletedFolderIds.forEach((id) => foldersById.delete(id));
response.files.forEach((file) => filesById.set(file.id, file));
response.folders.forEach((folder) => foldersById.set(folder.id, folder));
return { files: Array.from(filesById.values()), folders: Array.from(foldersById.values()) };
}
export async function listManagedFileSnapshot(
settings: ApiSettings,
params: {owner_type: "user" | "group";owner_id: string;path_prefix?: string;page_size?: number;})
: Promise<ManagedFileSnapshotResponse> {
const pageSize = params.page_size ?? DEFAULT_MANAGED_FILE_WINDOW_SIZE;
let watermark: string | null | undefined = null;
let folderCursor: string | null | undefined = null;
let folders: FileFolder[] = [];
do {
const response = await listFolders(settings, {
owner_type: params.owner_type,
owner_id: params.owner_id,
page_size: pageSize,
cursor: folderCursor
});
watermark = watermark || response.watermark;
folders = folders.concat(response.folders);
folderCursor = response.next_cursor;
} while (folderCursor);
let fileCursor: string | null | undefined = null;
let files: ManagedFile[] = [];
do {
const response = await listFiles(settings, {
owner_type: params.owner_type,
owner_id: params.owner_id,
path_prefix: params.path_prefix,
page_size: pageSize,
cursor: fileCursor
});
watermark = watermark || response.watermark;
files = files.concat(response.files);
fileCursor = response.next_cursor;
} while (fileCursor);
if (watermark) {
let since = watermark;
let response: FileDeltaResponse | null = null;
do {
response = await listFilesDelta(settings, {
owner_type: params.owner_type,
owner_id: params.owner_id,
path_prefix: params.path_prefix,
since,
limit: pageSize
});
const snapshot = applyDeltaToSnapshot(files, folders, response);
files = snapshot.files;
folders = snapshot.folders;
since = response.watermark || "";
} while (response.has_more && response.watermark);
watermark = since || watermark;
}
return { files, folders, watermark };
}
export async function uploadFiles(
settings: ApiSettings,
files: File[],
options: {
owner_type: "user" | "group";
owner_id: string;
path?: string;
campaign_id?: string;
unpack_zip?: boolean;
conflict_strategy?: ConflictStrategy;
conflict_resolutions?: ConflictResolution[];
source_provenance?: FileSourceProvenance;
source_revision?: string;
connector_policy_sources?: FileConnectorPolicySource[];
onProgress?: (progress: FileUploadProgress) => void;
})
: Promise<FileUploadResponse> {
const form = new FormData();
files.forEach((file) => form.append("files", file));
form.append("owner_type", options.owner_type);
@@ -123,15 +498,18 @@ export async function uploadFiles(
if (options.unpack_zip) form.append("unpack_zip", "true");
if (options.conflict_strategy) form.append("conflict_strategy", options.conflict_strategy);
if (options.conflict_resolutions?.length) form.append("conflict_resolutions_json", JSON.stringify(options.conflict_resolutions));
if (options.source_provenance) form.append("source_provenance_json", JSON.stringify(options.source_provenance));
if (options.source_revision) form.append("source_revision", options.source_revision);
if (options.connector_policy_sources?.length) form.append("connector_policy_json", JSON.stringify({ sources: options.connector_policy_sources }));
if (options.onProgress) return uploadFilesWithProgress(settings, form, options.onProgress);
return apiFetch<FileUploadResponse>(settings, "/api/v1/files/upload", { method: "POST", body: form });
}
function uploadFilesWithProgress(
settings: ApiSettings,
form: FormData,
onProgress: (progress: FileUploadProgress) => void
): Promise<FileUploadResponse> {
settings: ApiSettings,
form: FormData,
onProgress: (progress: FileUploadProgress) => void)
: Promise<FileUploadResponse> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl(settings, "/api/v1/files/upload"));
@@ -145,12 +523,12 @@ function uploadFilesWithProgress(
onProgress({
loaded: event.loaded,
total,
percentage: total && total > 0 ? Math.round((event.loaded / total) * 100) : null
percentage: total && total > 0 ? Math.round(event.loaded / total * 100) : null
});
};
xhr.onerror = () => reject(new Error("Upload failed because the network request could not be completed."));
xhr.onabort = () => reject(new Error("Upload was cancelled."));
xhr.onerror = () => reject(new Error("i18n:govoplan-files.upload_failed_because_the_network_request_could_.360a5ab3"));
xhr.onabort = () => reject(new Error("i18n:govoplan-files.upload_was_cancelled.5bab0f9b"));
xhr.onload = () => {
const responseText = xhr.responseText || "";
if (xhr.status < 200 || xhr.status >= 300) {
@@ -179,7 +557,7 @@ export function bulkDeleteFiles(settings: ApiSettings, fileIds: string[]): Promi
return apiFetch<BulkDeleteResponse>(settings, "/api/v1/files/bulk-delete", { method: "POST", body: JSON.stringify({ file_ids: fileIds }) });
}
export type FileBulkShareResponse = { shares: FileShare[]; shared_count: number };
export type FileBulkShareResponse = {shares: FileShare[];shared_count: number;};
export function shareFilesWithTarget(settings: ApiSettings, fileIds: string[], target: FilesManagedFileLinkTarget): Promise<FileBulkShareResponse> {
return apiFetch<FileBulkShareResponse>(settings, "/api/v1/files/bulk-shares", {
@@ -188,6 +566,231 @@ export function shareFilesWithTarget(settings: ApiSettings, fileIds: string[], t
});
}
export function evaluateFileConnectorPolicy(
settings: ApiSettings,
payload: {source_provenance: FileSourceProvenance;operation?: string;policy_sources?: FileConnectorPolicySource[];})
: Promise<{decision: FileConnectorPolicyDecision;}> {
return apiFetch<{decision: FileConnectorPolicyDecision;}>(settings, "/api/v1/files/connector-policy/evaluate", {
method: "POST",
body: JSON.stringify({ operation: "access", policy_sources: [], ...payload })
});
}
export function getFileConnectorPolicy(
settings: ApiSettings,
scopeType: "system" | "tenant" | "user" | "group" | "campaign",
scopeId?: string | null)
: Promise<FileConnectorPolicyResponse> {
const search = new URLSearchParams();
if (scopeId) search.set("scope_id", scopeId);
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorPolicyResponse>(settings, `/api/v1/files/connectors/policies/${encodeURIComponent(scopeType)}${suffix}`);
}
export function fetchFileConnectorSettingsDelta(
settings: ApiSettings,
params: {
scope_type?: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
provider?: string;
campaign_id?: string | null;
include_disabled?: boolean;
include_inactive?: boolean;
owner_type?: "user" | "group";
owner_id?: string;
since?: string | null;
limit?: number;
} = {})
: Promise<FileConnectorSettingsDeltaResponse> {
const search = new URLSearchParams();
if (params.scope_type) search.set("scope_type", params.scope_type);
if (params.scope_id) search.set("scope_id", params.scope_id);
if (params.provider) search.set("provider", params.provider);
if (params.campaign_id) search.set("campaign_id", params.campaign_id);
if (params.include_disabled) search.set("include_disabled", "true");
if (params.include_inactive) search.set("include_inactive", "true");
if (params.owner_type) search.set("owner_type", params.owner_type);
if (params.owner_id) search.set("owner_id", params.owner_id);
if (params.since) search.set("since", params.since);
if (params.limit) search.set("limit", String(params.limit));
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorSettingsDeltaResponse>(settings, `/api/v1/files/connectors/settings/delta${suffix}`);
}
export function updateFileConnectorPolicy(
settings: ApiSettings,
scopeType: "system" | "tenant" | "user" | "group" | "campaign",
policy: Record<string, unknown>,
scopeId?: string | null)
: Promise<FileConnectorPolicyResponse> {
const search = new URLSearchParams();
if (scopeId) search.set("scope_id", scopeId);
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorPolicyResponse>(settings, `/api/v1/files/connectors/policies/${encodeURIComponent(scopeType)}${suffix}`, {
method: "PUT",
body: JSON.stringify({ policy })
});
}
export function listFileConnectorProfiles(
settings: ApiSettings,
params: {provider?: string;campaign_id?: string;include_disabled?: boolean;} = {})
: Promise<{profiles: FileConnectorProfile[];}> {
const search = new URLSearchParams();
if (params.provider) search.set("provider", params.provider);
if (params.campaign_id) search.set("campaign_id", params.campaign_id);
if (params.include_disabled) search.set("include_disabled", "true");
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<{profiles: FileConnectorProfile[];}>(settings, `/api/v1/files/connectors/profiles${suffix}`);
}
export function listFileConnectorProviders(settings: ApiSettings): Promise<{providers: FileConnectorProvider[];}> {
return apiFetch<{providers: FileConnectorProvider[];}>(settings, "/api/v1/files/connectors/providers");
}
export function discoverFileConnectorEndpoint(settings: ApiSettings, payload: FileConnectorDiscoveryPayload): Promise<FileConnectorDiscoveryResponse> {
return apiFetch<FileConnectorDiscoveryResponse>(settings, "/api/v1/files/connectors/discover", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listFileConnectorCredentials(
settings: ApiSettings,
params: {provider?: string;include_disabled?: boolean;} = {})
: Promise<{credentials: FileConnectorCredential[];}> {
const search = new URLSearchParams();
if (params.provider) search.set("provider", params.provider);
if (params.include_disabled) search.set("include_disabled", "true");
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<{credentials: FileConnectorCredential[];}>(settings, `/api/v1/files/connectors/credentials${suffix}`);
}
export function createFileConnectorCredential(settings: ApiSettings, payload: FileConnectorCredentialPayload): Promise<FileConnectorCredential> {
return apiFetch<FileConnectorCredential>(settings, "/api/v1/files/connectors/credentials", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function updateFileConnectorCredential(
settings: ApiSettings,
credentialId: string,
payload: FileConnectorCredentialUpdatePayload)
: Promise<FileConnectorCredential> {
return apiFetch<FileConnectorCredential>(settings, `/api/v1/files/connectors/credentials/${encodeURIComponent(credentialId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export function deactivateFileConnectorCredential(settings: ApiSettings, credentialId: string): Promise<FileConnectorCredential> {
return apiFetch<FileConnectorCredential>(settings, `/api/v1/files/connectors/credentials/${encodeURIComponent(credentialId)}`, { method: "DELETE" });
}
export function listFileConnectorSpaces(
settings: ApiSettings,
params: {owner_type?: "user" | "group";owner_id?: string;include_inactive?: boolean;} = {})
: Promise<{spaces: FileConnectorSpace[];}> {
const search = new URLSearchParams();
if (params.owner_type) search.set("owner_type", params.owner_type);
if (params.owner_id) search.set("owner_id", params.owner_id);
if (params.include_inactive) search.set("include_inactive", "true");
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<{spaces: FileConnectorSpace[];}>(settings, `/api/v1/files/connector-spaces${suffix}`);
}
export function createFileConnectorSpace(settings: ApiSettings, payload: FileConnectorSpacePayload): Promise<FileConnectorSpace> {
return apiFetch<FileConnectorSpace>(settings, "/api/v1/files/connector-spaces", {
method: "POST",
body: JSON.stringify({ owner_type: "user", remote_path: "", sync_mode: "manual", metadata: {}, ...payload })
});
}
export function updateFileConnectorSpace(
settings: ApiSettings,
spaceId: string,
payload: Partial<Omit<FileConnectorSpacePayload, "connector_profile_id" | "owner_type" | "owner_id">> & {is_active?: boolean;})
: Promise<FileConnectorSpace> {
return apiFetch<FileConnectorSpace>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export function deleteFileConnectorSpace(settings: ApiSettings, spaceId: string): Promise<FileConnectorSpace> {
return apiFetch<FileConnectorSpace>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, { method: "DELETE" });
}
export function getFileConnectorProfile(
settings: ApiSettings,
profileId: string,
params: {campaign_id?: string;include_disabled?: boolean;} = {})
: Promise<FileConnectorProfile> {
const search = new URLSearchParams();
if (params.campaign_id) search.set("campaign_id", params.campaign_id);
if (params.include_disabled) search.set("include_disabled", "true");
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorProfile>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}${suffix}`);
}
export function createFileConnectorProfile(settings: ApiSettings, payload: FileConnectorProfilePayload): Promise<FileConnectorProfile> {
return apiFetch<FileConnectorProfile>(settings, "/api/v1/files/connectors/profiles", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function updateFileConnectorProfile(
settings: ApiSettings,
profileId: string,
payload: FileConnectorProfileUpdatePayload)
: Promise<FileConnectorProfile> {
return apiFetch<FileConnectorProfile>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export function deactivateFileConnectorProfile(settings: ApiSettings, profileId: string): Promise<FileConnectorProfile> {
return apiFetch<FileConnectorProfile>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}`, { method: "DELETE" });
}
export function browseFileConnectorProfile(
settings: ApiSettings,
profileId: string,
params: {path?: string;library_id?: string;campaign_id?: string;} = {})
: Promise<FileConnectorBrowseResponse> {
const search = new URLSearchParams();
if (params.path) search.set("path", params.path);
if (params.library_id) search.set("library_id", params.library_id);
if (params.campaign_id) search.set("campaign_id", params.campaign_id);
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorBrowseResponse>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/browse${suffix}`);
}
export function importFileConnectorFile(
settings: ApiSettings,
profileId: string,
payload: FileConnectorFilePayload)
: Promise<FileUploadResponse> {
return apiFetch<FileUploadResponse>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/import`, {
method: "POST",
body: JSON.stringify({ conflict_strategy: "reject", metadata: {}, ...payload })
});
}
export function syncFileConnectorFile(
settings: ApiSettings,
profileId: string,
payload: FileConnectorFilePayload)
: Promise<FileConnectorSyncResponse> {
return apiFetch<FileConnectorSyncResponse>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/sync`, {
method: "POST",
body: JSON.stringify({ conflict_strategy: "rename", metadata: {}, ...payload })
});
}
export function shareFilesWithCampaign(settings: ApiSettings, fileIds: string[], campaignId: string): Promise<FileBulkShareResponse> {
return shareFilesWithTarget(settings, fileIds, { type: "campaign", id: campaignId, label: "campaign" });
}
@@ -195,14 +798,14 @@ export function shareFilesWithCampaign(settings: ApiSettings, fileIds: string[],
export async function shareFileWithCampaign(settings: ApiSettings, fileId: string, campaignId: string): Promise<FileShare> {
const response = await shareFilesWithCampaign(settings, [fileId], campaignId);
const share = response.shares[0];
if (!share) throw new Error("File share was not created.");
if (!share) throw new Error("i18n:govoplan-files.file_share_was_not_created.033ac476");
return share;
}
export function bulkRenameFiles(
settings: ApiSettings,
payload: { file_ids: string[]; folder_paths?: string[]; owner_type?: "user" | "group"; owner_id?: string; mode: "direct" | "prefix" | "suffix" | "replace"; new_name?: string; find?: string; replacement?: string; prefix?: string; suffix?: string; recursive?: boolean; dry_run?: boolean }
): Promise<RenameResponse> {
settings: ApiSettings,
payload: {file_ids: string[];folder_paths?: string[];owner_type: "user" | "group";owner_id: string;mode: "direct" | "prefix" | "suffix" | "replace";new_name?: string;find?: string;replacement?: string;prefix?: string;suffix?: string;recursive?: boolean;dry_run?: boolean;})
: Promise<RenameResponse> {
return apiFetch<RenameResponse>(settings, "/api/v1/files/bulk-rename", {
method: "POST",
body: JSON.stringify({ replacement: "", prefix: "", suffix: "", dry_run: true, ...payload })
@@ -210,27 +813,27 @@ export function bulkRenameFiles(
}
export function resolveFilePatterns(
settings: ApiSettings,
payload: { patterns: string[]; owner_type?: "user" | "group"; owner_id?: string; campaign_id?: string; path_prefix?: string; include_unmatched?: boolean; case_sensitive?: boolean }
): Promise<PatternResolveResponse> {
settings: ApiSettings,
payload: {patterns: string[];owner_type?: "user" | "group";owner_id?: string;campaign_id?: string;path_prefix?: string;include_unmatched?: boolean;case_sensitive?: boolean;})
: Promise<PatternResolveResponse> {
return apiFetch<PatternResolveResponse>(settings, "/api/v1/files/resolve-patterns", { method: "POST", body: JSON.stringify(payload) });
}
export function transferFiles(
settings: ApiSettings,
payload: {
operation: "move" | "copy";
file_ids: string[];
folder_paths: string[];
source_owner_type: "user" | "group";
source_owner_id: string;
target_owner_type: "user" | "group";
target_owner_id: string;
target_folder: string;
conflict_strategy?: ConflictStrategy;
conflict_resolutions?: ConflictResolution[];
}
): Promise<TransferResponse> {
settings: ApiSettings,
payload: {
operation: "move" | "copy";
file_ids: string[];
folder_paths: string[];
source_owner_type: "user" | "group";
source_owner_id: string;
target_owner_type: "user" | "group";
target_owner_id: string;
target_folder: string;
conflict_strategy?: ConflictStrategy;
conflict_resolutions?: ConflictResolution[];
})
: Promise<TransferResponse> {
return apiFetch<TransferResponse>(settings, "/api/v1/files/transfer", { method: "POST", body: JSON.stringify(payload) });
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
import { Copy, Download, FolderOpen, Home, MoveRight, Plus, Trash2, UploadCloud } from "lucide-react";
import { Button, Dialog, ExplorerTree, type ExplorerTreeNodeContext } from "@govoplan/core-webui";
import { Button, Dialog, ExplorerTree, type ExplorerTreeNodeContext, i18nMessage } from "@govoplan/core-webui";
import type { ConflictAction, FileSpace, RenameResponse } from "../../../api/files";
import type { ConflictDialogState, FileActionTarget, FileConflictItem, FolderNode, ContextMenuState } from "../types";
import { isPathUnderOrSame, normalizeFolder, treeNodeKey } from "../utils/fileManagerUtils";
@@ -11,28 +11,28 @@ export function TransferFolderSelector({
selectedFolder,
onSelect,
disabled
}: {
space: FileSpace | null;
nodes: FolderNode[];
selectedFolder: string;
onSelect: (folderPath: string) => void;
disabled?: boolean;
}) {
}: {space: FileSpace | null;nodes: FolderNode[];selectedFolder: string;onSelect: (folderPath: string) => void;disabled?: boolean;}) {
return (
<div className="file-folder-selector" role="tree" aria-label="Destination folder">
<div className="file-folder-selector" role="tree" aria-label="i18n:govoplan-files.destination_folder.f8ccb63b">
<button
type="button"
className={`file-folder-selector-node ${selectedFolder === "" ? "is-selected" : ""}`}
onClick={() => onSelect("")}
disabled={disabled || !space}
>
disabled={disabled || !space}>
<Home size={15} aria-hidden="true" />
<span>{space?.label || "Root"}</span>
<span>{space?.label || "i18n:govoplan-files.root.e96857c5"}</span>
</button>
{nodes.length === 0 && <span className="muted small-text file-folder-selector-empty">No folders yet. Choose the root folder.</span>}
{nodes.length === 0 && <span className="muted small-text file-folder-selector-empty">i18n:govoplan-files.no_folders_yet_choose_the_root_folder.6430304d</span>}
<TransferFolderSelectorNodes nodes={nodes} selectedFolder={selectedFolder} onSelect={onSelect} disabled={disabled} />
</div>
);
</div>);
}
export function TransferFolderSelectorNodes({
@@ -41,33 +41,33 @@ export function TransferFolderSelectorNodes({
onSelect,
disabled,
depth = 1
}: {
nodes: FolderNode[];
selectedFolder: string;
onSelect: (folderPath: string) => void;
disabled?: boolean;
depth?: number;
}) {
}: {nodes: FolderNode[];selectedFolder: string;onSelect: (folderPath: string) => void;disabled?: boolean;depth?: number;}) {
if (nodes.length === 0) return null;
return (
<div className="file-folder-selector-children">
{nodes.map((node) => (
<div key={node.path}>
{nodes.map((node) =>
<div key={node.path}>
<button
type="button"
className={`file-folder-selector-node ${selectedFolder === node.path ? "is-selected" : ""}`}
style={{ paddingLeft: `${Math.min(depth * 16 + 10, 74)}px` }}
onClick={() => onSelect(node.path)}
disabled={disabled}
>
type="button"
className={`file-folder-selector-node ${selectedFolder === node.path ? "is-selected" : ""}`}
style={{ paddingLeft: `${Math.min(depth * 16 + 10, 74)}px` }}
onClick={() => onSelect(node.path)}
disabled={disabled}>
<FolderOpen size={15} aria-hidden="true" />
<span>{node.name}</span>
</button>
<TransferFolderSelectorNodes nodes={node.children} selectedFolder={selectedFolder} onSelect={onSelect} disabled={disabled} depth={depth + 1} />
</div>
))}
</div>
);
)}
</div>);
}
export function FolderTree({
@@ -90,27 +90,27 @@ export function FolderTree({
contextMenuEnabled = true,
disabled,
depth = 1
}: {
nodes: FolderNode[];
activeSpaceId: string;
spaceId: string;
currentFolder: string;
dropTargetKey?: string;
expandedKeys: Set<string>;
onOpen: (spaceId: string, path: string) => void;
onToggle: (spaceId: string, path: string) => void;
onContextMenu?: (event: ReactMouseEvent<HTMLElement>, spaceId: string, path: string) => void;
onDragOverTarget?: (event: ReactDragEvent<HTMLElement>, target: FileActionTarget) => void;
onDropOnTarget?: (event: ReactDragEvent<HTMLElement>, target: FileActionTarget) => Promise<void>;
onClearDropState?: () => void;
onRequestDragExpand?: (spaceId: string, path: string) => void;
onDragStartFolder?: (spaceId: string, path: string, event: ReactDragEvent<HTMLElement>) => void;
onDragEndFolder?: () => void;
dragDropEnabled?: boolean;
contextMenuEnabled?: boolean;
disabled?: boolean;
depth?: number;
}) {
}: {nodes: FolderNode[];activeSpaceId: string;spaceId: string;currentFolder: string;dropTargetKey?: string;expandedKeys: Set<string>;onOpen: (spaceId: string, path: string) => void;onToggle: (spaceId: string, path: string) => void;onContextMenu?: (event: ReactMouseEvent<HTMLElement>, spaceId: string, path: string) => void;onDragOverTarget?: (event: ReactDragEvent<HTMLElement>, target: FileActionTarget) => void;onDropOnTarget?: (event: ReactDragEvent<HTMLElement>, target: FileActionTarget) => Promise<void>;onClearDropState?: () => void;onRequestDragExpand?: (spaceId: string, path: string) => void;onDragStartFolder?: (spaceId: string, path: string, event: ReactDragEvent<HTMLElement>) => void;onDragEndFolder?: () => void;dragDropEnabled?: boolean;contextMenuEnabled?: boolean;disabled?: boolean;depth?: number;}) {
return (
<ExplorerTree
nodes={nodes}
@@ -138,9 +138,9 @@ export function FolderTree({
if (context.hasChildren && !context.expanded) onRequestDragExpand?.(spaceId, node.path);
} : undefined}
onDragLeave={dragDropEnabled && onClearDropState ? () => onClearDropState() : undefined}
onDrop={dragDropEnabled && onDropOnTarget ? (event, node) => void onDropOnTarget(event, { spaceId, folderPath: node.path }) : undefined}
/>
);
onDrop={dragDropEnabled && onDropOnTarget ? (event, node) => void onDropOnTarget(event, { spaceId, folderPath: node.path }) : undefined} />);
}
export function RenamePreviewList({
@@ -151,23 +151,23 @@ export function RenamePreviewList({
visibleCount,
onShowMore,
onShowFewer
}: {
response: RenameResponse;
selectedFileIds: Set<string>;
selectedFolderPaths: Set<string>;
recursive: boolean;
visibleCount: number;
onShowMore: () => void;
onShowFewer: () => void;
}) {
}: {response: RenameResponse;selectedFileIds: Set<string>;selectedFolderPaths: Set<string>;recursive: boolean;visibleCount: number;onShowMore: () => void;onShowFewer: () => void;}) {
const selectedFolderRoots = Array.from(selectedFolderPaths).map(normalizeFolder);
const hiddenNonRecursiveItems = !recursive
? response.items.filter((item) => {
if (item.kind === "file" && selectedFileIds.has(item.id)) return false;
if (item.kind === "folder" && selectedFolderRoots.includes(normalizeFolder(item.old_path))) return false;
return selectedFolderRoots.some((folder) => isPathUnderOrSame(item.old_path, folder));
}).length
: 0;
const hiddenNonRecursiveItems = !recursive ?
response.items.filter((item) => {
if (item.kind === "file" && selectedFileIds.has(item.id)) return false;
if (item.kind === "folder" && selectedFolderRoots.includes(normalizeFolder(item.old_path))) return false;
return selectedFolderRoots.some((folder) => isPathUnderOrSame(item.old_path, folder));
}).length :
0;
const visibleItems = response.items.filter((item) => {
if (recursive) return true;
if (item.kind === "file") {
@@ -182,20 +182,20 @@ export function RenamePreviewList({
return (
<div className="rename-preview-panel">
<div className="placeholder-stack rename-preview-list">
{hiddenNonRecursiveItems > 0 && <span className="muted">{hiddenNonRecursiveItems} contained path(s) will move with the selected folder(s), but their own names will stay unchanged.</span>}
{hiddenNonRecursiveItems > 0 && <span className="muted">{hiddenNonRecursiveItems} i18n:govoplan-files.contained_path_s_will_move_with_the_selected_fol.b786f1a1</span>}
{shownItems.map((item) => <span key={`${item.kind}:${item.id}:${item.old_path}`}><code>{item.old_path}</code> <code>{item.new_path}</code></span>)}
{remaining > 0 && (
<button type="button" className="rename-preview-more" onClick={onShowMore}> and {remaining} more show next {Math.min(20, remaining)}</button>
)}
{shownItems.length > 20 && (
<button type="button" className="rename-preview-more" onClick={onShowFewer}>Show fewer</button>
)}
{remaining > 0 &&
<button type="button" className="rename-preview-more" onClick={onShowMore}>i18n:govoplan-files.and.a0d93385 {remaining} i18n:govoplan-files.more_show_next.f1912318 {Math.min(20, remaining)}</button>
}
{shownItems.length > 20 &&
<button type="button" className="rename-preview-more" onClick={onShowFewer}>i18n:govoplan-files.show_fewer.d94dfbe5</button>
}
</div>
</div>
);
</div>);
}
export function FileDialog({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) {
export function FileDialog({ title, onClose, children }: {title: string;onClose: () => void;children: ReactNode;}) {
return (
<Dialog
open
@@ -205,11 +205,11 @@ export function FileDialog({ title, onClose, children }: { title: string; onClos
className="file-dialog"
headerClassName="file-dialog-header"
titleClassName="file-dialog-title"
bodyClassName="file-dialog-body"
>
bodyClassName="file-dialog-body">
{children}
</Dialog>
);
</Dialog>);
}
export function FileContextMenu({
@@ -227,22 +227,22 @@ export function FileContextMenu({
onMove,
onCopy,
onDelete
}: {
menu: ContextMenuState;
hasSelection: boolean;
canCreateFolder: boolean;
canUpload: boolean;
canDownload: boolean;
canOrganize: boolean;
canDelete: boolean;
downloadLabel: string;
onCreateFolder: () => void;
onUpload: () => void;
onDownload: () => void;
onMove: () => void;
onCopy: () => void;
onDelete: () => void;
}) {
}: {menu: ContextMenuState;hasSelection: boolean;canCreateFolder: boolean;canUpload: boolean;canDownload: boolean;canOrganize: boolean;canDelete: boolean;downloadLabel: string;onCreateFolder: () => void;onUpload: () => void;onDownload: () => void;onMove: () => void;onCopy: () => void;onDelete: () => void;}) {
const showNewFolder = true;
const showDelete = menu.target !== "empty";
const viewportWidth = typeof window === "undefined" ? 1024 : window.innerWidth;
@@ -251,19 +251,19 @@ export function FileContextMenu({
const estimatedHeight = 260;
const left = Math.max(8, Math.min(menu.x, viewportWidth - estimatedWidth - 8));
const openUp = menu.y + estimatedHeight > viewportHeight;
const style: CSSProperties = openUp
? { left, bottom: Math.max(8, viewportHeight - menu.y) }
: { left, top: Math.max(8, menu.y) };
const style: CSSProperties = openUp ?
{ left, bottom: Math.max(8, viewportHeight - menu.y) } :
{ left, top: Math.max(8, menu.y) };
return (
<div className="file-context-menu" style={style} role="menu" onClick={(event) => event.stopPropagation()}>
{showNewFolder && <button type="button" role="menuitem" onClick={onCreateFolder} disabled={!canCreateFolder}><Plus size={15} aria-hidden="true" /> New folder</button>}
<button type="button" role="menuitem" onClick={onUpload} disabled={!canUpload}><UploadCloud size={15} aria-hidden="true" /> Upload</button>
{showNewFolder && <button type="button" role="menuitem" onClick={onCreateFolder} disabled={!canCreateFolder}><Plus size={15} aria-hidden="true" /> i18n:govoplan-files.new_folder.a711999b</button>}
<button type="button" role="menuitem" onClick={onUpload} disabled={!canUpload}><UploadCloud size={15} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</button>
<button type="button" role="menuitem" onClick={onDownload} disabled={!hasSelection || !canDownload}><Download size={15} aria-hidden="true" /> {downloadLabel}</button>
<button type="button" role="menuitem" onClick={onMove} disabled={!hasSelection || !canOrganize}><MoveRight size={15} aria-hidden="true" /> Move</button>
<button type="button" role="menuitem" onClick={onCopy} disabled={!hasSelection || !canOrganize}><Copy size={15} aria-hidden="true" /> Copy</button>
{showDelete && <button type="button" role="menuitem" className="danger" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> Delete</button>}
</div>
);
<button type="button" role="menuitem" onClick={onMove} disabled={!hasSelection || !canOrganize}><MoveRight size={15} aria-hidden="true" /> i18n:govoplan-files.move.8a74a26e</button>
<button type="button" role="menuitem" onClick={onCopy} disabled={!hasSelection || !canOrganize}><Copy size={15} aria-hidden="true" /> i18n:govoplan-files.copy.92556c6d</button>
{showDelete && <button type="button" role="menuitem" className="danger" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</button>}
</div>);
}
export function FileConflictDialog({
@@ -276,60 +276,60 @@ export function FileConflictDialog({
onReview,
onApplyReview,
onUpdateItem
}: {
state: ConflictDialogState;
busy: boolean;
onClose: () => void;
onCancel: () => void;
onOverwrite: () => void;
onRename: () => void;
onReview: () => void;
onApplyReview: () => void;
onUpdateItem: (id: string, patch: Partial<FileConflictItem>) => void;
}) {
}: {state: ConflictDialogState;busy: boolean;onClose: () => void;onCancel: () => void;onOverwrite: () => void;onRename: () => void;onReview: () => void;onApplyReview: () => void;onUpdateItem: (id: string, patch: Partial<FileConflictItem>) => void;}) {
return (
<FileDialog title={state.title} onClose={onClose}>
<p className="muted">{state.message}</p>
<div className="file-conflict-summary">
<strong>{state.items.length} conflict(s)</strong>
<span>{state.items.length > 1 ? "Choose the same action for all conflicts or review the target names individually." : "Choose how to resolve this conflict."}</span>
<strong>{state.items.length} i18n:govoplan-files.conflict_s.60cea6d5</strong>
<span>{state.items.length > 1 ? "i18n:govoplan-files.choose_the_same_action_for_all_conflicts_or_revi.319cfe5f" : "i18n:govoplan-files.choose_how_to_resolve_this_conflict.c303fcc6"}</span>
</div>
{state.review && (
<div className="file-conflict-list">
{state.items.map((item) => (
<div className="file-conflict-row" key={item.id}>
{state.review &&
<div className="file-conflict-list">
{state.items.map((item) =>
<div className="file-conflict-row" key={item.id}>
<div>
<strong>{item.label}</strong>
<small>{item.kind === "folder" ? "Folder" : "File"} conflict at <code>{item.targetPath}</code></small>
<small>{item.kind === "folder" ? "i18n:govoplan-files.folder.30baa249" : "i18n:govoplan-files.file.2c3cafa4"} i18n:govoplan-files.conflict_at.0a91052f <code>{item.targetPath}</code></small>
</div>
<select value={item.action} onChange={(event) => onUpdateItem(item.id, { action: event.target.value as ConflictAction })} disabled={busy}>
<option value="overwrite">Overwrite</option>
<option value="rename">Rename</option>
<option value="skip">Skip</option>
<option value="overwrite">i18n:govoplan-files.overwrite.0625c54e</option>
<option value="rename">i18n:govoplan-files.rename.d3f4cb89</option>
<option value="skip">i18n:govoplan-files.skip.3da47453</option>
</select>
<input
value={item.newPath}
onChange={(event) => onUpdateItem(item.id, { newPath: event.target.value })}
disabled={busy || item.action !== "rename"}
aria-label={`New path for ${item.label}`}
/>
value={item.newPath}
onChange={(event) => onUpdateItem(item.id, { newPath: event.target.value })}
disabled={busy || item.action !== "rename"}
aria-label={i18nMessage("i18n:govoplan-files.new_path_for_value.a9a30c1c", { value0: item.label })} />
</div>
))}
)}
</div>
)}
{!state.review && (
<div className="placeholder-stack">
}
{!state.review &&
<div className="placeholder-stack">
{state.items.slice(0, 8).map((item) => <span key={item.id}><code>{item.targetPath}</code></span>)}
{state.items.length > 8 && <span> and {state.items.length - 8} more</span>}
{state.items.length > 8 && <span>i18n:govoplan-files.and.a0d93385 {state.items.length - 8} more</span>}
</div>
)}
}
<div className="button-row compact-actions align-end">
<Button onClick={onCancel} disabled={busy}>Cancel</Button>
<Button onClick={onOverwrite} disabled={busy}>Overwrite all</Button>
<Button onClick={onRename} disabled={busy}>Rename all</Button>
{!state.review && state.items.length > 1 && <Button onClick={onReview} disabled={busy}>Review individually</Button>}
{state.review && <Button variant="primary" onClick={onApplyReview} disabled={busy}>Apply reviewed choices</Button>}
<Button onClick={onCancel} disabled={busy}>i18n:govoplan-files.cancel.77dfd213</Button>
<Button onClick={onOverwrite} disabled={busy}>i18n:govoplan-files.overwrite_all.a76f2d04</Button>
<Button onClick={onRename} disabled={busy}>i18n:govoplan-files.rename_all.1d6b24dc</Button>
{!state.review && state.items.length > 1 && <Button onClick={onReview} disabled={busy}>i18n:govoplan-files.review_individually.19abbe58</Button>}
{state.review && <Button variant="primary" onClick={onApplyReview} disabled={busy}>i18n:govoplan-files.apply_reviewed_choices.bb55f7b3</Button>}
</div>
</FileDialog>
);
</FileDialog>);
}

View File

@@ -3,24 +3,24 @@ import { createPortal } from "react-dom";
import { ArrowDown, ArrowUp, ArrowUpDown, File, Folder, FolderOpen, Home, Link2, Search } from "lucide-react";
import {
listFileSpaces,
listFiles,
listFolders,
listManagedFileSnapshot,
resolveFilePatterns,
shareFilesWithTarget,
type FileFolder,
type FileSpace,
type ManagedFile
} from "../../../api/files";
type ManagedFile } from
"../../../api/files";
import {
Button,
ConfirmDialog,
Dialog,
DismissibleAlert,
LoadingFrame,
type FilesManagedAttachmentSelection,
type FilesManagedFileChooserProps,
type FilesManagedFileLinkTarget,
type FilesManagedFolderSelection
} from "@govoplan/core-webui";
type FilesManagedFolderSelection, i18nMessage } from
"@govoplan/core-webui";
import { useFileTreeState } from "../hooks/useFileTreeState";
import type { FolderNode, SortColumn, SortDirection } from "../types";
import {
@@ -32,8 +32,8 @@ import {
normalizeFolder,
parentFolderPath,
sortExplorerEntries,
treeNodeKey
} from "../utils/fileManagerUtils";
treeNodeKey } from
"../utils/fileManagerUtils";
type ManagedFileChooserMode = FilesManagedFileChooserProps["mode"];
@@ -57,7 +57,7 @@ type ManagedFileSource = {
const MANAGED_FILE_SOURCE_PREFIX = "managed:";
type ChooserExplorerEntry = ReturnType<typeof buildExplorerEntries>[number];
type VirtualFolderTreeRow = { node: FolderNode; depth: number };
type VirtualFolderTreeRow = {node: FolderNode;depth: number;};
export type ManagedFolderSelection = FilesManagedFolderSelection;
export type ManagedAttachmentSelection = FilesManagedAttachmentSelection;
@@ -104,9 +104,9 @@ export default function ManagedFileChooser({
const selectedSpace = spaces.find((item) => item.id === selectedSpaceId) ?? null;
const sourceLocked = mode === "attachment" && Boolean(parsedSource);
const sourceMatchesSpace = selectedSpace
? mode === "folder" || !parsedSource || (selectedSpace.owner_type === parsedSource.ownerType && selectedSpace.owner_id === parsedSource.ownerId)
: false;
const sourceMatchesSpace = selectedSpace ?
mode === "folder" || !parsedSource || selectedSpace.owner_type === parsedSource.ownerType && selectedSpace.owner_id === parsedSource.ownerId :
false;
const effectiveRoot = mode === "attachment" ? normalizedBasePath : "";
const entries = useMemo(
() => buildExplorerEntries(files, folders, currentFolder, false),
@@ -187,22 +187,23 @@ export default function ManagedFileChooser({
setExactSelectionPattern("");
setPatternMatches(null);
setPendingExactFile(null);
void listFileSpaces(settings)
.then((response) => {
if (cancelled) return;
setSpaces(response.spaces);
const sourceSpace = response.spaces.find((item) => sourceMatches(item, parsedSource));
const rememberedSpace = response.spaces.find((item) => item.id === remembered.spaceId);
const firstSpace = sourceSpace ?? rememberedSpace ?? response.spaces[0] ?? null;
setSelectedSpaceId(firstSpace?.id ?? "");
})
.catch((reason: unknown) => {
if (!cancelled) setError(errorMessage(reason));
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
void listFileSpaces(settings).
then((response) => {
if (cancelled) return;
const managedSpaces = response.spaces.filter((space) => space.space_type !== "connector");
setSpaces(managedSpaces);
const sourceSpace = managedSpaces.find((item) => sourceMatches(item, parsedSource));
const rememberedSpace = managedSpaces.find((item) => item.id === remembered.spaceId);
const firstSpace = sourceSpace ?? rememberedSpace ?? managedSpaces[0] ?? null;
setSelectedSpaceId(firstSpace?.id ?? "");
}).
catch((reason: unknown) => {
if (!cancelled) setError(errorMessage(reason));
}).
finally(() => {
if (!cancelled) setLoading(false);
});
return () => {cancelled = true;};
}, [initialPattern, open, parsedSource?.ownerId, parsedSource?.ownerType, settings.apiBaseUrl, settings.apiKey, settings.accessToken, storageKey]);
useEffect(() => {
@@ -214,22 +215,23 @@ export default function ManagedFileChooser({
const rememberedFolder = selectedSpace.id === remembered.spaceId ? normalizeFolder(remembered.folder || "") : "";
const initialFolder = rememberedFolder && isWithinRoot(rememberedFolder, effectiveRoot) ? rememberedFolder : effectiveRoot;
setCurrentFolder(initialFolder || effectiveRoot);
void Promise.all([
listFiles(settings, { owner_type: selectedSpace.owner_type, owner_id: selectedSpace.owner_id, path_prefix: mode === "attachment" ? effectiveRoot || undefined : undefined }),
listFolders(settings, { owner_type: selectedSpace.owner_type, owner_id: selectedSpace.owner_id })
])
.then(([fileResponse, folderResponse]) => {
if (cancelled) return;
setFiles(fileResponse.files);
setFolders(folderResponse.folders);
})
.catch((reason: unknown) => {
if (!cancelled) setError(errorMessage(reason));
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
void listManagedFileSnapshot(settings, {
owner_type: selectedSpace.owner_type,
owner_id: selectedSpace.owner_id,
path_prefix: mode === "attachment" ? effectiveRoot || undefined : undefined
}).
then((response) => {
if (cancelled) return;
setFiles(response.files);
setFolders(response.folders);
}).
catch((reason: unknown) => {
if (!cancelled) setError(errorMessage(reason));
}).
finally(() => {
if (!cancelled) setLoading(false);
});
return () => {cancelled = true;};
}, [effectiveRoot, mode, open, selectedSpace?.id, settings.apiBaseUrl, settings.apiKey, settings.accessToken, storageKey]);
useEffect(() => {
@@ -287,9 +289,9 @@ export default function ManagedFileChooser({
async function shareMatches(matches: ManagedFile[]) {
if (!linkTarget) return;
const fileIds = matches
.filter((file) => !isLinkedToTarget(file, linkTarget))
.map((file) => file.id);
const fileIds = matches.
filter((file) => !isLinkedToTarget(file, linkTarget)).
map((file) => file.id);
const uniqueIds = [...new Set(fileIds)];
if (uniqueIds.length > 0) await shareFilesWithTarget(settings, uniqueIds, linkTarget);
}
@@ -308,7 +310,7 @@ export default function ManagedFileChooser({
return;
}
const matches = patternMatches ?? await previewPattern();
const matches = patternMatches ?? (await previewPattern());
await shareMatches(matches);
const trimmedPattern = patternRef.current.trim();
onSelectAttachment?.({
@@ -329,148 +331,150 @@ export default function ManagedFileChooser({
if (!open || typeof document === "undefined") return null;
const dialog = (
<>
const dialog =
<>
<Dialog
open
title={mode === "folder" ? "Choose managed attachment source" : "Choose managed file or pattern"}
onClose={onClose}
closeDisabled={submitting}
backdropClassName="managed-file-chooser-backdrop"
className="managed-file-chooser-dialog"
bodyClassName="managed-file-chooser-body"
footerClassName="managed-file-chooser-footer"
footer={(
<>
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
open
title={mode === "folder" ? "i18n:govoplan-files.choose_managed_attachment_source.b3709493" : "i18n:govoplan-files.choose_managed_file_or_pattern.bcf8e183"}
onClose={onClose}
closeDisabled={submitting}
backdropClassName="managed-file-chooser-backdrop"
className="managed-file-chooser-dialog"
bodyClassName="managed-file-chooser-body"
footerClassName="managed-file-chooser-footer"
footer={
<>
<Button onClick={onClose} disabled={submitting}>i18n:govoplan-files.cancel.77dfd213</Button>
<Button
variant="primary"
onClick={() => void confirmSelection()}
disabled={loading || submitting || !selectedSpace || !sourceMatchesSpace || (mode === "attachment" && (!parsedSource || !patternHasText))}
>
{submitting ? (linkTarget ? "Linking…" : "Saving…") : mode === "folder" ? "Use this folder" : "Use pattern"}
variant="primary"
onClick={() => void confirmSelection()}
disabled={loading || submitting || !selectedSpace || !sourceMatchesSpace || mode === "attachment" && (!parsedSource || !patternHasText)}>
{submitting ? linkTarget ? "i18n:govoplan-files.linking.6f640897" : "i18n:govoplan-files.saving.56a2285c" : mode === "folder" ? "i18n:govoplan-files.use_this_folder.0e77d6d1" : "i18n:govoplan-files.use_pattern.ce54ba3e"}
</Button>
</>
)}
>
}>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{mode === "attachment" && !parsedSource && (
<DismissibleAlert tone="warning" dismissible={false}>
This attachment base path is not connected to a managed file space. Choose its folder under <strong>Attachments Attachment sources</strong> first.
{mode === "attachment" && !parsedSource &&
<DismissibleAlert tone="warning" dismissible={false}>
i18n:govoplan-files.this_attachment_base_path_is_not_connected_to_a_.3962b48f <strong>i18n:govoplan-files.attachments_attachment_sources.87d92d5b</strong> i18n:govoplan-files.first.49ec0838
</DismissibleAlert>
)}
{mode === "attachment" && parsedSource && !sourceMatchesSpace && !loading && (
<DismissibleAlert tone="danger" dismissible={false}>The configured managed file space is no longer available to this user.</DismissibleAlert>
)}
}
{mode === "attachment" && parsedSource && !sourceMatchesSpace && !loading &&
<DismissibleAlert tone="danger" dismissible={false}>i18n:govoplan-files.the_configured_managed_file_space_is_no_longer_a.5e93b729</DismissibleAlert>
}
<LoadingFrame loading={loading} label="i18n:govoplan-files.loading_connector_files.e5b0871d" className="managed-file-chooser-loading-frame">
<div className="managed-file-chooser-layout" aria-busy={loading}>
<aside className="managed-file-chooser-spaces file-tree-panel" aria-label="File spaces and folders">
<div className="file-tree-heading">Spaces</div>
<aside className="managed-file-chooser-spaces file-tree-panel" aria-label="i18n:govoplan-files.file_spaces_and_folders.443d2056">
<div className="file-tree-heading">i18n:govoplan-files.spaces.9b584b52</div>
<div className="file-tree-list">
{spaces.map((space) => {
const selected = space.id === selectedSpaceId;
const lockedOut = sourceLocked && !sourceMatches(space, parsedSource);
return (
<div className={`file-tree-space ${selected ? "is-active" : ""}`} key={space.id}>
const selected = space.id === selectedSpaceId;
const lockedOut = sourceLocked && !sourceMatches(space, parsedSource);
return (
<div className={`file-tree-space ${selected ? "is-active" : ""}`} key={space.id}>
<button
type="button"
className={`file-tree-node file-tree-root ${selected && currentFolder === effectiveRoot ? "is-active" : ""}`}
onClick={() => {
if (!selected) setSelectedSpaceId(space.id);
else openFolder(effectiveRoot);
}}
disabled={loading || lockedOut}
>
type="button"
className={`file-tree-node file-tree-root ${selected && currentFolder === effectiveRoot ? "is-active" : ""}`}
onClick={() => {
if (!selected) setSelectedSpaceId(space.id);else
openFolder(effectiveRoot);
}}
disabled={loading || lockedOut}>
{space.owner_type === "group" ? <FolderOpen size={15} aria-hidden="true" /> : <Home size={15} aria-hidden="true" />}
<span>{space.label}</span>
</button>
{selected && (
<VirtualChooserFolderTree
nodes={treeNodes}
activeSpaceId={selectedSpaceId}
spaceId={space.id}
currentFolder={currentFolder}
expandedKeys={expandedTreeNodes}
onOpen={(_spaceId, path) => openFolder(path)}
onToggle={toggleTreeFolder}
disabled={loading}
/>
)}
</div>
);
})}
{!loading && spaces.length === 0 && <p className="muted small-note">No accessible file spaces were found.</p>}
{selected &&
<VirtualChooserFolderTree
nodes={treeNodes}
activeSpaceId={selectedSpaceId}
spaceId={space.id}
currentFolder={currentFolder}
expandedKeys={expandedTreeNodes}
onOpen={(_spaceId, path) => openFolder(path)}
onToggle={toggleTreeFolder}
disabled={loading} />
}
</div>);
})}
{!loading && spaces.length === 0 && <p className="muted small-note">i18n:govoplan-files.no_accessible_file_spaces_were_found.abcf5003</p>}
</div>
</aside>
<section className={`managed-file-chooser-browser ${mode === "folder" ? "folder-mode" : "attachment-mode"}`}>
<div className="managed-file-chooser-toolbar">
<div className="managed-file-breadcrumb" aria-label="Current folder">
<div className="managed-file-breadcrumb" aria-label="i18n:govoplan-files.current_folder.5aeab2f0">
<button type="button" onClick={() => openFolder(effectiveRoot)} disabled={loading}>
<Home size={14} aria-hidden="true" /> {selectedSpace?.label || "Files"}
<Home size={14} aria-hidden="true" /> {selectedSpace?.label || "i18n:govoplan-files.files.6ce6c512"}
</button>
{breadcrumbs.map((crumb) => (
<span key={crumb.path}>
{breadcrumbs.map((crumb) =>
<span key={crumb.path}>
<span aria-hidden="true">/</span>
<button type="button" onClick={() => openFolder(crumb.path)} disabled={loading}>{crumb.name}</button>
</span>
))}
)}
</div>
</div>
{mode === "attachment" && (
<ManagedPatternEditor
value={pattern}
effectiveRoot={effectiveRoot}
resolving={resolving}
matchCount={patternMatches?.length ?? null}
previewContext={previewContext}
renderPatternPreview={renderPatternPreview}
onDraftChange={updatePatternInput}
onPreview={() => void previewPattern()}
onBackToFolder={() => setPatternMatches(null)}
/>
)}
{mode === "attachment" &&
<ManagedPatternEditor
value={pattern}
effectiveRoot={effectiveRoot}
resolving={resolving}
matchCount={patternMatches?.length ?? null}
previewContext={previewContext}
renderPatternPreview={renderPatternPreview}
onDraftChange={updatePatternInput}
onPreview={() => void previewPattern()}
onBackToFolder={() => setPatternMatches(null)} />
{patternMatches !== null && mode === "attachment" ? (
<PatternResultList files={patternMatches} linkTarget={linkTarget} onChoose={requestExactFile} />
) : (
<ChooserFileBrowser
mode={mode}
loading={loading}
currentFolder={currentFolder}
effectiveRoot={effectiveRoot}
sortedEntries={sortedEntries}
sortColumn={sortColumn}
sortDirection={sortDirection}
linkTarget={linkTarget}
selectedExactPattern={exactSelectionPattern}
onOpenFolder={openFolder}
onToggleSort={toggleSort}
onRequestExactFile={requestExactFile}
/>
)}
}
{mode === "folder" && (
<p className="muted small-note managed-file-chooser-note">Choose the folder that should act as this campaign attachment source.</p>
)}
{mode === "attachment" && (
<p className="muted small-note managed-file-chooser-note">Clicking a file sets its exact relative path as the pattern. Preview resolves the pattern against the current managed file space.</p>
)}
{patternMatches !== null && mode === "attachment" ?
<PatternResultList files={patternMatches} linkTarget={linkTarget} onChoose={requestExactFile} /> :
<ChooserFileBrowser
mode={mode}
loading={loading}
currentFolder={currentFolder}
effectiveRoot={effectiveRoot}
sortedEntries={sortedEntries}
sortColumn={sortColumn}
sortDirection={sortDirection}
linkTarget={linkTarget}
selectedExactPattern={exactSelectionPattern}
onOpenFolder={openFolder}
onToggleSort={toggleSort}
onRequestExactFile={requestExactFile} />
}
{mode === "folder" &&
<p className="muted small-note managed-file-chooser-note">i18n:govoplan-files.choose_the_folder_that_should_act_as_this_campai.9d0b7ef7</p>
}
{mode === "attachment" &&
<p className="muted small-note managed-file-chooser-note">i18n:govoplan-files.clicking_a_file_sets_its_exact_relative_path_as_.590abd24</p>
}
</section>
</div>
</LoadingFrame>
</Dialog>
<ConfirmDialog
open={Boolean(pendingExactFile)}
title="Replace the current pattern?"
message={pendingExactFile ? `Replace "${patternRef.current.trim()}" with the exact file "${relativePath(pendingExactFile.display_path, effectiveRoot)}"?` : "Replace the current pattern?"}
confirmLabel="Replace pattern"
onConfirm={() => pendingExactFile && applyExactFile(pendingExactFile)}
onCancel={() => setPendingExactFile(null)}
/>
</>
);
open={Boolean(pendingExactFile)}
title="i18n:govoplan-files.replace_the_current_pattern.96f80707"
message={pendingExactFile ? i18nMessage("i18n:govoplan-files.replace_value_with_the_exact_file_value.4a63236f", { value0: patternRef.current.trim(), value1: relativePath(pendingExactFile.display_path, effectiveRoot) }) : "i18n:govoplan-files.replace_the_current_pattern.96f80707"}
confirmLabel="i18n:govoplan-files.replace_pattern.d98e4c92"
onConfirm={() => pendingExactFile && applyExactFile(pendingExactFile)}
onCancel={() => setPendingExactFile(null)} />
</>;
return createPortal(dialog, document.body);
}
@@ -480,26 +484,26 @@ function ChooserSortButton({
label,
activeColumn,
direction,
onSort,
}: {
column: SortColumn;
label: string;
activeColumn: SortColumn;
direction: SortDirection;
onSort: (column: SortColumn) => void;
}) {
onSort
}: {column: SortColumn;label: string;activeColumn: SortColumn;direction: SortDirection;onSort: (column: SortColumn) => void;}) {
const active = activeColumn === column;
const Icon = active ? (direction === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
const Icon = active ? direction === "asc" ? ArrowUp : ArrowDown : ArrowUpDown;
return (
<button
type="button"
className={active ? "is-sorted" : ""}
aria-label={`${label}: ${active ? `sorted ${direction === "asc" ? "ascending" : "descending"}` : "not sorted"}. Activate to sort.`}
onClick={() => onSort(column)}
>
aria-label={i18nMessage("i18n:govoplan-files.value_value_activate_to_sort.3953ba71", { value0: label, value1: active ? `sorted ${direction === "asc" ? "ascending" : "descending"}` : "i18n:govoplan-files.not_sorted.0abc03c5" })}
onClick={() => onSort(column)}>
<span>{label}</span><Icon size={14} aria-hidden="true" />
</button>
);
</button>);
}
function ManagedPatternEditor({
@@ -512,17 +516,17 @@ function ManagedPatternEditor({
onDraftChange,
onPreview,
onBackToFolder
}: {
value: string;
effectiveRoot: string;
resolving: boolean;
matchCount: number | null;
previewContext?: Record<string, string>;
renderPatternPreview?: FilesManagedFileChooserProps["renderPatternPreview"];
onDraftChange: (value: string) => void;
onPreview: () => void;
onBackToFolder: () => void;
}) {
}: {value: string;effectiveRoot: string;resolving: boolean;matchCount: number | null;previewContext?: Record<string, string>;renderPatternPreview?: FilesManagedFileChooserProps["renderPatternPreview"];onDraftChange: (value: string) => void;onPreview: () => void;onBackToFolder: () => void;}) {
const [draft, setDraft] = useState(value);
const renderedPattern = useMemo(() => renderChooserPattern(draft, previewContext, renderPatternPreview), [draft, previewContext, renderPatternPreview]);
const patternIsRendered = Boolean(draft.trim() && renderedPattern && renderedPattern !== draft.trim());
@@ -539,28 +543,28 @@ function ManagedPatternEditor({
return (
<div className="managed-pattern-editor">
<label>
<span>File or pattern relative to <code>{effectiveRoot || "."}</code></span>
<span>i18n:govoplan-files.file_or_pattern_relative_to.0ab4d831 <code>{effectiveRoot || "."}</code></span>
<div className="field-with-action split-field-action">
<input
value={draft}
onChange={(event) => updateDraft(event.target.value)}
onKeyDown={(event) => { if (event.key === "Enter") onPreview(); }}
placeholder="folder/file.pdf or **/*.pdf"
/>
onKeyDown={(event) => {if (event.key === "Enter") onPreview();}}
placeholder="folder/file.pdf or **/*.pdf" />
<Button onClick={onPreview} disabled={resolving || !draft.trim()}>
<Search size={15} aria-hidden="true" /> {resolving ? "Checking..." : "Preview"}
<Search size={15} aria-hidden="true" /> {resolving ? "i18n:govoplan-files.checking.494d0f68" : "i18n:govoplan-files.preview.f1fbb2b4"}
</Button>
</div>
</label>
{matchCount !== null && (
<div className="managed-pattern-summary">
<span>{matchCount} current file{matchCount === 1 ? "" : "s"} match.</span>
<Button variant="ghost" onClick={onBackToFolder}>Back to folder</Button>
{matchCount !== null &&
<div className="managed-pattern-summary">
<span>{matchCount} i18n:govoplan-files.current_file.1fbfcf3d{matchCount === 1 ? "" : "s"} i18n:govoplan-files.match.c79af5c2</span>
<Button variant="ghost" onClick={onBackToFolder}>i18n:govoplan-files.back_to_folder.34ba1ed1</Button>
</div>
)}
{patternIsRendered && <small className="managed-pattern-rendered">Preview resolves to <code>{renderedPattern}</code>.</small>}
</div>
);
}
{patternIsRendered && <small className="managed-pattern-rendered">i18n:govoplan-files.preview_resolves_to.a8d99432 <code>{renderedPattern}</code>.</small>}
</div>);
}
function VirtualChooserFolderTree({
@@ -572,16 +576,16 @@ function VirtualChooserFolderTree({
onOpen,
onToggle,
disabled
}: {
nodes: FolderNode[];
activeSpaceId: string;
spaceId: string;
currentFolder: string;
expandedKeys: Set<string>;
onOpen: (spaceId: string, path: string) => void;
onToggle: (spaceId: string, path: string) => void;
disabled?: boolean;
}) {
}: {nodes: FolderNode[];activeSpaceId: string;spaceId: string;currentFolder: string;expandedKeys: Set<string>;onOpen: (spaceId: string, path: string) => void;onToggle: (spaceId: string, path: string) => void;disabled?: boolean;}) {
const rows = useMemo(() => flattenVisibleFolderTree(nodes, spaceId, expandedKeys), [expandedKeys, nodes, spaceId]);
const resetKey = useMemo(
() => `${spaceId}:${currentFolder}:${rows.length}:${Array.from(expandedKeys).sort().join("|")}`,
@@ -600,12 +604,12 @@ function VirtualChooserFolderTree({
className="managed-file-tree-virtual-list"
ref={virtualRows.viewportRef}
role="tree"
aria-label="Folders"
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}
>
{virtualRows.topSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
)}
aria-label="i18n:govoplan-files.folders.19adc47b"
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}>
{virtualRows.topSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
}
{rows.slice(virtualRows.startIndex, virtualRows.endIndex).map(({ node, depth }, index) => {
const nodeId = treeNodeKey(spaceId, node.path);
const hasChildren = node.children.length > 0;
@@ -619,8 +623,8 @@ function VirtualChooserFolderTree({
style={{ paddingLeft: `${Math.min(depth * 14, 56)}px` }}
role="treeitem"
aria-expanded={hasChildren ? expanded : undefined}
aria-selected={active}
>
aria-selected={active}>
<button
type="button"
className="file-tree-toggle"
@@ -629,26 +633,26 @@ function VirtualChooserFolderTree({
if (hasChildren) onToggle(spaceId, node.path);
}}
disabled={disabled || !hasChildren}
aria-label={`${expanded ? "Collapse" : "Expand"} ${node.name}`}
>
aria-label={i18nMessage("i18n:govoplan-files.value_value.dca59cc0", { value0: expanded ? "i18n:govoplan-files.collapse.9cf188d3" : "i18n:govoplan-files.expand.9869e506", value1: node.name })}>
{expanded ? <FolderOpen size={15} aria-hidden="true" /> : <Folder size={15} aria-hidden="true" />}
</button>
<button
type="button"
className="file-tree-node"
onClick={() => onOpen(spaceId, node.path)}
disabled={disabled}
>
disabled={disabled}>
<span>{node.name}</span>
</button>
</div>
);
</div>);
})}
{virtualRows.bottomSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
)}
</div>
);
{virtualRows.bottomSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
}
</div>);
}
const ChooserFileBrowser = memo(function ChooserFileBrowser({
@@ -664,20 +668,20 @@ const ChooserFileBrowser = memo(function ChooserFileBrowser({
onOpenFolder,
onToggleSort,
onRequestExactFile
}: {
mode: ManagedFileChooserMode;
loading: boolean;
currentFolder: string;
effectiveRoot: string;
sortedEntries: ChooserExplorerEntry[];
sortColumn: SortColumn;
sortDirection: SortDirection;
linkTarget?: FilesManagedFileLinkTarget;
selectedExactPattern: string;
onOpenFolder: (path: string) => void;
onToggleSort: (column: SortColumn) => void;
onRequestExactFile: (file: ManagedFile) => void;
}) {
}: {mode: ManagedFileChooserMode;loading: boolean;currentFolder: string;effectiveRoot: string;sortedEntries: ChooserExplorerEntry[];sortColumn: SortColumn;sortDirection: SortDirection;linkTarget?: FilesManagedFileLinkTarget;selectedExactPattern: string;onOpenFolder: (path: string) => void;onToggleSort: (column: SortColumn) => void;onRequestExactFile: (file: ManagedFile) => void;}) {
const resetKey = `${mode}:${currentFolder}:${sortColumn}:${sortDirection}:${sortedEntries.length}`;
const virtualRows = useFixedVirtualRows<HTMLButtonElement>({
itemCount: sortedEntries.length,
@@ -696,14 +700,14 @@ const ChooserFileBrowser = memo(function ChooserFileBrowser({
className="managed-file-entry is-folder"
onClick={() => onOpenFolder(entry.path)}
aria-posinset={position}
aria-setsize={sortedEntries.length + 1}
>
aria-setsize={sortedEntries.length + 1}>
<Folder size={18} aria-hidden="true" />
<span className="managed-file-entry-name"><strong>{entry.name}</strong><small>{entry.fileCount} file(s), {entry.folderCount} folder(s)</small></span>
<span className="managed-file-entry-name"><strong>{entry.name}</strong><small>{entry.fileCount} i18n:govoplan-files.file_s.ca61e97f {entry.folderCount} i18n:govoplan-files.folder_s.10e54730</small></span>
<span className="managed-file-entry-size">{formatBytes(entry.totalSize)}</span>
<span className="managed-file-entry-modified">{formatDate(entry.updatedAt)}</span>
</button>
);
</button>);
}
const exactPattern = relativePath(entry.file.display_path, effectiveRoot);
const selected = mode === "attachment" && selectedExactPattern === exactPattern;
@@ -715,36 +719,36 @@ const ChooserFileBrowser = memo(function ChooserFileBrowser({
className={`managed-file-entry is-file ${selected ? "is-selected" : ""} ${mode === "folder" ? "is-context-only" : ""}`}
onClick={() => mode === "attachment" ? onRequestExactFile(entry.file) : undefined}
disabled={mode === "folder"}
aria-label={mode === "folder" ? `${entry.file.filename}, file shown for context` : entry.file.filename}
aria-label={mode === "folder" ? i18nMessage("i18n:govoplan-files.value_file_shown_for_context.3d6e0acb", { value0: entry.file.filename }) : entry.file.filename}
aria-posinset={position}
aria-setsize={sortedEntries.length + 1}
>
aria-setsize={sortedEntries.length + 1}>
<File size={18} aria-hidden="true" />
<span className="managed-file-entry-name">
<strong>{entry.file.filename}</strong>
<small>{linked ? <><Link2 size={12} aria-hidden="true" /> linked to {linkTarget?.label || "target"}</> : "File"}</small>
<small>{linked ? <><Link2 size={12} aria-hidden="true" /> i18n:govoplan-files.linked_to.ca188768 {linkTarget?.label || "target"}</> : "i18n:govoplan-files.file.2c3cafa4"}</small>
</span>
<span className="managed-file-entry-size">{formatBytes(entry.file.size_bytes)}</span>
<span className="managed-file-entry-modified">{formatDate(entry.file.updated_at)}</span>
</button>
);
</button>);
}
return (
<div className="managed-file-entry-table">
<div className="managed-file-entry-head" role="row">
<span aria-hidden="true" />
<ChooserSortButton column="name" label="Name" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
<ChooserSortButton column="size" label="Size" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
<ChooserSortButton column="modified" label="Modified" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
<ChooserSortButton column="name" label="i18n:govoplan-files.name.709a2322" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
<ChooserSortButton column="size" label="i18n:govoplan-files.size.b7152342" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
<ChooserSortButton column="modified" label="i18n:govoplan-files.modified.19a532c8" activeColumn={sortColumn} direction={sortDirection} onSort={onToggleSort} />
</div>
<div
className="managed-file-entry-list"
ref={virtualRows.viewportRef}
role="listbox"
aria-label={mode === "folder" ? "Folders and files" : "Managed files"}
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}
>
aria-label={mode === "folder" ? "i18n:govoplan-files.folders_and_files.d107ac99" : "i18n:govoplan-files.managed_files.4dd6ad11"}
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}>
<button
type="button"
ref={virtualRows.measureRowRef}
@@ -752,31 +756,31 @@ const ChooserFileBrowser = memo(function ChooserFileBrowser({
onClick={() => onOpenFolder(parentWithinRoot(currentFolder, effectiveRoot))}
disabled={loading || currentFolder === effectiveRoot}
aria-posinset={1}
aria-setsize={sortedEntries.length + 1}
>
aria-setsize={sortedEntries.length + 1}>
<FolderOpen size={18} aria-hidden="true" />
<span className="managed-file-entry-name"><strong>..</strong><small>Parent folder</small></span>
<span className="managed-file-entry-name"><strong>..</strong><small>i18n:govoplan-files.parent_folder.d8ed4c2a</small></span>
<span className="managed-file-entry-size">-</span>
<span className="managed-file-entry-modified">-</span>
</button>
{virtualRows.topSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
)}
{sortedEntries.slice(virtualRows.startIndex, virtualRows.endIndex).map((entry, index) => (
renderEntry(entry, virtualRows.startIndex + index + 2)
))}
{virtualRows.bottomSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
)}
{!loading && sortedEntries.length === 0 && (
<p className="managed-file-empty muted">This folder is empty. Upload files in the top-level Files module.</p>
{virtualRows.topSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
}
{sortedEntries.slice(virtualRows.startIndex, virtualRows.endIndex).map((entry, index) =>
renderEntry(entry, virtualRows.startIndex + index + 2)
)}
{virtualRows.bottomSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
}
{!loading && sortedEntries.length === 0 &&
<p className="managed-file-empty muted">i18n:govoplan-files.this_folder_is_empty_upload_files_in_the_top_lev.cb6c3a1a</p>
}
</div>
</div>
);
</div>);
});
function PatternResultList({ files, linkTarget, onChoose }: { files: ManagedFile[]; linkTarget?: FilesManagedFileLinkTarget; onChoose: (file: ManagedFile) => void }) {
function PatternResultList({ files, linkTarget, onChoose }: {files: ManagedFile[];linkTarget?: FilesManagedFileLinkTarget;onChoose: (file: ManagedFile) => void;}) {
const resetKey = `${files.length}:${files[0]?.id ?? ""}:${files[files.length - 1]?.id ?? ""}`;
const virtualRows = useFixedVirtualRows<HTMLButtonElement>({
itemCount: files.length,
@@ -785,45 +789,45 @@ function PatternResultList({ files, linkTarget, onChoose }: { files: ManagedFile
});
return (
<div className="managed-pattern-results" role="table" aria-label="Pattern preview results" aria-rowcount={files.length + 1}>
<div className="managed-pattern-results" role="table" aria-label="i18n:govoplan-files.pattern_preview_results.56cea56c" aria-rowcount={files.length + 1}>
<div className="file-list-table-head" role="row">
<span>Name</span><span>Size</span><span>Modified</span>
<span>i18n:govoplan-files.name.709a2322</span><span>i18n:govoplan-files.size.b7152342</span><span>i18n:govoplan-files.modified.19a532c8</span>
</div>
<div
className="file-list-table"
ref={virtualRows.viewportRef}
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}
>
{files.length === 0 && <div className="file-list-empty">No current files match this pattern.</div>}
{virtualRows.topSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
)}
{files.slice(virtualRows.startIndex, virtualRows.endIndex).map((file, index) => (
<button
type="button"
className="file-list-row file-row managed-pattern-result-row"
role="row"
key={file.id}
ref={index === 0 ? virtualRows.measureRowRef : undefined}
onClick={() => onChoose(file)}
aria-rowindex={virtualRows.startIndex + index + 2}
>
onScroll={(event) => virtualRows.setScrollTop(event.currentTarget.scrollTop)}>
{files.length === 0 && <div className="file-list-empty">i18n:govoplan-files.no_current_files_match_this_pattern.b84c5836</div>}
{virtualRows.topSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.topSpacerHeight }} aria-hidden="true" />
}
{files.slice(virtualRows.startIndex, virtualRows.endIndex).map((file, index) =>
<button
type="button"
className="file-list-row file-row managed-pattern-result-row"
role="row"
key={file.id}
ref={index === 0 ? virtualRows.measureRowRef : undefined}
onClick={() => onChoose(file)}
aria-rowindex={virtualRows.startIndex + index + 2}>
<span className="file-list-name-cell">
<span className="file-list-name">
<File className="file-row-icon" size={20} aria-hidden="true" />
<span><strong>{file.display_path}</strong>{isLinkedToTarget(file, linkTarget) && <small>Linked to {linkTarget?.label || "target"}</small>}</span>
<span><strong>{file.display_path}</strong>{isLinkedToTarget(file, linkTarget) && <small>i18n:govoplan-files.linked_to.e7ca9e02 {linkTarget?.label || "target"}</small>}</span>
</span>
</span>
<span>{formatBytes(file.size_bytes)}</span>
<span className="file-row-tail"><span>{formatDate(file.updated_at)}</span></span>
</button>
))}
{virtualRows.bottomSpacerHeight > 0 && (
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
)}
{virtualRows.bottomSpacerHeight > 0 &&
<div className="managed-file-virtual-spacer" style={{ height: virtualRows.bottomSpacerHeight }} aria-hidden="true" />
}
</div>
</div>
);
</div>);
}
function useFixedVirtualRows<T extends HTMLElement>({
@@ -832,13 +836,13 @@ function useFixedVirtualRows<T extends HTMLElement>({
defaultRowHeight,
rowGap = 0,
leadingRows = 0
}: {
itemCount: number;
resetKey: string;
defaultRowHeight: number;
rowGap?: number;
leadingRows?: number;
}) {
}: {itemCount: number;resetKey: string;defaultRowHeight: number;rowGap?: number;leadingRows?: number;}) {
const viewportRef = useRef<HTMLDivElement | null>(null);
const measureRowRef = useRef<T | null>(null);
const [viewportHeight, setViewportHeight] = useState(0);
@@ -917,12 +921,12 @@ function useFixedVirtualRows<T extends HTMLElement>({
}
function flattenVisibleFolderTree(
nodes: FolderNode[],
spaceId: string,
expandedKeys: Set<string>,
depth = 1,
rows: VirtualFolderTreeRow[] = []
): VirtualFolderTreeRow[] {
nodes: FolderNode[],
spaceId: string,
expandedKeys: Set<string>,
depth = 1,
rows: VirtualFolderTreeRow[] = [])
: VirtualFolderTreeRow[] {
for (const node of nodes) {
rows.push({ node, depth });
if (node.children.length > 0 && expandedKeys.has(treeNodeKey(spaceId, node.path))) {
@@ -944,7 +948,7 @@ function parseManagedFileSource(value: unknown): ManagedFileSource | null {
if (typeof value !== "string" || !value.startsWith(MANAGED_FILE_SOURCE_PREFIX)) return null;
const [, ownerType, ...ownerIdParts] = value.split(":");
const ownerId = ownerIdParts.join(":").trim();
if ((ownerType !== "user" && ownerType !== "group") || !ownerId) return null;
if (ownerType !== "user" && ownerType !== "group" || !ownerId) return null;
return { ownerType, ownerId };
}
@@ -961,12 +965,12 @@ function relativePath(path: string, root: string): string {
return normalizedPath.startsWith(prefix) ? normalizedPath.slice(prefix.length) : normalizedPath;
}
function chooserBreadcrumbs(folder: string, root: string): { name: string; path: string }[] {
function chooserBreadcrumbs(folder: string, root: string): {name: string;path: string;}[] {
const normalizedFolder = normalizeFolder(folder);
const normalizedRoot = normalizeFolder(root);
const relative = normalizedRoot && normalizedFolder.startsWith(`${normalizedRoot}/`)
? normalizedFolder.slice(normalizedRoot.length + 1)
: normalizedRoot === normalizedFolder ? "" : normalizedFolder;
const relative = normalizedRoot && normalizedFolder.startsWith(`${normalizedRoot}/`) ?
normalizedFolder.slice(normalizedRoot.length + 1) :
normalizedRoot === normalizedFolder ? "" : normalizedFolder;
const parts = relative.split("/").filter(Boolean);
return parts.map((name, index) => ({
name,
@@ -1009,10 +1013,10 @@ function isExactPattern(value: string): boolean {
}
function renderChooserPattern(
pattern: string,
previewContext?: Record<string, string>,
renderPatternPreview?: FilesManagedFileChooserProps["renderPatternPreview"]
): string {
pattern: string,
previewContext?: Record<string, string>,
renderPatternPreview?: FilesManagedFileChooserProps["renderPatternPreview"])
: string {
const trimmed = pattern.trim();
if (!trimmed) return trimmed;
return (renderPatternPreview?.(trimmed, previewContext) ?? trimmed).trim();
@@ -1038,10 +1042,10 @@ function writeRememberedState(key: string, state: RememberedChooserState) {
try {
window.localStorage.setItem(key, JSON.stringify(state));
} catch {
// Storage is optional; chooser behavior remains functional without it.
}
}
// Storage is optional; chooser behavior remains functional without it.
}}
function errorMessage(reason: unknown): string {
return reason instanceof Error ? reason.message : String(reason);
}

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" | "create-folder" | "rename" | "single-rename" | "transfer" | null;
export type DialogKind = "upload" | "connector-sync" | "connector-space" | "create-folder" | "rename" | "single-rename" | "transfer" | null;
export type EntrySelectionKey = `file:${string}` | `folder:${string}`;
export type ContextMenuState = {
x: number;

View File

@@ -1,4 +1,4 @@
import { formatDateTime as formatPlatformDateTime } from "@govoplan/core-webui";
import { formatDateTime as formatPlatformDateTime, i18nMessage } from "@govoplan/core-webui";
import type { FileFolder, ManagedFile } from "../../../api/files";
import type { DragSelectionState, EntrySelectionKey, ExplorerEntry, FileEntry, FolderEntry, FolderNode, SortColumn, SortDirection } from "../types";
@@ -55,7 +55,7 @@ export function treeNodeKey(spaceId: string, folderPath: string): string {
return `${spaceId}:${normalizeFolder(folderPath)}`;
}
export function folderAncestorPaths(folderPath: string, options: { includeSelf?: boolean } = {}): string[] {
export function folderAncestorPaths(folderPath: string, options: {includeSelf?: boolean;} = {}): string[] {
const parts = normalizeFolder(folderPath).split("/").filter(Boolean);
const limit = options.includeSelf ? parts.length : Math.max(parts.length - 1, 0);
const result: string[] = [];
@@ -134,7 +134,7 @@ export function sortExplorerEntries(entries: ExplorerEntry[], column: SortColumn
const factor = direction === "asc" ? 1 : -1;
return [...entries].sort((a, b) => {
if (column === "name") {
if (a.kind !== b.kind) return a.kind === "folder" ? (direction === "asc" ? -1 : 1) : (direction === "asc" ? 1 : -1);
if (a.kind !== b.kind) return a.kind === "folder" ? direction === "asc" ? -1 : 1 : direction === "asc" ? 1 : -1;
return factor * entryName(a).localeCompare(entryName(b), undefined, { numeric: true, sensitivity: "base" });
}
if (column === "size") {
@@ -163,7 +163,7 @@ export function entryModifiedTime(entry: ExplorerEntry): number {
return Number.isNaN(parsed) ? 0 : parsed;
}
export function directFolderCounts(files: ManagedFile[], folders: FileFolder[], folderPath: string): { files: number; folders: number } {
export function directFolderCounts(files: ManagedFile[], folders: FileFolder[], folderPath: string): {files: number;folders: number;} {
const stats = buildFolderStats(files, folders).get(normalizeFolder(folderPath));
return { files: stats?.directFiles ?? 0, folders: stats?.childFolders.size ?? 0 };
}
@@ -243,7 +243,7 @@ export function buildFolderEntryFromSources(files: ManagedFile[], folders: FileF
return {
kind: "folder",
id: `folder:${normalizedPath}`,
name: lastPathSegment(normalizedPath) || "Root",
name: lastPathSegment(normalizedPath) || "i18n:govoplan-files.root.e96857c5",
path: normalizedPath,
fileCount: counts.files,
folderCount: counts.folders,
@@ -271,7 +271,7 @@ export function fileIdsForSelection(files: ManagedFile[], selectedFileIds: Set<s
return Array.from(ids);
}
export function folderBreadcrumbs(folder: string): { name: string; path: string }[] {
export function folderBreadcrumbs(folder: string): {name: string;path: string;}[] {
const parts = normalizeFolder(folder).split("/").filter(Boolean);
return parts.map((name, index) => ({ name, path: parts.slice(0, index + 1).join("/") }));
}
@@ -318,14 +318,19 @@ export function parseDragState(value: string): DragSelectionState | null {
}
export function formatBytes(value: number): string {
if (value < 1024) return `${value} B`;
const units = ["KB", "MB", "GB", "TB"];
if (value < 1024) return i18nMessage("i18n:govoplan-files.bytes_b", { value0: value });
const units = [
"i18n:govoplan-files.bytes_kb",
"i18n:govoplan-files.bytes_mb",
"i18n:govoplan-files.bytes_gb",
"i18n:govoplan-files.bytes_tb"
];
let size = value / 1024;
for (const unit of units) {
if (size < 1024) return `${size.toFixed(size >= 10 ? 0 : 1)} ${unit}`;
if (size < 1024) return i18nMessage(unit, { value0: size.toFixed(size >= 10 ? 0 : 1) });
size /= 1024;
}
return `${size.toFixed(1)} PB`;
return i18nMessage("i18n:govoplan-files.bytes_pb", { value0: size.toFixed(1) });
}
export function formatDate(value: string): string {

View File

@@ -0,0 +1,690 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-files.add_connector_space.aa6bdbd6": "Add connector space",
"i18n:govoplan-files.add_credential_for_value.0fa9c1fe": "Add credential for {value0}",
"i18n:govoplan-files.add_prefix.672452bc": "Add prefix",
"i18n:govoplan-files.add_space.e4d674d4": "Add Space",
"i18n:govoplan-files.add_suffix_before_extension.784381fe": "Add suffix before extension",
"i18n:govoplan-files.added_connector_space_value.a24725b8": "Added connector space {value0}.",
"i18n:govoplan-files.allowed_connection_ids.0df854c8": "Allowed connection IDs",
"i18n:govoplan-files.allowed_credential_ids.efcaba2d": "Allowed credential IDs",
"i18n:govoplan-files.allowed_endpoint_urls.34cfa8e9": "Allowed endpoint URLs",
"i18n:govoplan-files.allowed_paths.c953b4be": "Allowed paths",
"i18n:govoplan-files.allowed_providers.82a9c23e": "Allowed providers",
"i18n:govoplan-files.allowed.77c7b490": "Allowed",
"i18n:govoplan-files.already_at_the_root_folder.1238f110": "Already at the root folder",
"i18n:govoplan-files.and.a0d93385": "… and",
"i18n:govoplan-files.anonymous.9bed5104": "Anonymous",
"i18n:govoplan-files.any_provider.b3fc542f": "Any provider",
"i18n:govoplan-files.apply_preview.8692d5eb": "Apply preview",
"i18n:govoplan-files.apply_recursively_to_folder_contents.c7076984": "Apply recursively to folder contents",
"i18n:govoplan-files.apply_reviewed_choices.bb55f7b3": "Apply reviewed choices",
"i18n:govoplan-files.attachments_attachment_sources.87d92d5b": "Attachments → Attachment sources",
"i18n:govoplan-files.audit_relevant_files_stay_retained_when_deleted_.2b7b4543": "Audit-relevant files stay retained when deleted from active browsing.",
"i18n:govoplan-files.available.974948f2": " · Available",
"i18n:govoplan-files.back_to_folder.34ba1ed1": "Back to folder",
"i18n:govoplan-files.base_path.6a4867ca": "Base path",
"i18n:govoplan-files.blocked_by_policy.8d971f86": "Blocked by policy",
"i18n:govoplan-files.blocked.99613c74": "Blocked",
"i18n:govoplan-files.bootstrap_settings.05e3df38": " · Bootstrap settings",
"i18n:govoplan-files.browse_ok_value_item_value.921906fd": "Browse OK ({value0} item{value1}).",
"i18n:govoplan-files.browse_protocol.d1f27c90": "Browse protocol",
"i18n:govoplan-files.browse.2f3b5c55": "Browse",
"i18n:govoplan-files.bulk_rename_changes_managed_display_paths_only_i.8cf34a6b": "Bulk rename changes managed display paths only. Immutable blobs stay stable for audit.",
"i18n:govoplan-files.bulk_rename_selected_items.5e79d694": "Bulk rename selected items",
"i18n:govoplan-files.bulk_rename.7dcaa624": "Bulk rename",
"i18n:govoplan-files.bytes_b": "{value0} B",
"i18n:govoplan-files.bytes_gb": "{value0} GB",
"i18n:govoplan-files.bytes_kb": "{value0} KB",
"i18n:govoplan-files.bytes_mb": "{value0} MB",
"i18n:govoplan-files.bytes_pb": "{value0} PB",
"i18n:govoplan-files.bytes_tb": "{value0} TB",
"i18n:govoplan-files.can_use_this_connection.5091a74c": "Can use this connection",
"i18n:govoplan-files.can_use_this_credential.f4a41971": "Can use this credential",
"i18n:govoplan-files.cancel.77dfd213": "Cancel",
"i18n:govoplan-files.cannot_move_or_copy_a_folder_into_itself_or_one_.4adbd5ea": "Cannot move or copy a folder into itself or one of its child folders.",
"i18n:govoplan-files.case_sensitive.c73af7bd": "Case sensitive",
"i18n:govoplan-files.checking.494d0f68": "Checking...",
"i18n:govoplan-files.choose_a_connector_file_and_destination_folder.3c6f3ffb": "Choose a connector file and destination folder.",
"i18n:govoplan-files.choose_a_connector_profile_and_owner_space.4c386b00": "Choose a connector profile and owner space.",
"i18n:govoplan-files.choose_a_destination_space_and_folder_folders_ar.45158643": "Choose a destination space and folder. Folders are transferred recursively.",
"i18n:govoplan-files.choose_how_the_selected_names_should_be_changed.0231854f": "Choose how the selected names should be changed.",
"i18n:govoplan-files.choose_how_to_resolve_this_conflict.c303fcc6": "Choose how to resolve this conflict.",
"i18n:govoplan-files.choose_managed_attachment_source.b3709493": "Choose managed attachment source",
"i18n:govoplan-files.choose_managed_file_or_pattern.bcf8e183": "Choose managed file or pattern",
"i18n:govoplan-files.choose_the_destination_folder_before_selecting_o.a4922d75": "Choose the destination folder before selecting or dropping files.",
"i18n:govoplan-files.campaigns": "Campaigns",
"i18n:govoplan-files.choose_the_folder_that_should_act_as_this_campai.9d0b7ef7": "Choose the folder that should act as this campaign attachment source.",
"i18n:govoplan-files.choose_the_parent_folder_for_the_new_subfolder.2fcb6580": "Choose the parent folder for the new subfolder.",
"i18n:govoplan-files.choose_the_same_action_for_all_conflicts_or_revi.319cfe5f": "Choose the same action for all conflicts or review the target names individually.",
"i18n:govoplan-files.choose_the_target_folder_folders_are_transferred.f7ab8e9e": "Choose the target folder. Folders are transferred recursively.",
"i18n:govoplan-files.clear_saved_password.7442260d": "Clear saved password",
"i18n:govoplan-files.clear_saved_token.a9c670aa": "Clear saved token",
"i18n:govoplan-files.clear.719ea396": "Clear",
"i18n:govoplan-files.clicking_a_file_sets_its_exact_relative_path_as_.590abd24": "Clicking a file sets its exact relative path as the pattern. Preview resolves the pattern against the current managed file space.",
"i18n:govoplan-files.collapse.9cf188d3": "Collapse",
"i18n:govoplan-files.conflict_at.0a91052f": "conflict at",
"i18n:govoplan-files.conflict_s.60cea6d5": "conflict(s)",
"i18n:govoplan-files.connection_credential.178babe0": "Connection / credential",
"i18n:govoplan-files.connector_files.8f351f5d": "Connector files",
"i18n:govoplan-files.connector_folders.960cbb03": "Connector folders",
"i18n:govoplan-files.connector_profile_is_not_configured_for_this_spa.669f57b5": "Connector profile is not configured for this space.",
"i18n:govoplan-files.connector_profile.9e9cd317": "Connector profile",
"i18n:govoplan-files.connector_root.9027235c": "Connector root",
"i18n:govoplan-files.connector_source_already_matched.7eaf5be1": "Connector source already matched",
"i18n:govoplan-files.connector_space_files.dbb0ab24": "Connector space files",
"i18n:govoplan-files.connector.ba358306": "Connector",
"i18n:govoplan-files.contained_path_s_will_move_with_the_selected_fol.b786f1a1": "contained path(s) will move with the selected folder(s), but their own names will stay unchanged.",
"i18n:govoplan-files.copied.8e3df45a": "Copied",
"i18n:govoplan-files.copy.92556c6d": "Copy…",
"i18n:govoplan-files.copy.a69c71d0": " copy",
"i18n:govoplan-files.copy.af74f7c5": "Copy",
"i18n:govoplan-files.copying.43b7de98": "Copying",
"i18n:govoplan-files.copymove.d0fa5904": "copyMove",
"i18n:govoplan-files.create_a_folder_below_the_selected_destination.9041ee76": "Create a folder below the selected destination.",
"i18n:govoplan-files.create_folder.97bafaba": "Create Folder",
"i18n:govoplan-files.create_folder.e59f63fa": "Create folder",
"i18n:govoplan-files.create_space.b50606b4": "Create Space",
"i18n:govoplan-files.created_folder_value.6c3002f9": "Created folder {value0}.",
"i18n:govoplan-files.created_inside.3426a815": "Created inside",
"i18n:govoplan-files.credential_id_and_label_are_required.4cfd1ffd": "Credential id and label are required.",
"i18n:govoplan-files.credential_id.9432a6e1": "Credential id",
"i18n:govoplan-files.credential_mode.23fdd899": "Credential mode",
"i18n:govoplan-files.credential.8bede3ea": "Credential",
"i18n:govoplan-files.current_file.1fbfcf3d": "current file",
"i18n:govoplan-files.current_folder_contents.f9a24fa8": "Current folder contents",
"i18n:govoplan-files.current_folder.5aeab2f0": "Current folder",
"i18n:govoplan-files.delete_selected_item_s.4b6a0023": "Delete selected item(s)?",
"i18n:govoplan-files.delete_value_audit_relevant_blobs_remain_retaine.290af88f": "Delete {value0}? Audit-relevant blobs remain retained.",
"i18n:govoplan-files.delete.f6fdbe48": "Delete",
"i18n:govoplan-files.denied_connection_ids.a95218b4": "Denied connection IDs",
"i18n:govoplan-files.denied_credential_ids.b6838bc2": "Denied credential IDs",
"i18n:govoplan-files.denied_endpoint_urls.1d8d4369": "Denied endpoint URLs",
"i18n:govoplan-files.denied_paths.d25e4315": "Denied paths",
"i18n:govoplan-files.denied_providers.149b29f4": "Denied providers",
"i18n:govoplan-files.deny_listed_paths.fcde62cc": "Deny-listed paths",
"i18n:govoplan-files.destination_folder.f8ccb63b": "Destination folder",
"i18n:govoplan-files.destination_space.92b63970": "Destination space",
"i18n:govoplan-files.disable_file_connection.3fd940ae": "Disable file connection",
"i18n:govoplan-files.disable_file_credential.49bff614": "Disable file credential",
"i18n:govoplan-files.disable_value_connections_using_it_will_stop_aut.fc3a1708": "Disable {value0}? Connections using it will stop authenticating until another credential is selected.",
"i18n:govoplan-files.disable_value_existing_linked_spaces_will_stop_b.8c1b0ac6": "Disable {value0}? Existing linked spaces will stop browsing until the connection is enabled again.",
"i18n:govoplan-files.disable_value.09485f7f": "Disable {value0}",
"i18n:govoplan-files.disable.9a7d4e06": "Disable",
"i18n:govoplan-files.disabled.f4f4473d": "Disabled",
"i18n:govoplan-files.dms.477e5652": "DMS",
"i18n:govoplan-files.download_zip.7bd0e4b0": "Download ZIP",
"i18n:govoplan-files.download.a479c9c3": "Download",
"i18n:govoplan-files.e_g_invoices.77a73bd1": "e.g. invoices",
"i18n:govoplan-files.edit_file_connection.78698154": "Edit file connection",
"i18n:govoplan-files.edit_file_credential.bc49d44f": "Edit file credential",
"i18n:govoplan-files.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-files.effective_sources_none.9a7c407f": "Effective sources: none",
"i18n:govoplan-files.effective_sources.9a576802": "Effective sources:",
"i18n:govoplan-files.enabled.df174a3f": "Enabled",
"i18n:govoplan-files.endpoint_url.65aaaa45": "Endpoint URL",
"i18n:govoplan-files.endpoint.92ec6350": "Endpoint",
"i18n:govoplan-files.expand.9869e506": "Expand",
"i18n:govoplan-files.extracting_files_on_the_server.845a3c1a": "Extracting files on the server…",
"i18n:govoplan-files.file_actions.9e1b94c5": "File actions",
"i18n:govoplan-files.file_connection_created.bdf6e1f6": "File connection created.",
"i18n:govoplan-files.file_connection_disabled_value.059a7de9": "File connection disabled: {value0}.",
"i18n:govoplan-files.file_connection_saved.68c16ee9": "File connection saved.",
"i18n:govoplan-files.file_connections.1e362326": "File connections",
"i18n:govoplan-files.file_credential_created.87c31882": "File credential created.",
"i18n:govoplan-files.file_credential_disabled_value.947538fd": "File credential disabled: {value0}.",
"i18n:govoplan-files.file_credential_saved.d6a1eef8": "File credential saved.",
"i18n:govoplan-files.file_or_pattern_relative_to.0ab4d831": "File or pattern relative to",
"i18n:govoplan-files.file_s.ca61e97f": "file(s),",
"i18n:govoplan-files.file_share_was_not_created.033ac476": "File share was not created.",
"i18n:govoplan-files.file_spaces_and_folders.443d2056": "File spaces and folders",
"i18n:govoplan-files.file.2c3cafa4": "File",
"i18n:govoplan-files.files_with_the_same_visible_name_already_exist_i.1bb487b0": "Files with the same visible name already exist in this folder.",
"i18n:govoplan-files.files.6ce6c512": "Files",
"i18n:govoplan-files.finalizing_upload.bcce936d": "Finalizing upload…",
"i18n:govoplan-files.find_and_replace.2c6b404b": "Find and replace",
"i18n:govoplan-files.find.df251b06": "Find",
"i18n:govoplan-files.first.49ec0838": "first.",
"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.folders_and_files.d107ac99": "Folders and files",
"i18n:govoplan-files.folders.19adc47b": "Folders",
"i18n:govoplan-files.gb.505a44fa": "GB",
"i18n:govoplan-files.generic.ff7613e5": "Generic",
"i18n:govoplan-files.govoplan_files.b20752ed": "GovOPlaN files",
"i18n:govoplan-files.govoplan_webdav_credentials.bc696d69": "GovOPlaN WebDAV credentials",
"i18n:govoplan-files.group.171a0606": "Group",
"i18n:govoplan-files.groups": "Groups",
"i18n:govoplan-files.import.d6fbc9d2": "Import",
"i18n:govoplan-files.inherited.58823af0": "Inherited",
"i18n:govoplan-files.kb.315b39a0": "KB",
"i18n:govoplan-files.kerberos.53c35fb7": "Kerberos",
"i18n:govoplan-files.label.74341e3c": "Label",
"i18n:govoplan-files.leave_empty_to_keep_saved_password.6ec39f5e": "Leave empty to keep saved password",
"i18n:govoplan-files.leave_empty_to_keep_saved_token.e725857b": "Leave empty to keep saved token",
"i18n:govoplan-files.library.b8100f5b": "Library",
"i18n:govoplan-files.linked_file_space.1a614c7d": "Linked file space",
"i18n:govoplan-files.linked_to_1_campaign.5349694f": "Linked to 1 campaign",
"i18n:govoplan-files.linked_to_value_campaigns.1ad7be94": "Linked to {value0} campaigns",
"i18n:govoplan-files.linked_to.ca188768": "linked to",
"i18n:govoplan-files.linked_to.e7ca9e02": "Linked to",
"i18n:govoplan-files.linking.6f640897": "Linking…",
"i18n:govoplan-files.loading_connector_files.02c876e0": "Loading connector files…",
"i18n:govoplan-files.loading_connector_files.e5b0871d": "Loading connector files",
"i18n:govoplan-files.loading_connector_folders.426de3b6": "Loading connector folders",
"i18n:govoplan-files.loading_connector_folders.dde26f3e": "Loading connector folders...",
"i18n:govoplan-files.loading_connector_policy.2b984eec": "Loading connector policy...",
"i18n:govoplan-files.loading_connector_policy.f12ab285": "Loading connector policy",
"i18n:govoplan-files.loading_connector_space.356d83c6": "Loading connector space…",
"i18n:govoplan-files.loading_connector_space.c176e6b8": "Loading connector space",
"i18n:govoplan-files.loading_file_connections.bd68f224": "Loading file connections",
"i18n:govoplan-files.loading_file_connections.ef22dc81": "Loading file connections...",
"i18n:govoplan-files.loading_files.3b142382": "Loading files",
"i18n:govoplan-files.loading_files.bfd27a7e": "Loading files…",
"i18n:govoplan-files.loading.b04ba49f": "Loading...",
"i18n:govoplan-files.lower_connection_limits.4f9838bc": "Lower connection limits",
"i18n:govoplan-files.lower_credential_limits.ec2b72bc": "Lower credential limits",
"i18n:govoplan-files.lower_endpoint_limits.3fd781d5": "Lower endpoint limits",
"i18n:govoplan-files.lower_path_limits.1abcccb1": "Lower path limits",
"i18n:govoplan-files.lower_provider_limits.5816270a": "Lower provider limits",
"i18n:govoplan-files.managed_files.4dd6ad11": "Managed files",
"i18n:govoplan-files.match.c79af5c2": "match.",
"i18n:govoplan-files.mb.6e979f42": "MB",
"i18n:govoplan-files.metadata_json_must_be_an_object.48629f13": "Metadata JSON must be an object.",
"i18n:govoplan-files.metadata_json.b0e4c283": "Metadata JSON",
"i18n:govoplan-files.mode.a7b93d21": "Mode",
"i18n:govoplan-files.modified.19a532c8": "Modified",
"i18n:govoplan-files.more_show_next.f1912318": "more — show next",
"i18n:govoplan-files.move.76cdb950": "Move",
"i18n:govoplan-files.move.8a74a26e": "Move…",
"i18n:govoplan-files.moved.0fc28db0": "Moved",
"i18n:govoplan-files.moving.65cd4dd8": "Moving",
"i18n:govoplan-files.must_stay_in_allowed_paths.dc5b4eb4": "Must stay in allowed paths",
"i18n:govoplan-files.name.709a2322": "Name",
"i18n:govoplan-files.native_default.ba32f7b6": "Native/default",
"i18n:govoplan-files.new_connection.ac979fe4": "New connection",
"i18n:govoplan-files.new_credential.65b2a9b1": "New credential",
"i18n:govoplan-files.new_file_connection.2ec6eb56": "New file connection",
"i18n:govoplan-files.new_file_credential.4c061082": "New file credential",
"i18n:govoplan-files.new_folder.a711999b": "New folder",
"i18n:govoplan-files.new_name.9e627c7b": "New name",
"i18n:govoplan-files.new_path_for_value.a9a30c1c": "New path for {value0}",
"i18n:govoplan-files.nextcloud.aab73a3d": "Nextcloud",
"i18n:govoplan-files.nfs.db121865": "NFS",
"i18n:govoplan-files.no_accessible_file_spaces_were_found.abcf5003": "No accessible file spaces were found.",
"i18n:govoplan-files.no_campaign_links.4203c2be": "No campaign links",
"i18n:govoplan-files.no_connector_items.d634e023": "No connector items.",
"i18n:govoplan-files.no_connector_profiles_are_available.9be3be28": "No connector profiles are available.",
"i18n:govoplan-files.no_connector_profiles.03c730ee": "No connector profiles",
"i18n:govoplan-files.no_current_files_match_this_pattern.b84c5836": "No current files match this pattern.",
"i18n:govoplan-files.no_endpoint.d0ea68c4": "No endpoint",
"i18n:govoplan-files.no_file_connections_configured.3ef02049": "No file connections configured.",
"i18n:govoplan-files.no_file_selected.f76f1c1c": "No file selected",
"i18n:govoplan-files.no_file_spaces_available.a81fa3d0": "No file spaces available.",
"i18n:govoplan-files.no_value_available": "No {value0} available.",
"i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped.1166f3b5": "No files uploaded; all conflicts were skipped.",
"i18n:govoplan-files.no_folders_yet_choose_the_root_folder.6430304d": "No folders yet. Choose the root folder.",
"i18n:govoplan-files.no_saved_credential.30a5a951": "No saved credential",
"i18n:govoplan-files.no_secret.33c1a339": "No secret",
"i18n:govoplan-files.none.6eef6648": "None",
"i18n:govoplan-files.not_sorted.0abc03c5": "not sorted",
"i18n:govoplan-files.ntlm.026eac85": "NTLM",
"i18n:govoplan-files.only_the_visible_name_changes_immutable_blobs_st.01557e71": "Only the visible name changes. Immutable blobs stay stable for audit.",
"i18n:govoplan-files.open_parent_folder_value.8030a7c7": "Open parent folder {value0}",
"i18n:govoplan-files.overwrite_all.a76f2d04": "Overwrite all",
"i18n:govoplan-files.overwrite.0625c54e": "Overwrite",
"i18n:govoplan-files.owner_space.364c4b62": "Owner space",
"i18n:govoplan-files.parent_folder.d8ed4c2a": "Parent folder",
"i18n:govoplan-files.password_environment_variable.0e77673f": "Password environment variable",
"i18n:govoplan-files.password.8be3c943": "Password",
"i18n:govoplan-files.path_limited.d32cbef0": "Path-limited",
"i18n:govoplan-files.pattern_preview_results.56cea56c": "Pattern preview results",
"i18n:govoplan-files.policy.bb9cf141": "Policy",
"i18n:govoplan-files.prefix.90eceb01": "Prefix",
"i18n:govoplan-files.preview_rename.bb890b24": "Preview rename",
"i18n:govoplan-files.preview_resolves_to.a8d99432": "Preview resolves to",
"i18n:govoplan-files.preview.f1fbb2b4": "Preview",
"i18n:govoplan-files.profile_id_and_label_are_required.6ad85df1": "Profile id and label are required.",
"i18n:govoplan-files.profile_id.25961d68": "Profile id",
"i18n:govoplan-files.provider.7ceee3f3": "Provider",
"i18n:govoplan-files.read_only.9b19a5a2": "Read-only",
"i18n:govoplan-files.refresh.56e3badc": "Refresh",
"i18n:govoplan-files.reload.cce71553": "Reload",
"i18n:govoplan-files.remote_connector_space.d8956863": "Remote connector space",
"i18n:govoplan-files.rename_all.1d6b24dc": "Rename all",
"i18n:govoplan-files.rename_item.3d21d6ca": "Rename item",
"i18n:govoplan-files.rename.d3f4cb89": "Rename",
"i18n:govoplan-files.renamed_value_item_s.3c4a40fe": "Renamed {value0} item(s).",
"i18n:govoplan-files.replace_pattern.d98e4c92": "Replace pattern",
"i18n:govoplan-files.replace_the_current_pattern.96f80707": "Replace the current pattern?",
"i18n:govoplan-files.replace_value_with_the_exact_file_value.4a63236f": "Replace \"{value0}\" with the exact file \"{value1}\"?",
"i18n:govoplan-files.replacement.c385e347": "Replacement",
"i18n:govoplan-files.require_smb_signing.5168a83d": "Require SMB signing",
"i18n:govoplan-files.review_individually.19abbe58": "Review individually",
"i18n:govoplan-files.root.e96857c5": "Root",
"i18n:govoplan-files.save_connection.07796d38": "Save connection",
"i18n:govoplan-files.save_credential.2c02a6d2": "Save credential",
"i18n:govoplan-files.save_policy.77d67ce3": "Save policy",
"i18n:govoplan-files.saving.56a2285c": "Saving…",
"i18n:govoplan-files.saving.ae7e8875": "Saving...",
"i18n:govoplan-files.seafile.600192cc": "Seafile",
"i18n:govoplan-files.search_this_folder_pdf_pdf_or_2026_pdf.74dec36a": "Search this folder: *.pdf, **/*.pdf or 2026-??-*.pdf",
"i18n:govoplan-files.search.bce06414": "Search",
"i18n:govoplan-files.secret_configured.4dc0f095": "Secret configured",
"i18n:govoplan-files.secret_reference.04ed2221": "Secret reference",
"i18n:govoplan-files.select_a_remote_file_to_sync_into_the_destinatio.80477a47": "Select a remote file to sync into the destination folder.",
"i18n:govoplan-files.select_a_remote_file_to_sync_it_into_this_space_.8e58a5ae": "Select a remote file to sync it into this space owner.",
"i18n:govoplan-files.select_the_space_that_will_receive_the_selected_.0a8bea8f": "Select the space that will receive the selected file(s) or folder(s).",
"i18n:govoplan-files.selected.840fac38": " · Selected",
"i18n:govoplan-files.show_fewer.d94dfbe5": "Show fewer",
"i18n:govoplan-files.size.b7152342": "Size",
"i18n:govoplan-files.skip.3da47453": "Skip",
"i18n:govoplan-files.smb_authentication.96f7cb83": "SMB authentication",
"i18n:govoplan-files.smb.515d2f88": "SMB",
"i18n:govoplan-files.space_label.ec892726": "Space label",
"i18n:govoplan-files.spaces.9b584b52": "Spaces",
"i18n:govoplan-files.status.bae7d5be": "Status",
"i18n:govoplan-files.suffix.885db975": "Suffix",
"i18n:govoplan-files.sync_to_value_value.29c362ea": "Sync to {value0} / {value1}",
"i18n:govoplan-files.sync.905f6309": "Sync",
"i18n:govoplan-files.synced_new_file.d5e8243d": "Synced new file",
"i18n:govoplan-files.synced_new_version.83784264": "Synced new version",
"i18n:govoplan-files.system.bc0792d8": "System",
"i18n:govoplan-files.target.61ad50a9": "Target",
"i18n:govoplan-files.tb.f8a902b0": "TB",
"i18n:govoplan-files.tenant.3ca93c78": "Tenant",
"i18n:govoplan-files.test_value.6a5d10a5": "Test {value0}",
"i18n:govoplan-files.testing.15ccc832": "Testing...",
"i18n:govoplan-files.the_configured_managed_file_space_is_no_longer_a.5e93b729": "The configured managed file space is no longer available to this user.",
"i18n:govoplan-files.this_attachment_base_path_is_not_connected_to_a_.3962b48f": "This attachment base path is not connected to a managed file space. Choose its folder under",
"i18n:govoplan-files.this_file_connection.edcde997": "this file connection",
"i18n:govoplan-files.this_file_credential.695efb90": "this file credential",
"i18n:govoplan-files.this_folder_is_empty_upload_files_in_the_top_lev.cb6c3a1a": "This folder is empty. Upload files in the top-level Files module.",
"i18n:govoplan-files.this_folder_is_empty_use_upload_or_create_folder.5977d784": "This folder is empty. Use Upload or Create Folder to add content.",
"i18n:govoplan-files.token_environment_variable.5af4a79e": "Token environment variable",
"i18n:govoplan-files.token.a1141eb9": "Token",
"i18n:govoplan-files.unpack_zip_uploads.256fead4": "Unpack ZIP uploads",
"i18n:govoplan-files.unpacking_zip_archive.698095f4": "Unpacking ZIP archive",
"i18n:govoplan-files.unpacking_zip_upload.35019691": "Unpacking ZIP upload…",
"i18n:govoplan-files.up.2038bdec": "Up",
"i18n:govoplan-files.upload_failed_because_the_network_request_could_.360a5ab3": "Upload failed because the network request could not be completed.",
"i18n:govoplan-files.upload_to_value_value.b83a34b4": "Upload to {value0} / {value1}",
"i18n:govoplan-files.upload_was_cancelled.5bab0f9b": "Upload was cancelled.",
"i18n:govoplan-files.upload.8bdf057f": "Upload",
"i18n:govoplan-files.uploaded_value_file_s_into_value.d0fa052b": "Uploaded {value0} file(s) into {value1}.",
"i18n:govoplan-files.uploading_files.6536791d": "Uploading files",
"i18n:govoplan-files.uploading_value_file_s.715ba963": "Uploading {value0} file(s)…",
"i18n:govoplan-files.use_pattern.ce54ba3e": "Use pattern",
"i18n:govoplan-files.use_this_folder.0e77d6d1": "Use this folder",
"i18n:govoplan-files.user.9f8a2389": "User",
"i18n:govoplan-files.users": "Users",
"i18n:govoplan-files.username_password.e8ba8896": "Username/password",
"i18n:govoplan-files.username.84c29015": "Username",
"i18n:govoplan-files.value_conflict_s.b3872a48": "{value0} conflict(s)",
"i18n:govoplan-files.value_connector_policy_saved.430d4eca": "{value0} connector policy saved.",
"i18n:govoplan-files.value_connector_policy.0bf7f53b": "{value0} connector policy",
"i18n:govoplan-files.value_file_shown_for_context.3d6e0acb": "{value0}, file shown for context",
"i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c": "{value0} folder(s) · {value1} file(s)",
"i18n:govoplan-files.value_this_selection_would_create_duplicate_visi.26ae34d4": "{value0} this selection would create duplicate visible names in the destination.",
"i18n:govoplan-files.value_upload_conflict_s.3a04f117": "{value0} upload conflict(s)",
"i18n:govoplan-files.value_value_activate_to_sort.3953ba71": "{value0}: {value1}. Activate to sort.",
"i18n:govoplan-files.value_value_file_s_and_value_folder_s.c160e725": "{value0} {value1} file(s) and {value2} folder(s).",
"i18n:govoplan-files.value_value.36bc3a40": "{value0}: {value1}.",
"i18n:govoplan-files.value_value.598aab59": "{value0} -> {value1}",
"i18n:govoplan-files.value_value.c189e8bc": "{value0} ({value1})",
"i18n:govoplan-files.value_value.dca59cc0": "{value0} {value1}",
"i18n:govoplan-files.value.48afe802": "· {value0}",
"i18n:govoplan-files.value_file_connections": "{value0} file connections",
"i18n:govoplan-files.value_scope": "{value0} scope",
"i18n:govoplan-files.visible_file_s_were_not_matched.dc497e90": "visible file(s) were not matched.",
"i18n:govoplan-files.webdav.521b4c8b": "WebDAV",
"i18n:govoplan-files.working.13b7bfca": "Working…",
"i18n:govoplan-files.writable.dd35487a": "Writable"
},
"de": {
"i18n:govoplan-files.add_connector_space.aa6bdbd6": "Add connector space",
"i18n:govoplan-files.add_credential_for_value.0fa9c1fe": "Add credential for {value0}",
"i18n:govoplan-files.add_prefix.672452bc": "Add prefix",
"i18n:govoplan-files.add_space.e4d674d4": "Add Space",
"i18n:govoplan-files.add_suffix_before_extension.784381fe": "Add suffix before extension",
"i18n:govoplan-files.added_connector_space_value.a24725b8": "Added connector space {value0}.",
"i18n:govoplan-files.allowed_connection_ids.0df854c8": "Allowed connection IDs",
"i18n:govoplan-files.allowed_credential_ids.efcaba2d": "Allowed credential IDs",
"i18n:govoplan-files.allowed_endpoint_urls.34cfa8e9": "Allowed endpoint URLs",
"i18n:govoplan-files.allowed_paths.c953b4be": "Allowed paths",
"i18n:govoplan-files.allowed_providers.82a9c23e": "Allowed providers",
"i18n:govoplan-files.allowed.77c7b490": "Allowed",
"i18n:govoplan-files.already_at_the_root_folder.1238f110": "Already at the root folder",
"i18n:govoplan-files.and.a0d93385": "… and",
"i18n:govoplan-files.anonymous.9bed5104": "Anonymous",
"i18n:govoplan-files.any_provider.b3fc542f": "Any provider",
"i18n:govoplan-files.apply_preview.8692d5eb": "Apply preview",
"i18n:govoplan-files.apply_recursively_to_folder_contents.c7076984": "Apply recursively to folder contents",
"i18n:govoplan-files.apply_reviewed_choices.bb55f7b3": "Apply reviewed choices",
"i18n:govoplan-files.attachments_attachment_sources.87d92d5b": "Attachments → Attachment sources",
"i18n:govoplan-files.audit_relevant_files_stay_retained_when_deleted_.2b7b4543": "Audit-relevant files stay retained when deleted from active browsing.",
"i18n:govoplan-files.available.974948f2": " · Available",
"i18n:govoplan-files.back_to_folder.34ba1ed1": "Back to folder",
"i18n:govoplan-files.base_path.6a4867ca": "Base path",
"i18n:govoplan-files.blocked_by_policy.8d971f86": "Blocked by policy",
"i18n:govoplan-files.blocked.99613c74": "Blocked",
"i18n:govoplan-files.bootstrap_settings.05e3df38": " · Bootstrap settings",
"i18n:govoplan-files.browse_ok_value_item_value.921906fd": "Browse OK ({value0} item{value1}).",
"i18n:govoplan-files.browse_protocol.d1f27c90": "Browse protocol",
"i18n:govoplan-files.browse.2f3b5c55": "Durchsuchen",
"i18n:govoplan-files.bulk_rename_changes_managed_display_paths_only_i.8cf34a6b": "Bulk rename changes managed display paths only. Immutable blobs stay stable for audit.",
"i18n:govoplan-files.bulk_rename_selected_items.5e79d694": "Bulk rename selected items",
"i18n:govoplan-files.bulk_rename.7dcaa624": "Bulk rename",
"i18n:govoplan-files.bytes_b": "{value0} B",
"i18n:govoplan-files.bytes_gb": "{value0} GB",
"i18n:govoplan-files.bytes_kb": "{value0} KB",
"i18n:govoplan-files.bytes_mb": "{value0} MB",
"i18n:govoplan-files.bytes_pb": "{value0} PB",
"i18n:govoplan-files.bytes_tb": "{value0} TB",
"i18n:govoplan-files.can_use_this_connection.5091a74c": "Can use this connection",
"i18n:govoplan-files.can_use_this_credential.f4a41971": "Can use this credential",
"i18n:govoplan-files.cancel.77dfd213": "Abbrechen",
"i18n:govoplan-files.cannot_move_or_copy_a_folder_into_itself_or_one_.4adbd5ea": "Cannot move or copy a folder into itself or one of its child folders.",
"i18n:govoplan-files.case_sensitive.c73af7bd": "Case sensitive",
"i18n:govoplan-files.checking.494d0f68": "Checking...",
"i18n:govoplan-files.choose_a_connector_file_and_destination_folder.3c6f3ffb": "Choose a connector file and destination folder.",
"i18n:govoplan-files.choose_a_connector_profile_and_owner_space.4c386b00": "Choose a connector profile and owner space.",
"i18n:govoplan-files.choose_a_destination_space_and_folder_folders_ar.45158643": "Choose a destination space and folder. Folders are transferred recursively.",
"i18n:govoplan-files.choose_how_the_selected_names_should_be_changed.0231854f": "Choose how the selected names should be changed.",
"i18n:govoplan-files.choose_how_to_resolve_this_conflict.c303fcc6": "Choose how to resolve this conflict.",
"i18n:govoplan-files.choose_managed_attachment_source.b3709493": "Choose managed attachment source",
"i18n:govoplan-files.choose_managed_file_or_pattern.bcf8e183": "Choose managed file or pattern",
"i18n:govoplan-files.choose_the_destination_folder_before_selecting_o.a4922d75": "Choose the destination folder before selecting or dropping files.",
"i18n:govoplan-files.campaigns": "Kampagnen",
"i18n:govoplan-files.choose_the_folder_that_should_act_as_this_campai.9d0b7ef7": "Choose the folder that should act as this campaign attachment source.",
"i18n:govoplan-files.choose_the_parent_folder_for_the_new_subfolder.2fcb6580": "Choose the parent folder for the new subfolder.",
"i18n:govoplan-files.choose_the_same_action_for_all_conflicts_or_revi.319cfe5f": "Choose the same action for all conflicts or review the target names individually.",
"i18n:govoplan-files.choose_the_target_folder_folders_are_transferred.f7ab8e9e": "Choose the target folder. Folders are transferred recursively.",
"i18n:govoplan-files.clear_saved_password.7442260d": "Clear saved password",
"i18n:govoplan-files.clear_saved_token.a9c670aa": "Clear saved token",
"i18n:govoplan-files.clear.719ea396": "Leeren",
"i18n:govoplan-files.clicking_a_file_sets_its_exact_relative_path_as_.590abd24": "Clicking a file sets its exact relative path as the pattern. Preview resolves the pattern against the current managed file space.",
"i18n:govoplan-files.collapse.9cf188d3": "Collapse",
"i18n:govoplan-files.conflict_at.0a91052f": "conflict at",
"i18n:govoplan-files.conflict_s.60cea6d5": "conflict(s)",
"i18n:govoplan-files.connection_credential.178babe0": "Connection / credential",
"i18n:govoplan-files.connector_files.8f351f5d": "Connector files",
"i18n:govoplan-files.connector_folders.960cbb03": "Connector folders",
"i18n:govoplan-files.connector_profile_is_not_configured_for_this_spa.669f57b5": "Connector profile is not configured for this space.",
"i18n:govoplan-files.connector_profile.9e9cd317": "Connector profile",
"i18n:govoplan-files.connector_root.9027235c": "Connector root",
"i18n:govoplan-files.connector_source_already_matched.7eaf5be1": "Connector source already matched",
"i18n:govoplan-files.connector_space_files.dbb0ab24": "Connector space files",
"i18n:govoplan-files.connector.ba358306": "Verbindung",
"i18n:govoplan-files.contained_path_s_will_move_with_the_selected_fol.b786f1a1": "contained path(s) will move with the selected folder(s), but their own names will stay unchanged.",
"i18n:govoplan-files.copied.8e3df45a": "Copied",
"i18n:govoplan-files.copy.92556c6d": "Copy…",
"i18n:govoplan-files.copy.a69c71d0": " copy",
"i18n:govoplan-files.copy.af74f7c5": "Copy",
"i18n:govoplan-files.copying.43b7de98": "Copying",
"i18n:govoplan-files.copymove.d0fa5904": "copyMove",
"i18n:govoplan-files.create_a_folder_below_the_selected_destination.9041ee76": "Create a folder below the selected destination.",
"i18n:govoplan-files.create_folder.97bafaba": "Create Folder",
"i18n:govoplan-files.create_folder.e59f63fa": "Ordner erstellen",
"i18n:govoplan-files.create_space.b50606b4": "Create Space",
"i18n:govoplan-files.created_folder_value.6c3002f9": "Created folder {value0}.",
"i18n:govoplan-files.created_inside.3426a815": "Created inside",
"i18n:govoplan-files.credential_id_and_label_are_required.4cfd1ffd": "Credential id and label are required.",
"i18n:govoplan-files.credential_id.9432a6e1": "Credential id",
"i18n:govoplan-files.credential_mode.23fdd899": "Credential mode",
"i18n:govoplan-files.credential.8bede3ea": "Credential",
"i18n:govoplan-files.current_file.1fbfcf3d": "current file",
"i18n:govoplan-files.current_folder_contents.f9a24fa8": "Current folder contents",
"i18n:govoplan-files.current_folder.5aeab2f0": "Current folder",
"i18n:govoplan-files.delete_selected_item_s.4b6a0023": "Delete selected item(s)?",
"i18n:govoplan-files.delete_value_audit_relevant_blobs_remain_retaine.290af88f": "Delete {value0}? Audit-relevant blobs remain retained.",
"i18n:govoplan-files.delete.f6fdbe48": "Löschen",
"i18n:govoplan-files.denied_connection_ids.a95218b4": "Denied connection IDs",
"i18n:govoplan-files.denied_credential_ids.b6838bc2": "Denied credential IDs",
"i18n:govoplan-files.denied_endpoint_urls.1d8d4369": "Denied endpoint URLs",
"i18n:govoplan-files.denied_paths.d25e4315": "Denied paths",
"i18n:govoplan-files.denied_providers.149b29f4": "Denied providers",
"i18n:govoplan-files.deny_listed_paths.fcde62cc": "Deny-listed paths",
"i18n:govoplan-files.destination_folder.f8ccb63b": "Destination folder",
"i18n:govoplan-files.destination_space.92b63970": "Destination space",
"i18n:govoplan-files.disable_file_connection.3fd940ae": "Disable file connection",
"i18n:govoplan-files.disable_file_credential.49bff614": "Disable file credential",
"i18n:govoplan-files.disable_value_connections_using_it_will_stop_aut.fc3a1708": "Disable {value0}? Connections using it will stop authenticating until another credential is selected.",
"i18n:govoplan-files.disable_value_existing_linked_spaces_will_stop_b.8c1b0ac6": "Disable {value0}? Existing linked spaces will stop browsing until the connection is enabled again.",
"i18n:govoplan-files.disable_value.09485f7f": "Disable {value0}",
"i18n:govoplan-files.disable.9a7d4e06": "Disable",
"i18n:govoplan-files.disabled.f4f4473d": "Disabled",
"i18n:govoplan-files.dms.477e5652": "DMS",
"i18n:govoplan-files.download_zip.7bd0e4b0": "Download ZIP",
"i18n:govoplan-files.download.a479c9c3": "Download",
"i18n:govoplan-files.e_g_invoices.77a73bd1": "e.g. invoices",
"i18n:govoplan-files.edit_file_connection.78698154": "Edit file connection",
"i18n:govoplan-files.edit_file_credential.bc49d44f": "Edit file credential",
"i18n:govoplan-files.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-files.effective_sources_none.9a7c407f": "Effective sources: none",
"i18n:govoplan-files.effective_sources.9a576802": "Effective sources:",
"i18n:govoplan-files.enabled.df174a3f": "Aktiviert",
"i18n:govoplan-files.endpoint_url.65aaaa45": "Endpoint URL",
"i18n:govoplan-files.endpoint.92ec6350": "Endpoint",
"i18n:govoplan-files.expand.9869e506": "Expand",
"i18n:govoplan-files.extracting_files_on_the_server.845a3c1a": "Extracting files on the server…",
"i18n:govoplan-files.file_actions.9e1b94c5": "File actions",
"i18n:govoplan-files.file_connection_created.bdf6e1f6": "File connection created.",
"i18n:govoplan-files.file_connection_disabled_value.059a7de9": "File connection disabled: {value0}.",
"i18n:govoplan-files.file_connection_saved.68c16ee9": "File connection saved.",
"i18n:govoplan-files.file_connections.1e362326": "Dateiverbindungen",
"i18n:govoplan-files.file_credential_created.87c31882": "File credential created.",
"i18n:govoplan-files.file_credential_disabled_value.947538fd": "File credential disabled: {value0}.",
"i18n:govoplan-files.file_credential_saved.d6a1eef8": "File credential saved.",
"i18n:govoplan-files.file_or_pattern_relative_to.0ab4d831": "File or pattern relative to",
"i18n:govoplan-files.file_s.ca61e97f": "file(s),",
"i18n:govoplan-files.file_share_was_not_created.033ac476": "File share was not created.",
"i18n:govoplan-files.file_spaces_and_folders.443d2056": "File spaces and folders",
"i18n:govoplan-files.file.2c3cafa4": "Datei",
"i18n:govoplan-files.files_with_the_same_visible_name_already_exist_i.1bb487b0": "Files with the same visible name already exist in this folder.",
"i18n:govoplan-files.files.6ce6c512": "Dateien",
"i18n:govoplan-files.finalizing_upload.bcce936d": "Finalizing upload…",
"i18n:govoplan-files.find_and_replace.2c6b404b": "Find and replace",
"i18n:govoplan-files.find.df251b06": "Find",
"i18n:govoplan-files.first.49ec0838": "first.",
"i18n:govoplan-files.folder_name.b2ce023b": "Ordnername",
"i18n:govoplan-files.folder_s.10e54730": "folder(s)",
"i18n:govoplan-files.folder.30baa249": "Ordner",
"i18n:govoplan-files.folders_and_files.d107ac99": "Folders and files",
"i18n:govoplan-files.folders.19adc47b": "Ordner",
"i18n:govoplan-files.gb.505a44fa": "GB",
"i18n:govoplan-files.generic.ff7613e5": "Generic",
"i18n:govoplan-files.govoplan_files.b20752ed": "GovOPlaN files",
"i18n:govoplan-files.govoplan_webdav_credentials.bc696d69": "GovOPlaN WebDAV credentials",
"i18n:govoplan-files.group.171a0606": "Group",
"i18n:govoplan-files.groups": "Gruppen",
"i18n:govoplan-files.import.d6fbc9d2": "Import",
"i18n:govoplan-files.inherited.58823af0": "Inherited",
"i18n:govoplan-files.kb.315b39a0": "KB",
"i18n:govoplan-files.kerberos.53c35fb7": "Kerberos",
"i18n:govoplan-files.label.74341e3c": "Label",
"i18n:govoplan-files.leave_empty_to_keep_saved_password.6ec39f5e": "Leave empty to keep saved password",
"i18n:govoplan-files.leave_empty_to_keep_saved_token.e725857b": "Leave empty to keep saved token",
"i18n:govoplan-files.library.b8100f5b": "Library",
"i18n:govoplan-files.linked_file_space.1a614c7d": "Linked file space",
"i18n:govoplan-files.linked_to_1_campaign.5349694f": "Linked to 1 campaign",
"i18n:govoplan-files.linked_to_value_campaigns.1ad7be94": "Linked to {value0} campaigns",
"i18n:govoplan-files.linked_to.ca188768": "linked to",
"i18n:govoplan-files.linked_to.e7ca9e02": "Linked to",
"i18n:govoplan-files.linking.6f640897": "Linking…",
"i18n:govoplan-files.loading_connector_files.02c876e0": "Loading connector files…",
"i18n:govoplan-files.loading_connector_files.e5b0871d": "Loading connector files",
"i18n:govoplan-files.loading_connector_folders.426de3b6": "Loading connector folders",
"i18n:govoplan-files.loading_connector_folders.dde26f3e": "Loading connector folders...",
"i18n:govoplan-files.loading_connector_policy.2b984eec": "Loading connector policy...",
"i18n:govoplan-files.loading_connector_policy.f12ab285": "Loading connector policy",
"i18n:govoplan-files.loading_connector_space.356d83c6": "Loading connector space…",
"i18n:govoplan-files.loading_connector_space.c176e6b8": "Loading connector space",
"i18n:govoplan-files.loading_file_connections.bd68f224": "Loading file connections",
"i18n:govoplan-files.loading_file_connections.ef22dc81": "Loading file connections...",
"i18n:govoplan-files.loading_files.3b142382": "Loading files",
"i18n:govoplan-files.loading_files.bfd27a7e": "Loading files…",
"i18n:govoplan-files.loading.b04ba49f": "Loading...",
"i18n:govoplan-files.lower_connection_limits.4f9838bc": "Lower connection limits",
"i18n:govoplan-files.lower_credential_limits.ec2b72bc": "Lower credential limits",
"i18n:govoplan-files.lower_endpoint_limits.3fd781d5": "Lower endpoint limits",
"i18n:govoplan-files.lower_path_limits.1abcccb1": "Lower path limits",
"i18n:govoplan-files.lower_provider_limits.5816270a": "Lower provider limits",
"i18n:govoplan-files.managed_files.4dd6ad11": "Verwaltete Dateien",
"i18n:govoplan-files.match.c79af5c2": "match.",
"i18n:govoplan-files.mb.6e979f42": "MB",
"i18n:govoplan-files.metadata_json_must_be_an_object.48629f13": "Metadata JSON must be an object.",
"i18n:govoplan-files.metadata_json.b0e4c283": "Metadata JSON",
"i18n:govoplan-files.mode.a7b93d21": "Mode",
"i18n:govoplan-files.modified.19a532c8": "Modified",
"i18n:govoplan-files.more_show_next.f1912318": "more — show next",
"i18n:govoplan-files.move.76cdb950": "Verschieben",
"i18n:govoplan-files.move.8a74a26e": "Move…",
"i18n:govoplan-files.moved.0fc28db0": "Moved",
"i18n:govoplan-files.moving.65cd4dd8": "Moving",
"i18n:govoplan-files.must_stay_in_allowed_paths.dc5b4eb4": "Must stay in allowed paths",
"i18n:govoplan-files.name.709a2322": "Name",
"i18n:govoplan-files.native_default.ba32f7b6": "Native/default",
"i18n:govoplan-files.new_connection.ac979fe4": "New connection",
"i18n:govoplan-files.new_credential.65b2a9b1": "New credential",
"i18n:govoplan-files.new_file_connection.2ec6eb56": "New file connection",
"i18n:govoplan-files.new_file_credential.4c061082": "New file credential",
"i18n:govoplan-files.new_folder.a711999b": "Neuer Ordner",
"i18n:govoplan-files.new_name.9e627c7b": "New name",
"i18n:govoplan-files.new_path_for_value.a9a30c1c": "New path for {value0}",
"i18n:govoplan-files.nextcloud.aab73a3d": "Nextcloud",
"i18n:govoplan-files.nfs.db121865": "NFS",
"i18n:govoplan-files.no_accessible_file_spaces_were_found.abcf5003": "No accessible file spaces were found.",
"i18n:govoplan-files.no_campaign_links.4203c2be": "No campaign links",
"i18n:govoplan-files.no_connector_items.d634e023": "No connector items.",
"i18n:govoplan-files.no_connector_profiles_are_available.9be3be28": "No connector profiles are available.",
"i18n:govoplan-files.no_connector_profiles.03c730ee": "No connector profiles",
"i18n:govoplan-files.no_current_files_match_this_pattern.b84c5836": "No current files match this pattern.",
"i18n:govoplan-files.no_endpoint.d0ea68c4": "No endpoint",
"i18n:govoplan-files.no_file_connections_configured.3ef02049": "No file connections configured.",
"i18n:govoplan-files.no_file_selected.f76f1c1c": "No file selected",
"i18n:govoplan-files.no_file_spaces_available.a81fa3d0": "No file spaces available.",
"i18n:govoplan-files.no_value_available": "Keine {value0} verfügbar.",
"i18n:govoplan-files.no_files_uploaded_all_conflicts_were_skipped.1166f3b5": "No files uploaded; all conflicts were skipped.",
"i18n:govoplan-files.no_folders_yet_choose_the_root_folder.6430304d": "No folders yet. Choose the root folder.",
"i18n:govoplan-files.no_saved_credential.30a5a951": "No saved credential",
"i18n:govoplan-files.no_secret.33c1a339": "No secret",
"i18n:govoplan-files.none.6eef6648": "Keine",
"i18n:govoplan-files.not_sorted.0abc03c5": "not sorted",
"i18n:govoplan-files.ntlm.026eac85": "NTLM",
"i18n:govoplan-files.only_the_visible_name_changes_immutable_blobs_st.01557e71": "Only the visible name changes. Immutable blobs stay stable for audit.",
"i18n:govoplan-files.open_parent_folder_value.8030a7c7": "Open parent folder {value0}",
"i18n:govoplan-files.overwrite_all.a76f2d04": "Overwrite all",
"i18n:govoplan-files.overwrite.0625c54e": "Overwrite",
"i18n:govoplan-files.owner_space.364c4b62": "Owner space",
"i18n:govoplan-files.parent_folder.d8ed4c2a": "Parent folder",
"i18n:govoplan-files.password_environment_variable.0e77673f": "Password environment variable",
"i18n:govoplan-files.password.8be3c943": "Passwort",
"i18n:govoplan-files.path_limited.d32cbef0": "Path-limited",
"i18n:govoplan-files.pattern_preview_results.56cea56c": "Pattern preview results",
"i18n:govoplan-files.policy.bb9cf141": "Richtlinie",
"i18n:govoplan-files.prefix.90eceb01": "Prefix",
"i18n:govoplan-files.preview_rename.bb890b24": "Preview rename",
"i18n:govoplan-files.preview_resolves_to.a8d99432": "Preview resolves to",
"i18n:govoplan-files.preview.f1fbb2b4": "Vorschau",
"i18n:govoplan-files.profile_id_and_label_are_required.6ad85df1": "Profile id and label are required.",
"i18n:govoplan-files.profile_id.25961d68": "Profile id",
"i18n:govoplan-files.provider.7ceee3f3": "Provider",
"i18n:govoplan-files.read_only.9b19a5a2": "Read-only",
"i18n:govoplan-files.refresh.56e3badc": "Refresh",
"i18n:govoplan-files.reload.cce71553": "Neu laden",
"i18n:govoplan-files.remote_connector_space.d8956863": "Remote connector space",
"i18n:govoplan-files.rename_all.1d6b24dc": "Rename all",
"i18n:govoplan-files.rename_item.3d21d6ca": "Rename item",
"i18n:govoplan-files.rename.d3f4cb89": "Umbenennen",
"i18n:govoplan-files.renamed_value_item_s.3c4a40fe": "Renamed {value0} item(s).",
"i18n:govoplan-files.replace_pattern.d98e4c92": "Replace pattern",
"i18n:govoplan-files.replace_the_current_pattern.96f80707": "Replace the current pattern?",
"i18n:govoplan-files.replace_value_with_the_exact_file_value.4a63236f": "Replace \"{value0}\" with the exact file \"{value1}\"?",
"i18n:govoplan-files.replacement.c385e347": "Replacement",
"i18n:govoplan-files.require_smb_signing.5168a83d": "Require SMB signing",
"i18n:govoplan-files.review_individually.19abbe58": "Review individually",
"i18n:govoplan-files.root.e96857c5": "Wurzel",
"i18n:govoplan-files.save_connection.07796d38": "Save connection",
"i18n:govoplan-files.save_credential.2c02a6d2": "Save credential",
"i18n:govoplan-files.save_policy.77d67ce3": "Save policy",
"i18n:govoplan-files.saving.56a2285c": "Saving…",
"i18n:govoplan-files.saving.ae7e8875": "Saving...",
"i18n:govoplan-files.seafile.600192cc": "Seafile",
"i18n:govoplan-files.search_this_folder_pdf_pdf_or_2026_pdf.74dec36a": "Search this folder: *.pdf, **/*.pdf or 2026-??-*.pdf",
"i18n:govoplan-files.search.bce06414": "Suchen",
"i18n:govoplan-files.secret_configured.4dc0f095": "Secret configured",
"i18n:govoplan-files.secret_reference.04ed2221": "Secret reference",
"i18n:govoplan-files.select_a_remote_file_to_sync_into_the_destinatio.80477a47": "Select a remote file to sync into the destination folder.",
"i18n:govoplan-files.select_a_remote_file_to_sync_it_into_this_space_.8e58a5ae": "Select a remote file to sync it into this space owner.",
"i18n:govoplan-files.select_the_space_that_will_receive_the_selected_.0a8bea8f": "Select the space that will receive the selected file(s) or folder(s).",
"i18n:govoplan-files.selected.840fac38": " · Selected",
"i18n:govoplan-files.show_fewer.d94dfbe5": "Show fewer",
"i18n:govoplan-files.size.b7152342": "Größe",
"i18n:govoplan-files.skip.3da47453": "Skip",
"i18n:govoplan-files.smb_authentication.96f7cb83": "SMB authentication",
"i18n:govoplan-files.smb.515d2f88": "SMB",
"i18n:govoplan-files.space_label.ec892726": "Space label",
"i18n:govoplan-files.spaces.9b584b52": "Spaces",
"i18n:govoplan-files.status.bae7d5be": "Status",
"i18n:govoplan-files.suffix.885db975": "Suffix",
"i18n:govoplan-files.sync_to_value_value.29c362ea": "Sync to {value0} / {value1}",
"i18n:govoplan-files.sync.905f6309": "Sync",
"i18n:govoplan-files.synced_new_file.d5e8243d": "Synced new file",
"i18n:govoplan-files.synced_new_version.83784264": "Synced new version",
"i18n:govoplan-files.system.bc0792d8": "System",
"i18n:govoplan-files.target.61ad50a9": "Target",
"i18n:govoplan-files.tb.f8a902b0": "TB",
"i18n:govoplan-files.tenant.3ca93c78": "Tenant",
"i18n:govoplan-files.test_value.6a5d10a5": "Test {value0}",
"i18n:govoplan-files.testing.15ccc832": "Testing...",
"i18n:govoplan-files.the_configured_managed_file_space_is_no_longer_a.5e93b729": "The configured managed file space is no longer available to this user.",
"i18n:govoplan-files.this_attachment_base_path_is_not_connected_to_a_.3962b48f": "This attachment base path is not connected to a managed file space. Choose its folder under",
"i18n:govoplan-files.this_file_connection.edcde997": "this file connection",
"i18n:govoplan-files.this_file_credential.695efb90": "this file credential",
"i18n:govoplan-files.this_folder_is_empty_upload_files_in_the_top_lev.cb6c3a1a": "This folder is empty. Upload files in the top-level Files module.",
"i18n:govoplan-files.this_folder_is_empty_use_upload_or_create_folder.5977d784": "This folder is empty. Use Upload or Create Folder to add content.",
"i18n:govoplan-files.token_environment_variable.5af4a79e": "Token environment variable",
"i18n:govoplan-files.token.a1141eb9": "Token",
"i18n:govoplan-files.unpack_zip_uploads.256fead4": "Unpack ZIP uploads",
"i18n:govoplan-files.unpacking_zip_archive.698095f4": "Unpacking ZIP archive",
"i18n:govoplan-files.unpacking_zip_upload.35019691": "Unpacking ZIP upload…",
"i18n:govoplan-files.up.2038bdec": "Up",
"i18n:govoplan-files.upload_failed_because_the_network_request_could_.360a5ab3": "Upload failed because the network request could not be completed.",
"i18n:govoplan-files.upload_to_value_value.b83a34b4": "Upload to {value0} / {value1}",
"i18n:govoplan-files.upload_was_cancelled.5bab0f9b": "Upload was cancelled.",
"i18n:govoplan-files.upload.8bdf057f": "Hochladen",
"i18n:govoplan-files.uploaded_value_file_s_into_value.d0fa052b": "Uploaded {value0} file(s) into {value1}.",
"i18n:govoplan-files.uploading_files.6536791d": "Uploading files",
"i18n:govoplan-files.uploading_value_file_s.715ba963": "Uploading {value0} file(s)…",
"i18n:govoplan-files.use_pattern.ce54ba3e": "Use pattern",
"i18n:govoplan-files.use_this_folder.0e77d6d1": "Use this folder",
"i18n:govoplan-files.user.9f8a2389": "User",
"i18n:govoplan-files.users": "Benutzer",
"i18n:govoplan-files.username_password.e8ba8896": "Username/password",
"i18n:govoplan-files.username.84c29015": "Benutzername",
"i18n:govoplan-files.value_conflict_s.b3872a48": "{value0} conflict(s)",
"i18n:govoplan-files.value_connector_policy_saved.430d4eca": "{value0} connector policy saved.",
"i18n:govoplan-files.value_connector_policy.0bf7f53b": "{value0} connector policy",
"i18n:govoplan-files.value_file_shown_for_context.3d6e0acb": "{value0}, file shown for context",
"i18n:govoplan-files.value_folder_s_value_file_s.76b92c8c": "{value0} folder(s) · {value1} file(s)",
"i18n:govoplan-files.value_this_selection_would_create_duplicate_visi.26ae34d4": "{value0} this selection would create duplicate visible names in the destination.",
"i18n:govoplan-files.value_upload_conflict_s.3a04f117": "{value0} upload conflict(s)",
"i18n:govoplan-files.value_value_activate_to_sort.3953ba71": "{value0}: {value1}. Activate to sort.",
"i18n:govoplan-files.value_value_file_s_and_value_folder_s.c160e725": "{value0} {value1} file(s) and {value2} folder(s).",
"i18n:govoplan-files.value_value.36bc3a40": "{value0}: {value1}.",
"i18n:govoplan-files.value_value.598aab59": "{value0} -> {value1}",
"i18n:govoplan-files.value_value.c189e8bc": "{value0} ({value1})",
"i18n:govoplan-files.value_value.dca59cc0": "{value0} {value1}",
"i18n:govoplan-files.value.48afe802": "· {value0}",
"i18n:govoplan-files.value_file_connections": "{value0}-Dateiverbindungen",
"i18n:govoplan-files.value_scope": "Geltungsbereich: {value0}",
"i18n:govoplan-files.visible_file_s_were_not_matched.dc497e90": "visible file(s) were not matched.",
"i18n:govoplan-files.webdav.521b4c8b": "WebDAV",
"i18n:govoplan-files.working.13b7bfca": "Wird ausgeführt...",
"i18n:govoplan-files.writable.dd35487a": "Writable"
}
};

View File

@@ -1,4 +1,3 @@
import "./styles/file-manager.css";
export { default } from "./module";
export * from "./module";
export { default as FilesPage } from "./features/files/FilesPage";

View File

@@ -1,32 +1,73 @@
import { createElement, lazy } from "react";
import type { FilesFileExplorerUiCapability, PlatformWebModule } from "@govoplan/core-webui";
import { hasScope, type AdminSectionsUiCapability, type FilesConnectorsUiCapability, type FilesFileExplorerUiCapability, type PlatformWebModule } from "@govoplan/core-webui";
import { FolderTree } from "./features/files/components/FileManagerComponents";
import FileConnectorSettingsPanel from "./features/files/FileConnectorSettingsPanel";
import ManagedFileChooser from "./features/files/components/ManagedFileChooser";
import { listFileSpaces, listFiles, listFolders, resolveFilePatterns, shareFilesWithTarget } from "./api/files";
import { listFileSpaces, listFiles, listFilesDelta, listFolders, resolveFilePatterns, shareFilesWithTarget } from "./api/files";
import { generatedTranslations } from "./i18n/generatedTranslations";
import "./styles/file-manager.css";
const FilesPage = lazy(() => import("./features/files/FilesPage"));
const fileRead = ["files:file:read"];
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
};
const fileConnectorAdminSections: AdminSectionsUiCapability = {
sections: [
{
id: "system-file-connectors",
label: "i18n:govoplan-files.file_connections.1e362326",
group: "SYSTEM",
order: 75,
anyOf: ["system:settings:read", "files:file:admin"],
render: ({ settings, auth }) => createElement(FileConnectorSettingsPanel, {
settings,
scopeType: "system",
canWrite: hasScope(auth, "system:settings:write") || hasScope(auth, "files:file:admin")
})
},
{
id: "tenant-file-connectors",
label: "i18n:govoplan-files.file_connections.1e362326",
group: "TENANT",
order: 65,
anyOf: ["admin:settings:read", "files:file:admin"],
render: ({ settings, auth }) => createElement(FileConnectorSettingsPanel, {
settings,
scopeType: "tenant",
canWrite: hasScope(auth, "admin:settings:write") || hasScope(auth, "files:file:admin")
})
}]
};
export const filesModule: PlatformWebModule = {
id: "files",
label: "Files",
label: "i18n:govoplan-files.files.6ce6c512",
version: "1.0.0",
dependencies: ["access"],
navItems: [{ to: "/files", label: "Files", iconName: "folder", anyOf: fileRead, order: 40 }],
translations,
navItems: [{ to: "/files", label: "i18n:govoplan-files.files.6ce6c512", iconName: "folder", anyOf: fileRead, order: 40 }],
routes: [
{ path: "/files", anyOf: fileRead, order: 40, render: ({ settings, auth }) => createElement(FilesPage, { settings, auth }) }
],
{ path: "/files", anyOf: fileRead, order: 40, render: ({ settings, auth }) => createElement(FilesPage, { settings, auth }) }],
uiCapabilities: {
"files.fileExplorer": {
FolderTree,
ManagedFileChooser,
listFileSpaces,
listFiles,
listFilesDelta,
listFolders,
resolveFilePatterns,
shareFilesWithTarget
} satisfies FilesFileExplorerUiCapability
} satisfies FilesFileExplorerUiCapability,
"files.connectors": {
FileConnectorScopeManager: FileConnectorSettingsPanel
} satisfies FilesConnectorsUiCapability,
"admin.sections": fileConnectorAdminSections
}
};

View File

@@ -35,134 +35,6 @@
background: var(--panel);
}
.file-tree-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
border-right: 1px solid var(--line);
background: linear-gradient(180deg, var(--panel-soft), var(--panel));
}
.file-tree-heading {
flex: 0 0 auto;
padding: 13px 14px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: .05em;
text-transform: uppercase;
}
.file-tree-list {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 0px 0px 16px;
}
.file-tree-space + .file-tree-space {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--line);
}
.file-tree-node-wrap {
display: grid;
grid-template-columns: 26px minmax(0, 1fr);
align-items: center;
gap: 2px;
border-radius: 0px;
}
.file-tree-node,
.file-tree-select,
.file-tree-toggle {
border: 0;
background: transparent;
color: var(--text);
cursor: pointer;
}
.file-tree-toggle {
display: grid;
place-items: center;
width: 26px;
height: 32px;
border-radius: 0px;
color: var(--muted);
}
.file-tree-toggle:disabled {
cursor: default;
opacity: .55;
}
.file-tree-node {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
width: 100%;
padding: 8px 9px;
border-radius: 0px;
font: inherit;
text-align: left;
user-select: none;
}
.file-tree-node span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-tree-root {
font-weight: 800;
}
.file-tree-select {
width: 24px;
height: 24px;
border-radius: 999px;
color: var(--text-strong);
font-weight: 900;
line-height: 1;
}
.file-tree-node-wrap:hover,
.file-tree-node-wrap:focus-within,
.file-tree-node-wrap.is-active,
.file-tree-root:hover:not(:disabled),
.file-tree-root:focus-visible,
.file-tree-root.is-active {
background: var(--line);
outline: none;
}
.file-tree-node:focus-visible,
.file-tree-toggle:focus-visible,
.file-tree-select:focus-visible {
outline: none;
}
.file-tree-node-wrap.is-drop-target,
.file-tree-root.is-drop-target {
background: var(--line);
box-shadow: inset 0 0 0 2px var(--line-dark);
}
.file-tree-node-wrap.is-selected .file-tree-select {
background: #0d6efd;
color: #fff;
}
.file-tree-children {
display: grid;
gap: 1px;
}
.file-list-panel {
display: flex;
flex-direction: column;
@@ -574,6 +446,278 @@
gap: 2px;
}
.file-dialog:has(.connector-sync-grid) {
width: min(820px, 100%);
}
.connector-sync-grid {
display: grid;
grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr);
gap: 16px;
align-items: start;
}
.connector-browser {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
min-height: 320px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel);
}
.connector-browser-toolbar {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
padding: 10px;
border-bottom: 1px solid var(--line);
background: var(--panel-soft);
}
.connector-browser-path {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
font-size: 13px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.connector-browser-error {
margin: 0;
padding: 8px 10px 0;
}
.connector-browser-list {
display: grid;
align-content: start;
min-height: 0;
overflow: auto;
padding: 6px;
}
.connector-browser-row {
display: grid;
grid-template-columns: 22px minmax(0, 1fr);
gap: 10px;
align-items: center;
min-height: 48px;
width: 100%;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text);
cursor: pointer;
font: inherit;
padding: 8px 9px;
text-align: left;
}
.connector-browser-row:hover:not(:disabled),
.connector-browser-row:focus-visible,
.connector-browser-row.is-selected {
background: var(--line);
outline: none;
}
.connector-browser-row.is-selected {
box-shadow: inset 0 0 0 1px var(--line-dark);
}
.connector-browser-row > span {
display: grid;
gap: 2px;
min-width: 0;
}
.connector-browser-row strong,
.connector-browser-row small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.connector-browser-row small {
color: var(--muted);
font-size: 12px;
}
.connector-browser-empty {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 120px;
color: var(--muted);
font-weight: 700;
text-align: center;
}
.connector-space-panel {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
gap: 12px;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
padding: 12px;
}
.connector-space-browser {
min-height: 0;
border-radius: 0;
}
.connector-sync-selection {
display: grid;
gap: 3px;
min-width: 0;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel-soft);
}
.connector-sync-selection strong,
.connector-sync-selection small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.connector-sync-selection small {
color: var(--muted);
}
.file-connector-settings-panel {
display: grid;
gap: 16px;
}
.file-connector-settings-empty,
.file-connector-profile-row,
.file-connector-profile-main,
.file-connector-profile-actions,
.file-connector-settings-section {
min-width: 0;
}
.file-connector-settings-empty {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-weight: 700;
}
.file-connector-profile-list {
display: grid;
gap: 10px;
}
.file-connector-profile-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
}
.file-connector-profile-main {
display: grid;
gap: 3px;
}
.file-connector-profile-main strong,
.file-connector-profile-main small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-connector-profile-main small {
color: var(--muted);
}
.file-connector-test-ok {
color: var(--success-text, var(--muted));
}
.file-connector-test-error {
color: var(--danger-text, var(--muted));
}
.file-connector-profile-actions,
.file-connector-settings-section {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.file-connector-settings-section {
margin: 14px 0;
}
.file-connector-policy-locks {
align-items: flex-start;
}
.file-connector-policy-sources {
color: var(--muted);
font-size: 0.88rem;
overflow-wrap: anywhere;
}
.file-connector-settings-toggle-field {
display: flex;
align-items: end;
min-height: 58px;
}
.file-connector-provider-field {
grid-column: 1 / -1;
min-width: 0;
}
.file-connector-provider-switch {
grid-auto-columns: minmax(86px, 1fr);
overflow-x: auto;
}
.file-connector-provider-switch .segmented-control-option {
min-height: 34px;
padding: 0 10px;
white-space: nowrap;
}
@media (max-width: 760px) {
.file-connector-profile-row {
grid-template-columns: 1fr;
}
.file-connector-profile-actions {
justify-content: flex-start;
}
}
@media (max-width: 760px) {
.connector-sync-grid {
grid-template-columns: 1fr;
}
.connector-browser {
min-height: 260px;
}
}
@media (max-width: 1050px) {
.file-manager-shell {
grid-template-columns: 1fr;
@@ -733,6 +877,17 @@
margin: 14px 14px 0;
}
.managed-file-chooser-loading-frame {
display: flex;
flex: 1 1 auto;
min-height: 0;
}
.managed-file-chooser-loading-frame > .managed-file-chooser-layout {
flex: 1 1 auto;
width: 100%;
}
.managed-file-chooser-footer {
flex: 0 0 auto;
padding: 12px 18px;