Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/formWidget/builder/SettingsTab.tsx
T

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>
);
}