refactor: enhance CSV field mapping logic and improve localization

- Updated `sanitizeCsvFieldMappings` to utilize constants for protected target IDs.
- Refactored connector actions to streamline field mapping processing.
- Added a new test case for sanitization to ensure correct handling of all-protected mappings.
- Improved localization for CSV row count and field types in the UI components.
This commit is contained in:
Javi Aguilar
2026-05-08 09:31:42 +02:00
parent 1fc96e194e
commit cf828b00fe
9 changed files with 49 additions and 20 deletions
+6 -5
View File
@@ -339,13 +339,14 @@ export const duplicateConnectorAction = authenticatedActionClient
})),
};
} else if (source.fieldMappings.length > 0) {
const projected = source.fieldMappings.map((m) => ({
sourceFieldId: m.sourceFieldId,
targetFieldId: m.targetFieldId,
staticValue: m.staticValue ?? undefined,
}));
mappingsInput = {
type: "field",
mappings: source.fieldMappings.map((m) => ({
sourceFieldId: m.sourceFieldId,
targetFieldId: m.targetFieldId,
staticValue: m.staticValue ?? undefined,
})),
mappings: source.type === "csv" ? (sanitizeCsvFieldMappings(projected) ?? []) : projected,
};
}
@@ -20,4 +20,15 @@ describe("sanitizeCsvFieldMappings", () => {
expect(sanitizeCsvFieldMappings(undefined)).toBeUndefined();
expect(sanitizeCsvFieldMappings([])).toBeUndefined();
});
test("returns only the static csv source_type mapping when input is all-protected", () => {
const mappings: TConnectorFieldMappingCreateInput[] = [
{ sourceFieldId: "tenant", targetFieldId: "tenant_id" },
{ sourceFieldId: "type", targetFieldId: "source_type" },
];
expect(sanitizeCsvFieldMappings(mappings)).toEqual([
{ sourceFieldId: "", targetFieldId: "source_type", staticValue: "csv" },
]);
});
});
+7 -3
View File
@@ -1,13 +1,17 @@
import { TConnectorFieldMappingCreateInput } from "@formbricks/types/connector";
import {
CSV_HIDDEN_STATIC_MAPPINGS,
CSV_PROTECTED_TARGET_IDS,
} from "@/modules/ee/unify-feedback/sources/types";
export const sanitizeCsvFieldMappings = (
fieldMappings: TConnectorFieldMappingCreateInput[] | undefined
): TConnectorFieldMappingCreateInput[] | undefined => {
if (!fieldMappings?.length) return undefined;
const userMappings = fieldMappings.filter(
(mapping) => mapping.targetFieldId !== "tenant_id" && mapping.targetFieldId !== "source_type"
const userMappings = fieldMappings.filter((mapping) =>
CSV_PROTECTED_TARGET_IDS.every((id) => mapping.targetFieldId !== id)
);
return [...userMappings, { sourceFieldId: "", targetFieldId: "source_type", staticValue: "csv" }];
return [...userMappings, ...(CSV_HIDDEN_STATIC_MAPPINGS as TConnectorFieldMappingCreateInput[])];
};
+7
View File
@@ -3691,6 +3691,7 @@
"csv_pick_column_placeholder": "Pick a column or set a value…",
"csv_required_fields_missing": "Please map required fields before saving: {fields}",
"csv_response_preview": "Sample: \"{sample}\" → stored as {target}.",
"csv_rows_count": "{count, plural, one {# row} other {# rows}}",
"csv_sample_label": "Sample CSV",
"csv_source_context": "Source Context",
"csv_source_context_hint": "Identifies where this batch of feedback came from.",
@@ -3731,6 +3732,12 @@
"field_id": "Field ID",
"field_label": "Field Label",
"field_type": "Field Type",
"fields": {
"boolean": "Boolean",
"date": "Date",
"number": "Number",
"text": "Text"
},
"formbricks_surveys": "Formbricks Surveys",
"go_to_feedback_directories": "Go to directories settings",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
@@ -44,6 +44,7 @@ import {
import { Switch } from "@/modules/ui/components/switch";
import {
CSV_HIDDEN_STATIC_MAPPINGS,
CSV_PROTECTED_TARGET_IDS,
TCreateConnectorStep,
TFieldMapping,
TFormbricksConnectorForm,
@@ -373,8 +374,9 @@ export const CreateConnectorModal = ({
setIsCreating(true);
// Strip any user-supplied tenant_id and merge hidden static mappings (source_type=csv).
const protectedIds = ["tenant_id", "source_type"];
const userMappings = mappings.filter((m) => protectedIds.every((id) => m.targetFieldId !== id));
const userMappings = mappings.filter((m) =>
CSV_PROTECTED_TARGET_IDS.every((id) => m.targetFieldId !== id)
);
const fieldMappings = [...userMappings, ...CSV_HIDDEN_STATIC_MAPPINGS];
const connectorId = await onCreateConnector({
@@ -50,11 +50,7 @@ export function CsvConnectorUI({
useEffect(() => {
const sourceNameMapping = mappings.find((m) => m.targetFieldId === "source_name");
const current = sourceNameMapping?.staticValue ?? sourceNameMapping?.sourceFieldId;
if (
lastAutoSourceNameRef.current !== undefined &&
current !== undefined &&
current !== lastAutoSourceNameRef.current
) {
if (lastAutoSourceNameRef.current !== undefined && current !== lastAutoSourceNameRef.current) {
userEditedSourceNameRef.current = true;
}
}, [mappings]);
@@ -212,7 +208,11 @@ export function CsvConnectorUI({
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-4 py-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-800">{sourceLabel}</span>
<Badge text={`${csvTotalRows} rows`} type="gray" size="tiny" />
<Badge
text={t("workspace.unify.csv_rows_count", { count: csvTotalRows })}
type="gray"
size="tiny"
/>
</div>
<Button
variant="secondary"
@@ -34,6 +34,7 @@ import {
} from "@/modules/ui/components/select";
import {
CSV_HIDDEN_STATIC_MAPPINGS,
CSV_PROTECTED_TARGET_IDS,
SAMPLE_CSV_COLUMNS,
TFieldMapping,
TFormbricksConnectorForm,
@@ -199,8 +200,9 @@ export const EditConnectorModal = ({
}
setIsUpdating(true);
const protectedIds = ["tenant_id", "source_type"];
const userMappings = mappings.filter((m) => protectedIds.every((id) => m.targetFieldId !== id));
const userMappings = mappings.filter((m) =>
CSV_PROTECTED_TARGET_IDS.every((id) => m.targetFieldId !== id)
);
const fieldMappings = [...userMappings, ...CSV_HIDDEN_STATIC_MAPPINGS];
await onUpdateConnector({
@@ -312,9 +312,9 @@ const computeResponseValuePreview = ({
sampleRow?.[responseValueMapping.sourceFieldId] ??
sourceFields.find((f) => f.id === responseValueMapping.sourceFieldId)?.sampleValue ??
"";
const targetLabel = target.replace("value_", "");
const localizedTarget = t(`workspace.unify.fields.${target.replace("value_", "")}`);
return t("workspace.unify.csv_response_preview", {
sample,
target: targetLabel.charAt(0).toUpperCase() + targetLabel.slice(1),
target: localizedTarget,
});
};
@@ -210,6 +210,8 @@ export const CSV_FIELD_GROUPS = {
],
} as const;
export const CSV_PROTECTED_TARGET_IDS = ["tenant_id", "source_type"] as const;
export const CSV_HIDDEN_STATIC_MAPPINGS: TFieldMapping[] = [
{ sourceFieldId: "", targetFieldId: "source_type", staticValue: "csv" },
];