146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
/**
|
|
* Settings tab for the form-builder. Surfaces non-definition settings
|
|
* the parent `Form` row persists alongside the JSON definition:
|
|
*
|
|
* - allowedOrigins → array of hostnames the submit API accepts
|
|
* - notifyEmails → mail recipients for new submissions
|
|
* - webhookUrl → optional outbound webhook
|
|
* - targetType → routing target (generic FormSubmission, CRM
|
|
* contact, or entity-row)
|
|
* - fieldMapping → field → target-column mapping for non-generic
|
|
* target types
|
|
*
|
|
* Save semantics live with the parent route action — this is a
|
|
* controlled view that emits onChange callbacks.
|
|
*/
|
|
import type * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { FormField } from "@toolctrl/form-widget";
|
|
|
|
export type FormSettingsValue = {
|
|
allowedOrigins: string[];
|
|
notifyEmails: string[];
|
|
webhookUrl: string;
|
|
targetType: string;
|
|
fieldMapping: Record<string, string>;
|
|
};
|
|
|
|
export type SettingsTabProps = {
|
|
value: FormSettingsValue;
|
|
fields: FormField[];
|
|
onChange: (next: FormSettingsValue) => void;
|
|
};
|
|
|
|
const TARGET_OPTIONS = [
|
|
{ value: "generic", labelKey: "settings.forms.builder.settings.targets.generic" },
|
|
{ value: "crm-contact", labelKey: "settings.forms.builder.settings.targets.crmContact" },
|
|
{ value: "entity-row", labelKey: "settings.forms.builder.settings.targets.entityRow" },
|
|
];
|
|
|
|
function joinLines(values: string[]): string {
|
|
return values.join("\n");
|
|
}
|
|
|
|
function parseLines(raw: string): string[] {
|
|
return raw
|
|
.split(/\n+/u)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0);
|
|
}
|
|
|
|
function parseCsvOrLines(raw: string): string[] {
|
|
return raw
|
|
.split(/[\n,]+/u)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0);
|
|
}
|
|
|
|
export function SettingsTab({ value, fields, onChange }: SettingsTabProps): React.JSX.Element {
|
|
const { t } = useTranslation();
|
|
const showMapping = value.targetType === "crm-contact" || value.targetType === "entity-row";
|
|
return (
|
|
<div className="space-y-4 max-w-2xl">
|
|
<label className="block">
|
|
<span className="block text-xs uppercase tracking-wide text-gray-500">
|
|
{t("settings.forms.builder.settings.allowedOrigins")}
|
|
</span>
|
|
<textarea
|
|
rows={3}
|
|
className="border rounded px-2 py-1 w-full font-mono text-xs"
|
|
value={joinLines(value.allowedOrigins)}
|
|
onChange={(e) => onChange({ ...value, allowedOrigins: parseLines(e.target.value) })}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<span className="block text-xs uppercase tracking-wide text-gray-500">
|
|
{t("settings.forms.builder.settings.notifyEmails")}
|
|
</span>
|
|
<textarea
|
|
rows={2}
|
|
className="border rounded px-2 py-1 w-full font-mono text-xs"
|
|
value={joinLines(value.notifyEmails)}
|
|
onChange={(e) => onChange({ ...value, notifyEmails: parseCsvOrLines(e.target.value) })}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<span className="block text-xs uppercase tracking-wide text-gray-500">
|
|
{t("settings.forms.builder.settings.webhookUrl")}
|
|
</span>
|
|
<input
|
|
className="border rounded px-2 py-1 w-full font-mono text-xs"
|
|
value={value.webhookUrl}
|
|
onChange={(e) => onChange({ ...value, webhookUrl: e.target.value })}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<span className="block text-xs uppercase tracking-wide text-gray-500">{t("settings.forms.builder.settings.targetType")}</span>
|
|
<select
|
|
className="border rounded px-2 py-1 w-full"
|
|
value={value.targetType}
|
|
onChange={(e) => onChange({ ...value, targetType: e.target.value })}
|
|
>
|
|
{TARGET_OPTIONS.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{t(opt.labelKey)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
{showMapping ? (
|
|
<div>
|
|
<span className="block text-xs uppercase tracking-wide text-gray-500 mb-1">{t("settings.forms.builder.settings.fieldMapping")}</span>
|
|
<p className="text-[11px] text-gray-500 mb-2">
|
|
{t("settings.forms.builder.settings.fieldMappingDescription")}
|
|
</p>
|
|
<div className="space-y-1">
|
|
{fields.length === 0 ? (
|
|
<p className="text-sm text-gray-500 italic">{t("settings.forms.builder.settings.noFields")}</p>
|
|
) : (
|
|
fields
|
|
.filter((f) => f.type !== "honeypot")
|
|
.map((f) => (
|
|
<div key={f.key} className="grid grid-cols-[1fr_auto_1fr] items-center gap-2">
|
|
<code className="text-xs text-gray-600">{f.key}</code>
|
|
<span aria-hidden="true">{"→"}</span>
|
|
<input
|
|
className="border rounded px-2 py-1 text-sm"
|
|
placeholder={f.key}
|
|
value={value.fieldMapping[f.key] ?? ""}
|
|
onChange={(e) =>
|
|
onChange({
|
|
...value,
|
|
fieldMapping: { ...value.fieldMapping, [f.key]: e.target.value },
|
|
})
|
|
}
|
|
aria-label={t("settings.forms.builder.settings.targetColumnAria", { fieldKey: f.key })}
|
|
/>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|