mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-05 18:39:20 -06:00
chore: merge with epic
This commit is contained in:
@@ -20,7 +20,7 @@ interface DraggableSourceFieldProps {
|
||||
isMapped: boolean;
|
||||
}
|
||||
|
||||
export function DraggableSourceField({ field, isMapped }: DraggableSourceFieldProps) {
|
||||
export const DraggableSourceField = ({ field, isMapped }: DraggableSourceFieldProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: field.id,
|
||||
data: field,
|
||||
@@ -55,8 +55,192 @@ export function DraggableSourceField({ field, isMapped }: DraggableSourceFieldPr
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getMappingStateClass = (isActive: boolean, hasMapping: unknown): string => {
|
||||
if (isActive) return "border-brand-dark bg-slate-100";
|
||||
if (hasMapping) return "border-green-300 bg-green-50";
|
||||
return "border-dashed border-slate-300 bg-slate-50";
|
||||
};
|
||||
|
||||
interface RemoveMappingButtonProps {
|
||||
onClick: () => void;
|
||||
variant: "green" | "blue";
|
||||
}
|
||||
|
||||
const RemoveMappingButton = ({ onClick, variant }: RemoveMappingButtonProps) => {
|
||||
const colorClass = variant === "green" ? "hover:bg-green-100" : "hover:bg-blue-100";
|
||||
const iconClass = variant === "green" ? "text-green-600" : "text-blue-600";
|
||||
return (
|
||||
<button type="button" onClick={onClick} className={`ml-1 rounded p-0.5 ${colorClass}`}>
|
||||
<XIcon className={`h-3 w-3 ${iconClass}`} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface EnumTargetFieldContentProps {
|
||||
field: TTargetField;
|
||||
mappedSourceField: TSourceField | null;
|
||||
mapping: TFieldMapping | null;
|
||||
onRemoveMapping: () => void;
|
||||
onStaticValueChange: (value: string) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
const EnumTargetFieldContent = ({
|
||||
field,
|
||||
mappedSourceField,
|
||||
mapping,
|
||||
onRemoveMapping,
|
||||
onStaticValueChange,
|
||||
t,
|
||||
}: EnumTargetFieldContentProps) => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{field.name}</span>
|
||||
{field.required && <span className="text-xs text-red-500">*</span>}
|
||||
<span className="text-xs text-slate-400">{t("environments.unify.enum")}</span>
|
||||
</div>
|
||||
|
||||
{mappedSourceField && !mapping?.staticValue ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-green-700">← {mappedSourceField.name}</span>
|
||||
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
|
||||
</div>
|
||||
) : (
|
||||
<Select value={mapping?.staticValue || ""} onValueChange={onStaticValueChange}>
|
||||
<SelectTrigger className="h-8 w-full bg-white">
|
||||
<SelectValue placeholder={t("environments.unify.select_a_value")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.enumValues?.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface StringTargetFieldContentProps {
|
||||
field: TTargetField;
|
||||
mappedSourceField: TSourceField | null;
|
||||
mapping: TFieldMapping | null;
|
||||
hasMapping: unknown;
|
||||
onRemoveMapping: () => void;
|
||||
onStaticValueChange: (value: string) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
const StringTargetFieldContent = ({
|
||||
field,
|
||||
mappedSourceField,
|
||||
mapping,
|
||||
hasMapping,
|
||||
onRemoveMapping,
|
||||
onStaticValueChange,
|
||||
t,
|
||||
}: StringTargetFieldContentProps) => {
|
||||
const [isEditingStatic, setIsEditingStatic] = useState(false);
|
||||
const [customValue, setCustomValue] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{field.name}</span>
|
||||
{field.required && <span className="text-xs text-red-500">*</span>}
|
||||
</div>
|
||||
|
||||
{mappedSourceField && !mapping?.staticValue && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-green-700">← {mappedSourceField.name}</span>
|
||||
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mapping?.staticValue && !mappedSourceField && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
|
||||
= “{mapping.staticValue}”
|
||||
</span>
|
||||
<RemoveMappingButton onClick={onRemoveMapping} variant="blue" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditingStatic && !hasMapping && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={customValue}
|
||||
onChange={(e) => setCustomValue(e.target.value)}
|
||||
placeholder={
|
||||
field.exampleStaticValues
|
||||
? `e.g., ${field.exampleStaticValues[0]}`
|
||||
: t("environments.unify.enter_value")
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && customValue.trim()) {
|
||||
onStaticValueChange(customValue.trim());
|
||||
setCustomValue("");
|
||||
setIsEditingStatic(false);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setCustomValue("");
|
||||
setIsEditingStatic(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (customValue.trim()) {
|
||||
onStaticValueChange(customValue.trim());
|
||||
setCustomValue("");
|
||||
}
|
||||
setIsEditingStatic(false);
|
||||
}}
|
||||
className="rounded p-1 text-slate-500 hover:bg-slate-200">
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMapping && !isEditingStatic && (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<span className="text-xs text-slate-400">{t("environments.unify.drop_field_or")}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditingStatic(true)}
|
||||
className="flex items-center gap-1 rounded px-1 py-0.5 text-xs text-slate-500 hover:bg-slate-200">
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
{t("environments.unify.set_value")}
|
||||
</button>
|
||||
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (
|
||||
<>
|
||||
<span className="text-xs text-slate-300">|</span>
|
||||
{field.exampleStaticValues.slice(0, 3).map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
type="button"
|
||||
onClick={() => onStaticValueChange(val)}
|
||||
className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 hover:bg-slate-200">
|
||||
{val}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DroppableTargetFieldProps {
|
||||
field: TTargetField;
|
||||
mappedSourceField: TSourceField | null;
|
||||
@@ -80,177 +264,40 @@ export const DroppableTargetField = ({
|
||||
data: field,
|
||||
});
|
||||
|
||||
const [isEditingStatic, setIsEditingStatic] = useState(false);
|
||||
const [customValue, setCustomValue] = useState("");
|
||||
|
||||
const isActive = isOver || isOverCurrent;
|
||||
const hasMapping = mappedSourceField || mapping?.staticValue;
|
||||
const containerClass = cn(
|
||||
"flex items-center gap-2 rounded-md border p-2 text-sm transition-colors",
|
||||
getMappingStateClass(!!isActive, hasMapping)
|
||||
);
|
||||
|
||||
// Handle enum field type - support both column mapping and static dropdown
|
||||
if (field.type === "enum" && field.enumValues) {
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
`flex items-center gap-2 rounded-md border p-2 text-sm transition-colors ${
|
||||
isActive
|
||||
? "border-brand-dark bg-slate-100"
|
||||
: hasMapping
|
||||
? "border-green-300 bg-green-50"
|
||||
: "border-dashed border-slate-300 bg-slate-50"
|
||||
}`
|
||||
)}>
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{field.name}</span>
|
||||
{field.required && <span className="text-xs text-red-500">*</span>}
|
||||
<span className="text-xs text-slate-400">{t("environments.unify.enum")}</span>
|
||||
</div>
|
||||
|
||||
{mappedSourceField && !mapping?.staticValue ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-green-700">← {mappedSourceField.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemoveMapping}
|
||||
className="ml-1 rounded p-0.5 hover:bg-green-100">
|
||||
<XIcon className="h-3 w-3 text-green-600" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={mapping?.staticValue || ""} onValueChange={onStaticValueChange}>
|
||||
<SelectTrigger className="h-8 w-full bg-white">
|
||||
<SelectValue placeholder={t("environments.unify.select_a_value")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.enumValues.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<div ref={setNodeRef} className={containerClass}>
|
||||
<EnumTargetFieldContent
|
||||
field={field}
|
||||
mappedSourceField={mappedSourceField}
|
||||
mapping={mapping}
|
||||
onRemoveMapping={onRemoveMapping}
|
||||
onStaticValueChange={onStaticValueChange}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle string fields - allow drag & drop OR static value
|
||||
if (field.type === "string") {
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
`flex items-center gap-2 rounded-md border p-2 text-sm transition-colors`,
|
||||
isActive && "border-brand-dark bg-slate-100",
|
||||
!isActive && hasMapping
|
||||
? "border-green-300 bg-green-50"
|
||||
: "border-dashed border-slate-300 bg-slate-50"
|
||||
)}>
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{field.name}</span>
|
||||
{field.required && <span className="text-xs text-red-500">*</span>}
|
||||
</div>
|
||||
|
||||
{/* Show mapped source field */}
|
||||
{mappedSourceField && !mapping?.staticValue && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-green-700">← {mappedSourceField.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemoveMapping}
|
||||
className="ml-1 rounded p-0.5 hover:bg-green-100">
|
||||
<XIcon className="h-3 w-3 text-green-600" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show static value */}
|
||||
{mapping?.staticValue && !mappedSourceField && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
|
||||
= “{mapping.staticValue}”
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemoveMapping}
|
||||
className="ml-1 rounded p-0.5 hover:bg-blue-100">
|
||||
<XIcon className="h-3 w-3 text-blue-600" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show input for entering static value when editing */}
|
||||
{isEditingStatic && !hasMapping && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={customValue}
|
||||
onChange={(e) => setCustomValue(e.target.value)}
|
||||
placeholder={
|
||||
field.exampleStaticValues
|
||||
? `e.g., ${field.exampleStaticValues[0]}`
|
||||
: t("environments.unify.enter_value")
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && customValue.trim()) {
|
||||
onStaticValueChange(customValue.trim());
|
||||
setCustomValue("");
|
||||
setIsEditingStatic(false);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setCustomValue("");
|
||||
setIsEditingStatic(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (customValue.trim()) {
|
||||
onStaticValueChange(customValue.trim());
|
||||
setCustomValue("");
|
||||
}
|
||||
setIsEditingStatic(false);
|
||||
}}
|
||||
className="rounded p-1 text-slate-500 hover:bg-slate-200">
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show example values as quick select OR drop zone */}
|
||||
{!hasMapping && !isEditingStatic && (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<span className="text-xs text-slate-400">{t("environments.unify.drop_field_or")}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditingStatic(true)}
|
||||
className="flex items-center gap-1 rounded px-1 py-0.5 text-xs text-slate-500 hover:bg-slate-200">
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
{t("environments.unify.set_value")}
|
||||
</button>
|
||||
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (
|
||||
<>
|
||||
<span className="text-xs text-slate-300">|</span>
|
||||
{field.exampleStaticValues.slice(0, 3).map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
type="button"
|
||||
onClick={() => onStaticValueChange(val)}
|
||||
className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 hover:bg-slate-200">
|
||||
{val}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={setNodeRef} className={containerClass}>
|
||||
<StringTargetFieldContent
|
||||
field={field}
|
||||
mappedSourceField={mappedSourceField}
|
||||
mapping={mapping}
|
||||
hasMapping={hasMapping}
|
||||
onRemoveMapping={onRemoveMapping}
|
||||
onStaticValueChange={onStaticValueChange}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -261,19 +308,8 @@ export const DroppableTargetField = ({
|
||||
return value;
|
||||
};
|
||||
|
||||
// Default behavior for other field types (timestamp, float64, boolean, jsonb, etc.)
|
||||
const hasDefaultMapping = mappedSourceField || mapping?.staticValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2 text-sm transition-colors",
|
||||
isActive && "border-brand-dark bg-slate-100",
|
||||
!isActive && hasDefaultMapping
|
||||
? "border-green-300 bg-green-50"
|
||||
: "border-dashed border-slate-300 bg-slate-50"
|
||||
)}>
|
||||
<div ref={setNodeRef} className={containerClass}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{field.name}</span>
|
||||
@@ -281,30 +317,23 @@ export const DroppableTargetField = ({
|
||||
<span className="text-xs text-slate-400">({field.type})</span>
|
||||
</div>
|
||||
|
||||
{/* Show mapped source field */}
|
||||
{mappedSourceField && !mapping?.staticValue && (
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<span className="text-xs text-green-700">← {mappedSourceField.name}</span>
|
||||
<button type="button" onClick={onRemoveMapping} className="ml-1 rounded p-0.5 hover:bg-green-100">
|
||||
<XIcon className="h-3 w-3 text-green-600" />
|
||||
</button>
|
||||
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show static value */}
|
||||
{mapping?.staticValue && !mappedSourceField && (
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
|
||||
= {getStaticValueLabel(mapping.staticValue)}
|
||||
</span>
|
||||
<button type="button" onClick={onRemoveMapping} className="ml-1 rounded p-0.5 hover:bg-blue-100">
|
||||
<XIcon className="h-3 w-3 text-blue-600" />
|
||||
</button>
|
||||
<RemoveMappingButton onClick={onRemoveMapping} variant="blue" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show drop zone with preset options */}
|
||||
{!hasDefaultMapping && (
|
||||
{!hasMapping && (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1">
|
||||
<span className="text-xs text-slate-400">{t("environments.unify.drop_a_field_here")}</span>
|
||||
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (
|
||||
|
||||
@@ -52,7 +52,7 @@ export const validateEnumMappings = (
|
||||
if (!mapping.sourceFieldId || mapping.staticValue) continue;
|
||||
|
||||
const targetField = FEEDBACK_RECORD_FIELDS.find((f) => f.id === mapping.targetFieldId);
|
||||
if (!targetField || targetField.type !== "enum" || !targetField.enumValues) continue;
|
||||
if (targetField?.type !== "enum" || !targetField?.enumValues) continue;
|
||||
|
||||
const allowedValues = new Set(targetField.enumValues);
|
||||
const invalidEntries: { row: number; value: string }[] = [];
|
||||
|
||||
@@ -2081,7 +2081,7 @@
|
||||
"enum": "Enum",
|
||||
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
|
||||
"feedback_date": "Aktuelles Datum",
|
||||
"feedback_record_fields": "Feedback-Eintragsfelder",
|
||||
"feedback_record_fields": "Feedback-Datensatzfelder",
|
||||
"feedback_records": "Feedback-Einträge",
|
||||
"field_label": "Feldbezeichnung",
|
||||
"field_type": "Feldtyp",
|
||||
|
||||
Reference in New Issue
Block a user