import { useEffect, useMemo, useState } from "react"; import { Pencil } from "lucide-react"; import { useGuardedNavigate, usePlatformModuleInstalled, usePlatformUiCapability, type FilesFileExplorerUiCapability, type FilesFileSpace } from "@govoplan/core-webui"; import type { ApiSettings } from "../../types"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { PageTitle } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import LockedVersionNotice from "./components/LockedVersionNotice"; import VersionLine from "./components/VersionLine"; import { ToggleSwitch } from "@govoplan/core-webui"; import { DismissibleAlert } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../../components/table/DataGrid"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView"; import { updateNested } from "./utils/draftEditor"; import { AttachmentRulesDataGrid } from "./components/AttachmentRulesOverlay"; import TemplateExpressionEditorDialog from "./components/TemplateExpressionEditorDialog"; import { countIndividualAttachmentRules, countIndividualAttachmentRulesForBasePath, createAttachmentBasePath, ensureAttachmentBasePaths, normalizeAttachmentBasePaths, normalizeAttachmentRules, normalizeAttachmentZipCollection, createAttachmentZipArchive, parseManagedAttachmentSource, removeIndividualAttachmentRulesForBasePath, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentZipArchive, type AttachmentZipCollection } from "./utils/attachments"; import { insertAfter, moveArrayItem, i18nMessage } from "@govoplan/core-webui"; import { getDraftFields, humanizeFieldName } from "./utils/fieldDefinitions"; import { buildTemplatePreviewContext, recipientAddressTemplateFieldOptions } from "./utils/templatePlaceholders"; type PathChooserState = {index: number;}; type IndividualDisableState = {index: number;usageCount: number;}; export default function AttachmentsDataPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) { const navigate = useGuardedNavigate(); const filesModuleInstalled = usePlatformModuleInstalled("files"); const filesFileExplorer = usePlatformUiCapability("files.fileExplorer"); const ManagedFileChooser = filesModuleInstalled ? filesFileExplorer?.ManagedFileChooser : null; const managedFilesAvailable = Boolean(ManagedFileChooser); const listManagedFileSpaces = filesModuleInstalled ? filesFileExplorer?.listFileSpaces : undefined; const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const [pathChooser, setPathChooser] = useState(null); const [fileSpaces, setFileSpaces] = useState([]); const [individualDisable, setIndividualDisable] = useState(null); const [zipNameEditorIndex, setZipNameEditorIndex] = useState(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version, data.campaign?.current_version_id); const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({ settings, campaignId, version, locked, reload, setError, currentStep: "files", unsavedTitle: "i18n:govoplan-campaign.unsaved_attachment_settings.a6045f67", unsavedMessage: "i18n:govoplan-campaign.attachment_settings_have_unsaved_changes_save_th.b9b415bb" }); const attachments = asRecord(displayDraft.attachments); const basePaths = useMemo(() => normalizeAttachmentBasePaths(attachments.base_paths, attachments), [attachments]); const globalRules = useMemo(() => normalizeAttachmentRules(attachments.global), [attachments.global]); const zipConfig = useMemo(() => normalizeAttachmentZipCollection(attachments.zip), [attachments.zip]); const filenameFieldOptions = useMemo(() => buildZipFilenameFieldOptions(displayDraft), [displayDraft]); const passwordFields = useMemo(() => getDraftFields(displayDraft).filter((field) => field.type === "password"), [displayDraft]); const zipArchiveNameValidation = useMemo( () => zipConfig.enabled ? validateZipArchiveNames(zipConfig.archives) : EMPTY_ZIP_ARCHIVE_NAME_VALIDATION, [zipConfig.archives, zipConfig.enabled] ); const canSave = dirty && !locked && Boolean(draft) && !zipArchiveNameValidation.message; const globalSummary = useMemo(() => summarizeAttachmentRules(globalRules), [globalRules]); const individualRulesCount = useMemo(() => countIndividualAttachmentRules(displayDraft.entries), [displayDraft.entries]); const attachmentPreviewEntry = useMemo( () => asArray(asRecord(displayDraft.entries).inline).map(asRecord).find((entry) => entry.active !== false) ?? {}, [displayDraft.entries] ); const attachmentPreviewContext = useMemo( () => buildTemplatePreviewContext(displayDraft, attachmentPreviewEntry), [attachmentPreviewEntry, displayDraft] ); useEffect(() => { if (!listManagedFileSpaces) { setFileSpaces([]); return; } let cancelled = false; void listManagedFileSpaces(settings). then((response) => {if (!cancelled) setFileSpaces(response.spaces);}). catch(() => {if (!cancelled) setFileSpaces([]);}); return () => {cancelled = true;}; }, [listManagedFileSpaces, settings.apiBaseUrl, settings.apiKey, settings.accessToken]); function patchBasePaths(paths: AttachmentBasePath[]) { if (locked) return; const normalized = ensureAttachmentBasePaths(paths); setDraft((current) => { const withPaths = updateNested(current ?? {}, ["attachments", "base_paths"], normalized); return updateNested(withPaths, ["attachments", "base_path"], normalized[0]?.path || "."); }); markDirty(); } function patchBasePath(index: number, patch: Partial) { patchBasePaths(basePaths.map((basePath, currentIndex) => currentIndex === index ? { ...basePath, ...patch } : basePath)); } function addBasePath(afterIndex = basePaths.length - 1) { patchBasePaths(insertAfter(basePaths, afterIndex, createAttachmentBasePath("New attachment source", "."))); } function removeBasePath(index: number) { patchBasePaths(basePaths.filter((_, currentIndex) => currentIndex !== index)); } function moveBasePath(index: number, targetIndex: number) { if (locked || index === targetIndex) return; patchBasePaths(moveArrayItem(basePaths, index, targetIndex)); } function setIndividualEligibility(index: number, checked: boolean) { if (locked) return; if (checked) { patchBasePath(index, { allow_individual: true }); return; } const basePath = basePaths[index]; if (!basePath) return; const usageCount = countIndividualAttachmentRulesForBasePath(displayDraft.entries, basePath); if (usageCount > 0) { setIndividualDisable({ index, usageCount }); return; } patchBasePath(index, { allow_individual: false }); } function confirmIndividualDisable() { if (!individualDisable) return; const basePath = basePaths[individualDisable.index]; if (!basePath) { setIndividualDisable(null); return; } const nextPaths = basePaths.map((item, index) => index === individualDisable.index ? { ...item, allow_individual: false } : item); setDraft((current) => { const source = current ?? {}; const withPaths = updateNested(source, ["attachments", "base_paths"], nextPaths); const withPrimaryPath = updateNested(withPaths, ["attachments", "base_path"], nextPaths[0]?.path || "."); return updateNested(withPrimaryPath, ["entries"], removeIndividualAttachmentRulesForBasePath(asRecord(source).entries, basePath)); }); markDirty(); setIndividualDisable(null); } function patchZipCollection(next: AttachmentZipCollection) { if (locked) return; patch(["attachments", "zip"], next); } function setZipEnabled(enabled: boolean) { const archives = enabled && zipConfig.archives.length === 0 ? [createAttachmentZipArchive("{{local:id}}-attachments.zip", true)] : zipConfig.archives; patchZipCollection({ enabled, archives }); } function patchZipArchive(index: number, archivePatch: Partial) { const archives = zipConfig.archives.map((archive, currentIndex) => { if (currentIndex !== index) return archive; const next = { ...archive, ...archivePatch }; if (archivePatch.password_field !== undefined || archivePatch.password_scope !== undefined) { next.password_mode = "field"; delete next.password; delete next.password_template; } return next; }); patchZipCollection({ ...zipConfig, archives }); } function setStandardZipArchive(index: number) { patchZipCollection({ ...zipConfig, archives: zipConfig.archives.map((archive, currentIndex) => ({ ...archive, standard: currentIndex === index })) }); } function addZipArchive(afterIndex = zipConfig.archives.length - 1) { const archive = createAttachmentZipArchive(`attachments-${zipConfig.archives.length + 1}.zip`, zipConfig.archives.length === 0); patchZipCollection({ ...zipConfig, archives: insertAfter(zipConfig.archives, afterIndex, archive) }); } function removeZipArchive(index: number) { const removed = zipConfig.archives[index]; if (!removed) return; const removedWasStandard = removed.standard; const archives = zipConfig.archives.filter((_, currentIndex) => currentIndex !== index); if (removedWasStandard && archives.length > 0) archives[0] = { ...archives[0], standard: true }; const resetRule = (value: unknown) => { const rule = asRecord(value); const ruleZip = asRecord(rule.zip); return String(ruleZip.archive_id ?? "") === removed.id ? { ...rule, zip: { ...ruleZip, archive_id: "inherit" } } : rule; }; setDraft((current) => { const source = asRecord(current ?? {}); const currentAttachments = asRecord(source.attachments); const currentEntries = asRecord(source.entries); return { ...source, attachments: { ...currentAttachments, zip: { ...zipConfig, archives }, global: asArray(currentAttachments.global).map(resetRule) }, entries: { ...currentEntries, inline: asArray(currentEntries.inline).map((value) => { const entry = asRecord(value); return { ...entry, attachments: asArray(entry.attachments).map(resetRule) }; }) } }; }); markDirty(); } function moveZipArchive(index: number, targetIndex: number) { if (locked || index === targetIndex) return; patchZipCollection({ ...zipConfig, archives: moveArrayItem(zipConfig.archives, index, targetIndex) }); } return (
i18n:govoplan-campaign.attachments.6771ade6
{filesModuleInstalled && }
{error && {error}} {localError && {localError}} {locked && } <>
basePath.id} emptyText="i18n:govoplan-campaign.no_attachment_sources_configured.48664606" emptyAction={ addBasePath(-1)} disabled={locked} label="i18n:govoplan-campaign.add_first_attachment_source.cefa7882" />} className="attachment-sources-table-wrap attachment-sources-table" />
archive.id} emptyText={zipConfig.enabled ? "i18n:govoplan-campaign.no_zip_attachments_configured.29c16424" : "i18n:govoplan-campaign.zip_attachments_are_disabled.6969b41d"} emptyAction={zipConfig.enabled ? addZipArchive(-1)} disabled={locked} label="i18n:govoplan-campaign.add_first_zip_attachment.f64cc332" /> : undefined} className="attachment-zip-table-wrap" />
{zipConfig.enabled && passwordFields.length === 0 && zipConfig.archives.some((archive) => archive.password_enabled) && i18n:govoplan-campaign.no_password_field_exists_add_one_under_fields_wi.2b6d4a7f i18n:govoplan-campaign.password.8be3c943. } {zipArchiveNameValidation.message && {zipArchiveNameValidation.message} }

i18n:govoplan-campaign.archive_names_support_recipient_and_campaign_fie.d5b1b2d1

patch(["attachments", "global"], rules)} />
i18n:govoplan-campaign.base_paths.edf35a97
{basePaths.length}
i18n:govoplan-campaign.global_attachments.438263a1
direct: {globalSummary.direct} i18n:govoplan-campaign.rules.e5d36074 {globalSummary.rules}
i18n:govoplan-campaign.per_recipient_patterns.53da30da
{individualRulesCount}
i18n:govoplan-campaign.upload_support.1c54931c
{managedFilesAvailable ? "i18n:govoplan-campaign.connected_through_files.95007112" : "i18n:govoplan-campaign.manual_paths.8e20627a"}

{managedFilesAvailable ? "i18n:govoplan-campaign.files_are_managed_in_the_top_level_files_module_.4b370222" : filesModuleInstalled ? "i18n:govoplan-campaign.managed_file_browsing_is_unavailable_use_manual_.782867fe" : "i18n:govoplan-campaign.the_files_module_is_not_installed_use_manual_pat.49abc725"}

field.namespace === "local").map(({ name, label }) => ({ name, label }))} globalFields={filenameFieldOptions.filter((field) => field.namespace === "global").map(({ name, label }) => ({ name, label }))} placeholder="attachments.zip" description="i18n:govoplan-campaign.use_a_fixed_filename_or_combine_text_with_recipi.43a091fd" saveLabel="i18n:govoplan-campaign.save_filename.2dbd2273" validate={(value) => zipNameEditorIndex === null ? "" : describeZipArchiveNameValueProblem(zipConfig.archives, zipNameEditorIndex, value)} onCancel={() => setZipNameEditorIndex(null)} onSave={(name) => { if (zipNameEditorIndex === null) return; patchZipArchive(zipNameEditorIndex, { name }); setZipNameEditorIndex(null); }} onAddField={(name) => { if (!draft || locked || !name) return; const existingFields = asArray(draft.fields).map(asRecord); if (existingFields.some((field) => String(field.name || field.id || "").trim() === name)) return; patch(["fields"], [ ...existingFields, { name, label: humanizeFieldName(name), type: "string", required: false, can_override: true }] ); }} canAddField={(name) => !getDraftFields(displayDraft).some((field) => field.name === name)} /> {pathChooser && ManagedFileChooser && setPathChooser(null)} onSelectFolder={(selection) => { const current = basePaths[pathChooser.index]; const folderLabel = selection.folderPath ? selection.folderPath.split("/").filter(Boolean).slice(-1)[0] : selection.space.label; patchBasePath(pathChooser.index, { source: selection.source, path: selection.folderPath || ".", name: !current?.name || current.name === "i18n:govoplan-campaign.new_attachment_source.563145ba" || current.name === "i18n:govoplan-campaign.campaign_files.96e7004b" ? `${selection.space.label}${selection.folderPath ? ` / ${folderLabel}` : ""}` : current.name }); setPathChooser(null); }} /> } setIndividualDisable(null)} />
); } type ZipArchiveColumnContext = { disabled: boolean; archives: AttachmentZipArchive[]; invalidNameIndexes: ReadonlySet; onEditName: (index: number) => void; passwordFields: ReturnType; patchArchive: (index: number, patch: Partial) => void; setStandard: (index: number) => void; addArchive: (afterIndex?: number) => void; moveArchive: (index: number, targetIndex: number) => void; removeArchive: (index: number) => void; }; function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName, passwordFields, patchArchive, setStandard, addArchive, moveArchive, removeArchive }: ZipArchiveColumnContext): DataGridColumn[] { return [ { id: "name", header: "i18n:govoplan-campaign.archive_name.6310f9e1", width: "minmax(360px, 1fr)", resizable: true, sortable: true, filterable: true, sticky: "start", render: (archive, index) => , value: (archive) => archive.name }, { id: "standard", header: "i18n:govoplan-campaign.standard.2dfa6607", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "standard", label: "i18n:govoplan-campaign.standard.2dfa6607" }, { value: "optional", label: "i18n:govoplan-campaign.optional.0c6c4102" }] }, render: (archive, index) => checked && setStandard(index)} />, value: (archive) => archive.standard ? "standard" : "optional" }, { id: "password_enabled", header: "i18n:govoplan-campaign.password.8be3c943", width: 165, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "protected", label: "i18n:govoplan-campaign.protected.28531336" }, { value: "none", label: "i18n:govoplan-campaign.no_password.18518368" }] }, render: (archive, index) => patchArchive(index, { password_enabled: checked })} />, value: (archive) => archive.password_enabled ? "protected" : "none" }, { id: "password_field", header: "i18n:govoplan-campaign.password_field.a1fc8a1c", width: 230, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "", label: "i18n:govoplan-campaign.no_field.1fe00ed4" }, ...passwordFields.map((field) => ({ value: field.name, label: field.label || field.name }))] }, render: (archive, index) => , value: (archive) => archive.password_field }, { id: "password_scope", header: "i18n:govoplan-campaign.value_scope.5e1e16b2", width: 190, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "local", label: "i18n:govoplan-campaign.recipient_local.418a289f" }, { value: "global", label: "i18n:govoplan-campaign.campaign_global.ba944948" }] }, render: (archive, index) => , value: (archive) => archive.password_scope }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 180, sticky: "end", render: (_archive, index) => addArchive(index)} onRemove={() => removeArchive(index)} onMoveUp={index > 0 ? () => moveArchive(index, index - 1) : undefined} onMoveDown={index < archives.length - 1 ? () => moveArchive(index, index + 1) : undefined} addLabel="i18n:govoplan-campaign.add_zip_attachment_below.776dabc1" removeLabel="i18n:govoplan-campaign.remove_zip_attachment.d2bdf662" moveUpLabel="i18n:govoplan-campaign.move_zip_attachment_up.63446ac0" moveDownLabel="i18n:govoplan-campaign.move_zip_attachment_down.4ead4805" /> }]; } type ZipFilenameNamespace = "local" | "global"; type ZipFilenameFieldOption = { namespace: ZipFilenameNamespace; name: string; label: string; }; type ZipArchiveNameValidation = { invalidIndexes: ReadonlySet; message: string; }; const EMPTY_ZIP_ARCHIVE_NAME_VALIDATION: ZipArchiveNameValidation = { invalidIndexes: new Set(), message: "" }; function buildZipFilenameFieldOptions(draft: Record): ZipFilenameFieldOption[] { const fieldDefinitions = new Map(getDraftFields(draft).map((field) => [field.name, field])); const filenameFields = [...fieldDefinitions.values()].filter((field) => field.type !== "password"); const labels = new Map(filenameFields.map((field) => [field.name, field.label || field.name])); const localNames = uniqueStrings(["id", "name", "email", ...recipientAddressTemplateFieldOptions().map((field) => field.name), ...filenameFields.map((field) => field.name)]); const globalNames = uniqueStrings([...filenameFields.map((field) => field.name), ...Object.keys(asRecord(draft.global_values))]). filter((name) => fieldDefinitions.get(name)?.type !== "password"); return [ ...localNames.map((name) => ({ namespace: "local" as const, name, label: labels.get(name) || humanizeFieldName(name) })), ...globalNames.map((name) => ({ namespace: "global" as const, name, label: labels.get(name) || humanizeFieldName(name) }))]; } function describeZipArchiveNameValueProblem(archives: AttachmentZipArchive[], index: number, value: string): string { const normalized = normalizeZipArchiveName(value); if (!normalized) return "i18n:govoplan-campaign.archive_filename_must_not_be_empty.4234b817"; const duplicate = archives.some((archive, currentIndex) => currentIndex !== index && normalizeZipArchiveName(archive.name) === normalized); return duplicate ? "i18n:govoplan-campaign.another_zip_attachment_already_uses_this_effecti.d05eb098" : ""; } function validateZipArchiveNames(archives: AttachmentZipArchive[]): ZipArchiveNameValidation { const invalidIndexes = new Set(); const byName = new Map(); archives.forEach((archive, index) => { const normalized = normalizeZipArchiveName(archive.name); if (!normalized) { invalidIndexes.add(index); return; } const indexes = byName.get(normalized) ?? []; indexes.push(index); byName.set(normalized, indexes); }); for (const indexes of byName.values()) { if (indexes.length < 2) continue; indexes.forEach((index) => invalidIndexes.add(index)); } if (invalidIndexes.size === 0) return EMPTY_ZIP_ARCHIVE_NAME_VALIDATION; const hasEmpty = archives.some((archive, index) => invalidIndexes.has(index) && !archive.name.trim()); const duplicateNames = [...byName.entries()]. filter(([, indexes]) => indexes.length > 1). map(([, indexes]) => archives[indexes[0]]?.name.trim()). filter(Boolean); const parts: string[] = []; if (hasEmpty) parts.push("i18n:govoplan-campaign.archive_filenames_must_not_be_empty.42e91427"); if (duplicateNames.length > 0) parts.push(`Archive filenames must be unique: ${duplicateNames.join(", ")}`); return { invalidIndexes, message: i18nMessage("i18n:govoplan-campaign.value_saving_is_disabled_until_this_is_corrected.cd0a2ca0", { value0: parts.join(". ") }) }; } function normalizeZipArchiveName(value: string): string { const trimmed = value.trim().toLocaleLowerCase(); if (!trimmed) return ""; return trimmed.endsWith(".zip") ? trimmed : `${trimmed}.zip`; } function uniqueStrings(values: string[]): string[] { return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; } type AttachmentSourceColumnContext = { locked: boolean; basePaths: AttachmentBasePath[]; fileSpaces: FilesFileSpace[]; managedFilesAvailable: boolean; patchBasePath: (index: number, patch: Partial) => void; setIndividualEligibility: (index: number, checked: boolean) => void; addBasePath: (afterIndex?: number) => void; moveBasePath: (index: number, targetIndex: number) => void; removeBasePath: (index: number) => void; setPathChooser: (state: PathChooserState | null) => void; }; function attachmentSourceColumns({ locked, basePaths, fileSpaces, managedFilesAvailable, patchBasePath, setIndividualEligibility, addBasePath, moveBasePath, removeBasePath, setPathChooser }: AttachmentSourceColumnContext): DataGridColumn[] { return [ { id: "name", header: "i18n:govoplan-campaign.name.709a2322", width: 220, resizable: true, sortable: true, filterable: true, sticky: "start", render: (basePath, index) => patchBasePath(index, { name: event.target.value })} />, value: (basePath) => basePath.name }, { id: "path", header: "i18n:govoplan-campaign.path.519e3913", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, render: (basePath, index) =>
{ if (!managedFilesAvailable) patchBasePath(index, { path: event.target.value, source: "" }); }} onClick={() => managedFilesAvailable && !locked && setPathChooser({ index })} onKeyDown={(event) => { if (managedFilesAvailable && !locked && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); setPathChooser({ index }); } }} /> {managedFilesAvailable && }
, value: (basePath) => formatAttachmentSourcePath(basePath, fileSpaces) }, { id: "individual", header: "i18n:govoplan-campaign.individual_attachments.8164fc7a", width: 260, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "individual", label: "i18n:govoplan-campaign.individual.a7abed83" }, { value: "global_only", label: "i18n:govoplan-campaign.global_only.4641751c" }] }, render: (basePath, index) => setIndividualEligibility(index, checked)} />, value: (basePath) => basePath.allow_individual ? "individual" : "global_only" }, { id: "unsent_warning", header: "i18n:govoplan-campaign.unsent_warning.11bdfbf7", width: 200, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "warn", label: "i18n:govoplan-campaign.warn.3009d557" }, { value: "off", label: "i18n:govoplan-campaign.off.e3de5ab0" }] }, render: (basePath, index) => patchBasePath(index, { unsent_warning: checked })} />, value: (basePath) => basePath.unsent_warning ? "warn" : "off" }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 180, sticky: "end", render: (_basePath, index) => addBasePath(index)} onRemove={() => removeBasePath(index)} onMoveUp={index > 0 ? () => moveBasePath(index, index - 1) : undefined} onMoveDown={index < basePaths.length - 1 ? () => moveBasePath(index, index + 1) : undefined} addLabel="i18n:govoplan-campaign.add_attachment_source_below.635c6a61" removeLabel="i18n:govoplan-campaign.remove_attachment_source.95855c67" moveUpLabel="i18n:govoplan-campaign.move_attachment_source_up.956ec756" moveDownLabel="i18n:govoplan-campaign.move_attachment_source_down.cbf9ebd8" /> }]; } function formatAttachmentSourcePath(basePath: AttachmentBasePath, spaces: FilesFileSpace[]): string { const parsedSource = parseManagedAttachmentSource(basePath.source); const matchingSpace = parsedSource ? spaces.find((space) => space.owner_type === parsedSource.ownerType && space.owner_id === parsedSource.ownerId) : undefined; const rootLabel = matchingSpace?.label || ( parsedSource?.ownerType === "user" ? "i18n:govoplan-campaign.my_files.71d01a41" : parsedSource?.ownerType === "group" ? "i18n:govoplan-campaign.group_files.1aacad0b" : ""); const relativePath = basePath.path.trim().replace(/^\.\/?$/, "").replace(/^\/+|\/+$/g, ""); if (rootLabel) return `${rootLabel}/${relativePath ? `${relativePath}/` : ""}`; return `${relativePath || "."}/`; }