import { Form, useActionData, useLoaderData, useNavigation, useSubmit } from "react-router"; import type { TFunction } from "i18next"; import clsx from "clsx"; import { useState, useEffect, useMemo } from "react"; import type { FormEvent, SetStateAction } from "react"; import { useTranslation } from "react-i18next"; import type { RowHeaderDisplayDto } from "~/application/dtos/data/RowHeaderDisplayDto"; import { PropertyType } from "~/application/enums/entities/PropertyType"; import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary"; import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary"; import ExclamationTriangleIcon from "~/components/ui/icons/ExclamationTriangleIcon"; import { MinusIcon } from "~/components/ui/icons/vendor"; import InputCheckboxInline from "~/components/ui/input/InputCheckboxInline"; import InputSelect from "~/components/ui/input/InputSelect"; import InputSelector from "~/components/ui/input/InputSelector"; import InputText from "~/components/ui/input/InputText"; import Loading from "~/components/ui/loaders/Loading"; import Modal from "~/components/ui/modals/Modal"; import ProgressBar from "~/components/ui/steps/ProgressBar"; import type { StepDto } from "~/components/ui/steps/StepDto"; import TableSimple from "~/components/ui/tables/TableSimple"; import UploadDocuments from "~/components/ui/uploaders/UploadDocument"; import type { EntityWithDetails } from "~/utils/db/entities/entities.db.server"; import type { Rows_Import } from "../routes/Rows_Import.server"; import { appPath } from "~/utils/app/CanonicalUrlUtils"; import { useAppOrAdminData } from "~/utils/data/useAppOrAdminData"; import { useLocaleFormatters } from "~/locale/format"; import { translateEntityTitlePlural, translatePropertyTitle } from "~/utils/i18n/translateLabel"; type RawDataDto = { rows: RawRow[]; columns: RawColumn[]; }; type RawRow = { column: string; value: string }[]; type RawColumn = { column: string; name?: string }; function dryRunSourceKey(tenantId: string, entityName: string) { return `tenants/${tenantId}/imports/dry-run/${entityName.replace(/[^a-zA-Z0-9._-]+/g, "-") || "rows"}-import.csv`; } export function getImportSampleValue(propertyType: PropertyType, index: number, t: (key: string, options?: Record) => string) { switch (propertyType) { case PropertyType.TEXT: return `"${t("models.row.import.sampleValues.text", { number: index })}"`; case PropertyType.NUMBER: return `"123"`; case PropertyType.DATE: return `"2022-12-31"`; case PropertyType.BOOLEAN: return "true"; case PropertyType.SELECT: return `"${t("models.row.import.sampleValues.option", { number: 1 })}"`; case PropertyType.MEDIA: return `"https://via.placeholder.com/600x400"`; default: return ""; } } function createInitialSteps(t: TFunction): StepDto[] { return [ { name: "upload", title: t("models.row.import.steps.uploadCsvFile"), status: "current", }, { name: "map", title: t("models.row.import.steps.mapColumns"), status: "pending", }, { name: "confirm", title: t("models.row.import.steps.confirm"), status: "pending", }, ]; } export default function RowsImportRoute() { const data = useLoaderData(); const { t } = useTranslation(); const [entity] = useState(data.entity); const translatedInitialSteps = useMemo(() => createInitialSteps(t), [t]); const [steps, setSteps] = useState(translatedInitialSteps); const progressSteps = useMemo( () => steps.map((step) => ({ ...step, title: translatedInitialSteps.find((translatedStep) => translatedStep.name === step.name)?.title ?? step.title, })), [steps, translatedInitialSteps] ); const [currentStep, setCurrentStep] = useState(); const [rawData, setRawData] = useState(); const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string; slug: string } | null>(null); useEffect(() => { setCurrentStep(steps.find((s) => s.status === "current")?.name); }, [steps]); function onImport(data: RawDataDto) { const updatedSteps = steps.map((step) => { switch (step.name) { case "upload": return { ...step, status: "completed" }; case "map": return { ...step, status: "current" }; default: return { ...step, status: "pending" }; } }); setRawData(data); setSteps(updatedSteps as StepDto[]); } function onMapped(data: RawDataDto) { const updatedSteps = steps.map((step) => ({ ...step, status: step.name === "confirm" ? "current" : "completed", })); setRawData(data); setSteps(updatedSteps as StepDto[]); } return (
{currentStep === "upload" && ( )} {currentStep === "map" && rawData && } {currentStep === "confirm" && rawData && onImport(rawData)} selectedTenant={selectedTenant} />}
); } function ImportCsv({ entity, onImport, allTenants, selectedTenant, onChangeSelectedTenant, }: { entity: EntityWithDetails; onImport: (rawData: RawDataDto) => void; allTenants: { id: string; name: string; slug: string }[]; selectedTenant: { id: string; name: string; slug: string } | null; onChangeSelectedTenant: (e: { id: string; name: string; slug: string } | null) => void; }) { const { t, i18n } = useTranslation(); const appOrAdminData = useAppOrAdminData(); const [delimiter, setDelimiter] = useState("|"); const [firstRowHasHeaders, setFirstRowHasHeaders] = useState("firstRowHasHeaders"); function onDropped(base64: string, file: File) { const reader = new FileReader(); reader.onload = (e: any) => { const rawData: RawDataDto = { columns: [], rows: [], }; const results = csvToArray(e.target.result); for (let idx = 0; idx < results.length; idx++) { const result = results[idx]; // if (idx === 0 && firstRowHasHeaders === "firstRowHasHeaders") { // continue; // } const rowToImport: RawRow = []; const item: string[] = Object.values(result); item.forEach((value, index) => { let column = t("models.row.import.defaultColumnName", { number: index + 1 }); if (!rawData.columns.find((c) => c.column === column)) { rawData.columns.push({ column }); } if (true) { // replace all double quotes with single quotes value = value.replace(/"/g, ""); } rowToImport.push({ column, value }); }); rawData.rows.push(rowToImport); } onImport(rawData); }; reader.readAsText(file); } function csvToArray(str: string) { let headers: string[] = str.slice(0, str.indexOf("\n")).split(delimiter); let rows = str.slice(str.indexOf("\n") + 1).split("\n"); if (firstRowHasHeaders === "noHeaders") { rows = str.split("\n"); headers = headers.map((_header, index) => t("models.row.import.defaultColumnName", { number: index + 1 })); } const arr = rows.map((row) => { const values = row.split(delimiter); const el = headers.reduce((object: any, header, index) => { object[index] = values[index]; return object; }, {}); return el; }); return arr; } function downloadSample() { const rows = []; // Add header row if first row has headers if (firstRowHasHeaders === "firstRowHasHeaders") { const headerRow = entity.properties.filter((property) => !property.isDefault).map((property) => `"${property.name}"`); rows.push(headerRow); } // Add sample data rows for (let i = 1; i <= 5; i++) { const dataRow = entity.properties.filter((property) => !property.isDefault).map((property) => getImportSampleValue(property.type, i, t)); rows.push(dataRow); } const csvContent = "data:text/csv;charset=utf-8," + rows.map((row) => row.join(delimiter?.toString())).join("\n"); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `${entity.name}-import-sample.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); } return (
setDelimiter(i?.toString() ?? "|")} options={[ { name: t("models.row.import.delimiterOptions.comma"), value: "," }, { name: t("models.row.import.delimiterOptions.pipe"), value: "|" }, { name: t("models.row.import.delimiterOptions.semicolon"), value: ";" }, { name: t("models.row.import.delimiterOptions.tab"), value: "\t" }, ]} />
setFirstRowHasHeaders(i?.toString() ?? "|")} options={[ { name: t("models.row.import.headerOptions.firstRowHasHeaders"), value: "firstRowHasHeaders" }, { name: t("models.row.import.headerOptions.noHeaders"), value: "noHeaders" }, ]} />
{!appOrAdminData.currentTenant && ( { const tenant = allTenants.find((f) => f.id === i?.toString()) ?? null; onChangeSelectedTenant(tenant); }} options={[ { name: t("models.row.import.adminTenantOption"), value: "{null}", }, ...allTenants.map((tenant) => { return { name: tenant.name, value: tenant.id, }; }), ]} /> )}

{t("models.row.import.importingEntity", { entity: translateEntityTitlePlural(t, i18n, entity) })}

{/*
{t("shared.submit")}
*/}
); } type DryRunDiagnostic = { code?: string; message: string; row?: number; rowNumber?: number; column?: string; severity?: string; valuePreview?: string; }; type DryRunPreviewResponse = { status?: string; summary?: { totalRows?: number; acceptedRows?: number; rejectedRows?: number; diagnostics?: DryRunDiagnostic[]; }; rows?: { ok?: boolean; rowNumber?: number; diagnostics?: DryRunDiagnostic[]; warnings?: DryRunDiagnostic[] }[]; diagnostics?: DryRunDiagnostic[]; error?: string; }; type DryRunState = | { status: "idle" } | { status: "pending" } | { status: "success"; signature: string; response: DryRunPreviewResponse } | { status: "error"; signature: string; message: string; diagnostics: DryRunDiagnostic[] }; function redactDryRunMessage(message: string) { return message .replace(/\bauthorization\s*[:=]?\s*(?:bearer\s+)?[^,;\n]+/gi, (match) => { const [label] = match.split(/[:=]/, 1); return `${label.trim()}: [REDACTED]`; }) .replace(/\b(api[_-]?key|bearer|password|secret|token|rawBody)\s*[:=]\s*(?:\{[^}]*\}|[^\s,;]+)/gi, (match) => { const [label] = match.split(/[:=]/, 1); return `${label.trim()}: [REDACTED]`; }); } function translateDryRunDiagnostic(t: ReturnType["t"], diagnostic: DryRunDiagnostic) { const code = getKnownDryRunDiagnosticCode(diagnostic.code); return t(`models.row.import.diagnostics.${code}`, { column: diagnostic.column ?? "", value: diagnostic.valuePreview ?? "", }); } function getKnownDryRunDiagnosticCode(code: unknown) { switch (code) { case "blank_row": case "dry_run_failed": case "duplicate_key": case "invalid_dry_run_request": case "missing_required_field": case "required": return code; default: return "unknown"; } } function normalizeDryRunDiagnostic(diagnostic: DryRunDiagnostic, fallbackRowNumber?: number): DryRunDiagnostic { return { ...diagnostic, rowNumber: diagnostic.rowNumber ?? diagnostic.row ?? fallbackRowNumber, message: redactDryRunMessage(diagnostic.message), valuePreview: diagnostic.valuePreview ? redactDryRunMessage(diagnostic.valuePreview) : diagnostic.valuePreview, }; } function collectDryRunDiagnostics(response: DryRunPreviewResponse): DryRunDiagnostic[] { const responseDiagnostics = response.diagnostics ?? []; const summaryDiagnostics = response.summary?.diagnostics ?? []; const rowDiagnostics = response.rows?.flatMap((row) => [...(row.diagnostics ?? []), ...(row.warnings ?? [])].map((diagnostic) => normalizeDryRunDiagnostic(diagnostic, row.rowNumber))) ?? []; return [...responseDiagnostics, ...summaryDiagnostics].map((diagnostic) => normalizeDryRunDiagnostic(diagnostic)).concat(rowDiagnostics); } function MapFields({ entity, data, onConfirm, selectedTenant, }: { entity: EntityWithDetails; data: RawDataDto; onConfirm: (data: RawDataDto) => void; selectedTenant: { id: string; name: string; slug: string } | null; }) { const { t, i18n } = useTranslation(); const formatters = useLocaleFormatters(); const appOrAdminData = useAppOrAdminData(); const [rawData, setRawData] = useState(data); const [primaryProperty, setPrimaryProperty] = useState(); const [preview, setPreview] = useState(false); const [dryRun, setDryRun] = useState({ status: "idle" }); const uniqueRows = useMemo(() => { if (!primaryProperty) { return rawData.rows; } const seenPrimaryValues = new Set(); const deduplicatedRows: RawRow[] = []; for (const row of rawData.rows) { const primaryValue = row.find((field) => field.column === primaryProperty)?.value; if (!seenPrimaryValues.has(primaryValue)) { seenPrimaryValues.add(primaryValue); deduplicatedRows.push(row); } } return deduplicatedRows; }, [primaryProperty, rawData.rows]); const mappedColumns = useMemo(() => rawData.columns.filter((f) => f.name), [rawData.columns]); const mappedColumnNames = useMemo(() => new Set(mappedColumns.map((column) => column.name).filter((name): name is string => Boolean(name))), [mappedColumns]); const sampleRows = useMemo(() => rawData.rows.slice(0, 5), [rawData.rows]); const dryRunSignature = useMemo(() => JSON.stringify({ columns: rawData.columns, rows: uniqueRows, primaryProperty }), [primaryProperty, rawData.columns, uniqueRows]); const dryRunSummary = dryRun.status === "success" ? dryRun.response.summary : undefined; const dryRunRows = dryRun.status === "success" ? (dryRun.response.rows ?? []) : []; const dryRunDiagnostics = dryRun.status === "success" ? collectDryRunDiagnostics(dryRun.response) : dryRun.status === "error" ? dryRun.diagnostics : []; const dryRunIsStale = dryRun.status === "success" || dryRun.status === "error" ? dryRun.signature !== dryRunSignature : false; const acceptedRows = dryRunSummary?.acceptedRows ?? dryRunRows.filter((row) => row.ok).length; const continueDisabled = mappedColumns.length === 0 || uniqueRows.length === 0 || dryRun.status !== "success" || dryRunIsStale || acceptedRows < 1; async function requestDryRun() { const signature = dryRunSignature; const dryRunEndpoint = appOrAdminData.currentTenant?.slug ? appPath({ tenantSlug: appOrAdminData.currentTenant.slug, path: "internal/imports/dry-run", mode: "host-aware" }) : "/internal/imports/dry-run"; setPreview(true); setDryRun({ status: "pending" }); try { const sourceTenantId = appOrAdminData.currentTenant?.id ?? selectedTenant?.id ?? null; const response = await fetch(dryRunEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ format: "csv", importType: entity.name, source: { tenantId: sourceTenantId, sourceKey: sourceTenantId ? dryRunSourceKey(sourceTenantId, entity.name) : null, fileName: `${entity.name}-import.csv`, contentType: "text/csv", }, fields: entity.properties .filter((property) => !property.isDefault) .map((property) => ({ targetField: property.name, label: translatePropertyTitle(t, i18n, entity.name, property), required: false })), mappings: mappedColumns.map((column) => ({ sourceColumn: column.column, targetField: column.name ?? "", label: column.name ?? column.column, required: false, })), rows: uniqueRows.map((row, index) => ({ rowNumber: index + 1, values: Object.fromEntries(row.map((cell) => [cell.column, cell.value])), })), }), }); const payload = (await response.json().catch(() => ({}))) as DryRunPreviewResponse; if (!response.ok) { setDryRun({ status: "error", signature, message: t("models.row.import.errors.previewFailed"), diagnostics: (payload.diagnostics ?? []).map((diagnostic) => ({ ...diagnostic, message: redactDryRunMessage(diagnostic.message) })), }); return false; } setDryRun({ status: "success", signature, response: payload }); return (payload.summary?.acceptedRows ?? payload.rows?.filter((row) => row.ok).length ?? 0) > 0; } catch { setDryRun({ status: "error", signature, message: t("models.row.import.errors.networkPreview"), diagnostics: [], }); return false; } } async function removeDuplicatesAndConfirm() { if (continueDisabled) { const canContinue = await requestDryRun(); if (!canContinue) { return; } } onConfirm({ ...rawData, rows: uniqueRows }); } function hasBeenMapped(name: string) { return mappedColumnNames.has(name); } return (
{t("models.row.import.mapFieldsFor")}
{t("models.row.import.rowsCount", { count: uniqueRows.length, formattedCount: formatters.number(uniqueRows.length) })}
{rawData.columns.map((column, idx) => { return (
{/*
{column.column}
f === column.column).length > 0} setValue={(i) => { if (i) { setPrimaryProperties([...primaryProperties, column.column]); } else { setPrimaryProperties(primaryProperties.filter((f) => f !== column.column)); } }} />
*/}
{column.column}
{ if (i) { setDryRun({ status: "idle" }); setPrimaryProperty(column.column); } else { setDryRun({ status: "idle" }); setPrimaryProperty(undefined); } }} />
{ setDryRun({ status: "idle" }); setRawData({ ...rawData, columns: rawData.columns.map((c, cIdx) => { if (cIdx === idx) { return { ...c, name: i?.toString() }; } return c; }), }); }} options={[ { name: t("models.row.import.notMapped"), value: undefined, }, ...entity.properties .filter((f) => !f.isDefault) .map((prop) => { return { name: `${translatePropertyTitle(t, i18n, entity.name, prop)} (${prop.name})`, value: prop.type === PropertyType.MEDIA ? prop.name + "[]" : prop.name, disabled: hasBeenMapped(prop.name), }; }), ]} />
{sampleRows.map((row, idxRow) => { return (
{row.find((r) => r.column === column.column)?.value ?? t("shared.notSet")}
); })}
); })}
{uniqueRows.length === 0 ? (
{t("models.row.import.noRows")}
) : ( <>{t("shared.continue")} )}
{t("models.row.import.rowsCount", { count: uniqueRows.length, formattedCount: formatters.number(uniqueRows.length) })}
setPreview(false)}>{t("shared.back")} {uniqueRows.length === 0 ? (
{t("models.row.import.noRows")}
) : ( <>{t("shared.continue")} )}
{dryRun.status === "pending" &&
{t("models.row.import.previewingRows")}
} {dryRunIsStale &&
{t("models.row.import.mappingsChanged")}
} {dryRun.status === "error" && (
{dryRun.message}
)} {dryRun.status === "success" && dryRunSummary && (
{t("models.row.import.dryRunSummary", { accepted: dryRunSummary.acceptedRows ?? acceptedRows, rejected: dryRunSummary.rejectedRows ?? 0, total: dryRunSummary.totalRows ?? uniqueRows.length, formattedAccepted: formatters.number(dryRunSummary.acceptedRows ?? acceptedRows), formattedRejected: formatters.number(dryRunSummary.rejectedRows ?? 0), formattedTotal: formatters.number(dryRunSummary.totalRows ?? uniqueRows.length), })}
)} {dryRunDiagnostics.length > 0 && (
    {dryRunDiagnostics.map((diagnostic, index) => (
  • {(() => { const row = diagnostic.rowNumber ?? diagnostic.row; const formattedRow = typeof row === "number" ? formatters.number(row) : row; return row ? t("models.row.import.diagnosticRow", { formattedRow, row }) : ""; })()} {diagnostic.column ? t("models.row.import.diagnosticColumn", { column: diagnostic.column }) : ""} {translateDryRunDiagnostic(t, diagnostic)}
  • ))}
)} {dryRun.status === "success" && acceptedRows < 1 &&
{t("models.row.import.allPreviewRowsRejected")}
}
{mappedColumns.length === 0 ? (
{t("models.row.import.mapAtLeastOneColumn")}
) : uniqueRows.length === 0 ? (
{t("models.row.import.noRows")}
) : ( f.name) .map((c) => { return { name: c.column, title: c.column, value: (item: RawRow) => item?.find((f) => f.column === c.column)?.value ?? t("shared.notSet"), }; })} /> )}
); } type MediaType = "image" | "video" | "application/pdf" | "application/msword" | "application/zip"; interface MediaDto { title: string; name: string; type: MediaType; file: string; publicUrl: string; } function getMediaType(extension: string): MediaType { switch (extension) { case "jpg": case "jpeg": case "png": case "gif": return "image"; case "mp4": case "mov": case "avi": return "video"; case "pdf": return "application/pdf"; case "doc": case "docx": return "application/msword"; case "zip": return "application/zip"; default: return "image"; } } function getMediaFromProperty(property: { name: string; value: string }): MediaDto | null { if (property.name.includes("[]")) { const extension = property.value.split(".").pop(); if (extension) { const name = property.value.split("/").pop() ?? property.value; const type = getMediaType(extension); const media: MediaDto = { title: name, name, type, file: "", publicUrl: property.value, }; return media; } } return null; } function getImportRowStringValue(item: Rows_Import.ImportRow) { item.properties.forEach((property) => { const media = getMediaFromProperty(property); if (media) { property.value = JSON.stringify(media); } }); return JSON.stringify(item); } export function translateRowImportError(t: TFunction, error?: string | null) { const normalized = error?.trim(); if (!normalized) { return t("models.row.import.errors.rowFailed"); } const requiredMatch = normalized.match(/^(.+):\s*required$/i); if (requiredMatch) { return t("models.row.import.errors.requiredField", { field: requiredMatch[1] }); } if (/^Entity not found\b/i.test(normalized)) { return t("models.row.import.errors.entityNotFound"); } if (/^Row not found$|^Not found$/i.test(normalized) || /\bnot found:\s*/i.test(normalized)) { return t("models.row.import.errors.rowNotFound"); } if (/Row exists, but not in entity/i.test(normalized)) { return t("models.row.import.errors.rowEntityMismatch"); } if (/Row exists, but not in tenant/i.test(normalized)) { return t("models.row.import.errors.rowTenantMismatch"); } if (/don't have access|do not have access|missing permission|unknown permission/i.test(normalized)) { return t("models.row.import.errors.rowAccessDenied"); } if (/Invalid number range/i.test(normalized)) { return t("models.row.import.errors.invalidNumberRange"); } if (/Invalid date range/i.test(normalized)) { return t("models.row.import.errors.invalidDateRange"); } return t("models.row.import.errors.rowFailed"); } function Confirm({ entity, data, onBack, selectedTenant, }: { entity: EntityWithDetails; data: RawDataDto; onBack: () => void; selectedTenant: { id: string; name: string; slug: string } | null; }) { const { t, i18n } = useTranslation(); const formatters = useLocaleFormatters(); const appOrAdminData = useAppOrAdminData(); const [items, setItems] = useState([]); const [headers, setHeaders] = useState[]>([]); const [importTagDialogOpen, setImportTagDialogOpen] = useState(false); const [pendingImportFormData, setPendingImportFormData] = useState(null); const [importTag, setImportTag] = useState(""); const navigation = useNavigation(); const submit = useSubmit(); const actionData = useActionData(); useEffect(() => { const newItems: Rows_Import.ImportRow[] = []; data.rows.forEach((row) => { const newRow: Rows_Import.ImportRow = { properties: [], }; row.forEach((r) => { const column = data.columns.find((f) => f.column === r.column); if (column?.name) { newRow.properties.push({ name: column.name, value: r.value, }); } }); newItems.push(newRow); }); setItems(newItems); }, [data.columns, data.rows]); useEffect(() => { if (actionData?.rows) { setItems(actionData.rows); } }, [actionData]); useEffect(() => { const newHeaders: RowHeaderDisplayDto[] = [ { name: "tenant", title: t("models.tenant.object"), hidden: !!appOrAdminData.currentTenant, value: () => { if (selectedTenant) { return (
{selectedTenant.name} ({selectedTenant.slug})
); } return
{t("models.row.import.adminTenantOption")}
; }, }, ]; data.columns.forEach((column) => { if (column.name) { const property = entity.properties.find((f) => f.name === column.name); newHeaders.push({ name: property?.name ?? "", title: property ? translatePropertyTitle(t, i18n, entity.name, property) : "", value: (i) => i.properties?.find((f) => f.name === column.name)?.value ?? t("shared.notSet"), }); } }); newHeaders.push({ name: "status", title: t("shared.status"), value: (i) => (
{!i.row && navigation.state === "submitting" ? (
{t("models.row.import.importing")}
) : i.error ? (
{t("models.row.import.errorStatus", { error: translateRowImportError(t, i.error) })}
) : i.row ? (
{t("models.row.import.importedStatus", { id: i.row?.id })}
) : null}
), }); setHeaders(newHeaders); // eslint-disable-next-line react-hooks/exhaustive-deps }, [data.columns, entity.properties, t, navigation.state]); function handleSubmit(e: FormEvent) { e.stopPropagation(); e.preventDefault(); const formData = new FormData(e.currentTarget); setPendingImportFormData(formData); setImportTag(t("models.row.import.defaultTag")); setImportTagDialogOpen(true); } function closeImportTagDialog() { setImportTagDialogOpen(false); setPendingImportFormData(null); } function setImportTagDialogState(open: SetStateAction) { const nextOpen = typeof open === "function" ? open(importTagDialogOpen) : open; if (nextOpen) { setImportTagDialogOpen(true); } else { closeImportTagDialog(); } } function confirmImportTag() { if (!pendingImportFormData) { return; } pendingImportFormData.set("tag", importTag); submit(pendingImportFormData, { method: "post" }); closeImportTagDialog(); } const pendingItems = items.filter((f) => !f.row); const pendingItemCount = pendingItems.length; return (
{pendingItems.map((item, idx) => { return ; })}
{t("shared.importRecords", { count: items.length, formattedCount: formatters.number(items.length) })}
{navigation.state === "submitting" ? ( ) : ( { setItems(items.filter((f) => f !== item)); }, hidden: (item) => !!item.row, }, ]} /> )}
{t("shared.back")}
{pendingItemCount === 0 ? (
{t("models.row.import.noRows")}
) : (
{t("shared.importRecords", { count: pendingItemCount, formattedCount: formatters.number(pendingItemCount) })}
)}

{t("models.row.import.tagPromptDescription")}

{t("shared.cancel")} {t("shared.importRecords", { count: pendingItemCount, formattedCount: formatters.number(pendingItemCount) })}
); }