mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-04 10:19:31 -06:00
chore: merge with polish PR
This commit is contained in:
@@ -39,8 +39,7 @@ export function ConnectorsSection({
|
||||
const handleCreateConnector = async (data: {
|
||||
name: string;
|
||||
type: TConnectorType;
|
||||
surveyId?: string;
|
||||
elementIds?: string[];
|
||||
surveyMappings?: { surveyId: string; elementIds: string[] }[];
|
||||
fieldMappings?: TFieldMapping[];
|
||||
}): Promise<string | undefined> => {
|
||||
const result = await createConnectorWithMappingsAction({
|
||||
@@ -50,9 +49,7 @@ export function ConnectorsSection({
|
||||
type: data.type,
|
||||
},
|
||||
formbricksMappings:
|
||||
data.type === "formbricks" && data.surveyId && data.elementIds?.length
|
||||
? { surveyId: data.surveyId, elementIds: data.elementIds }
|
||||
: undefined,
|
||||
data.type === "formbricks" && data.surveyMappings?.length ? data.surveyMappings : undefined,
|
||||
fieldMappings:
|
||||
data.type !== "formbricks" && data.fieldMappings?.length
|
||||
? data.fieldMappings.map((m) => ({
|
||||
@@ -77,8 +74,7 @@ export function ConnectorsSection({
|
||||
connectorId: string;
|
||||
environmentId: string;
|
||||
name: string;
|
||||
surveyId?: string;
|
||||
elementIds?: string[];
|
||||
surveyMappings?: { surveyId: string; elementIds: string[] }[];
|
||||
fieldMappings?: TFieldMapping[];
|
||||
}) => {
|
||||
const result = await updateConnectorWithMappingsAction({
|
||||
@@ -87,10 +83,7 @@ export function ConnectorsSection({
|
||||
connectorInput: {
|
||||
name: data.name,
|
||||
},
|
||||
formbricksMappings:
|
||||
data.surveyId && data.elementIds?.length
|
||||
? { surveyId: data.surveyId, elementIds: data.elementIds }
|
||||
: undefined,
|
||||
formbricksMappings: data.surveyMappings?.length ? data.surveyMappings : undefined,
|
||||
fieldMappings: data.fieldMappings?.length
|
||||
? data.fieldMappings.map((m) => ({
|
||||
sourceFieldId: m.sourceFieldId || "",
|
||||
@@ -185,7 +178,6 @@ export function ConnectorsSection({
|
||||
open={editingConnector !== null}
|
||||
onOpenChange={(open) => !open && setEditingConnector(null)}
|
||||
onUpdateConnector={handleUpdateConnector}
|
||||
onDeleteConnector={handleDeleteConnector}
|
||||
surveys={initialSurveys}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -41,8 +41,7 @@ interface CreateConnectorModalProps {
|
||||
onCreateConnector: (data: {
|
||||
name: string;
|
||||
type: TConnectorType;
|
||||
surveyId?: string;
|
||||
elementIds?: string[];
|
||||
surveyMappings?: { surveyId: string; elementIds: string[] }[];
|
||||
fieldMappings?: TFieldMapping[];
|
||||
}) => Promise<string | undefined>;
|
||||
surveys: TUnifySurvey[];
|
||||
@@ -72,7 +71,7 @@ const getDialogDescription = (
|
||||
};
|
||||
|
||||
const getNextStepButtonLabel = (type: TConnectorType | null, t: (key: string) => string): string => {
|
||||
if (type === "formbricks") return t("environments.unify.select_elements");
|
||||
if (type === "formbricks") return t("environments.unify.select_questions");
|
||||
if (type === "csv") return t("environments.unify.configure_import");
|
||||
return t("environments.unify.create_mapping");
|
||||
};
|
||||
@@ -88,44 +87,64 @@ const getCreateDisabled = (
|
||||
return !allRequiredMapped;
|
||||
};
|
||||
|
||||
interface HistoricalImportSectionProps {
|
||||
responseCount: number;
|
||||
elementCount: number;
|
||||
totalFeedbackRecords: number;
|
||||
importHistorical: boolean;
|
||||
onImportHistoricalChange: (checked: boolean) => void;
|
||||
interface AggregateImportSectionProps {
|
||||
surveyEntries: {
|
||||
surveyId: string;
|
||||
surveyName: string;
|
||||
responseCount: number;
|
||||
elementCount: number;
|
||||
importHistorical: boolean;
|
||||
}[];
|
||||
onImportHistoricalChange: (surveyId: string, checked: boolean) => void;
|
||||
t: (key: string, options?: Record<string, unknown>) => string;
|
||||
}
|
||||
|
||||
const HistoricalImportSection = ({
|
||||
responseCount,
|
||||
elementCount,
|
||||
totalFeedbackRecords,
|
||||
importHistorical,
|
||||
const AggregateImportSection = ({
|
||||
surveyEntries,
|
||||
onImportHistoricalChange,
|
||||
t,
|
||||
}: HistoricalImportSectionProps) => (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<p className="mb-2 text-xs text-amber-800">
|
||||
{t("environments.unify.existing_responses_info", {
|
||||
responseCount,
|
||||
elementCount,
|
||||
total: totalFeedbackRecords,
|
||||
})}
|
||||
</p>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importHistorical}
|
||||
onChange={(e) => onImportHistoricalChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-amber-300 text-amber-600 focus:ring-amber-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-amber-900">
|
||||
{t("environments.unify.import_existing_responses")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}: AggregateImportSectionProps) => {
|
||||
const totalRecords = surveyEntries.reduce((sum, e) => sum + e.responseCount * e.elementCount, 0);
|
||||
const checkedCount = surveyEntries.filter((e) => e.importHistorical).length;
|
||||
|
||||
const checkedTotal = surveyEntries
|
||||
.filter((e) => e.importHistorical)
|
||||
.reduce((sum, e) => sum + e.responseCount * e.elementCount, 0);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<div className="space-y-2">
|
||||
{surveyEntries.map((entry) => (
|
||||
<label key={entry.surveyId} className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.importHistorical}
|
||||
onChange={(e) => onImportHistoricalChange(entry.surveyId, e.target.checked)}
|
||||
className="h-4 w-4 rounded border-amber-300 text-amber-600 focus:ring-amber-500"
|
||||
/>
|
||||
<span className="text-xs text-amber-800">
|
||||
{t("environments.unify.survey_import_line", {
|
||||
surveyName: entry.surveyName,
|
||||
responseCount: entry.responseCount,
|
||||
questionCount: entry.elementCount,
|
||||
total: entry.responseCount * entry.elementCount,
|
||||
})}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{surveyEntries.length > 1 && (
|
||||
<p className="mt-3 border-t border-amber-200 pt-2 text-xs font-medium text-amber-900">
|
||||
{t("environments.unify.total_feedback_records", {
|
||||
checked: checkedTotal,
|
||||
total: totalRecords,
|
||||
surveyCount: checkedCount,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateConnectorModal = ({
|
||||
open,
|
||||
@@ -147,37 +166,37 @@ export const CreateConnectorModal = ({
|
||||
const [sourceFields, setSourceFields] = useState<TSourceField[]>([]);
|
||||
|
||||
const [selectedSurveyId, setSelectedSurveyId] = useState<string | null>(null);
|
||||
const [selectedElementIds, setSelectedElementIds] = useState<string[]>([]);
|
||||
const [elementIdsBySurvey, setElementIdsBySurvey] = useState<Record<string, string[]>>({});
|
||||
|
||||
const [csvParsedData, setCsvParsedData] = useState<Record<string, string>[]>([]);
|
||||
|
||||
const [enumValidationErrors, setEnumValidationErrors] = useState<TEnumValidationError[]>([]);
|
||||
|
||||
const [responseCount, setResponseCount] = useState<number | null>(null);
|
||||
const [importHistorical, setImportHistorical] = useState(false);
|
||||
const selectedElementIds = selectedSurveyId ? (elementIdsBySurvey[selectedSurveyId] ?? []) : [];
|
||||
|
||||
const [responseCountBySurvey, setResponseCountBySurvey] = useState<Record<string, number | null>>({});
|
||||
const [importHistoricalBySurvey, setImportHistoricalBySurvey] = useState<Record<string, boolean>>({});
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const fetchResponseCount = useCallback(
|
||||
async (surveyId: string) => {
|
||||
setResponseCount(null);
|
||||
if (responseCountBySurvey[surveyId] !== undefined) return;
|
||||
try {
|
||||
const result = await getResponseCountAction({ surveyId, environmentId });
|
||||
if (result?.data !== undefined) {
|
||||
setResponseCount(result.data);
|
||||
setResponseCountBySurvey((prev) => ({ ...prev, [surveyId]: result.data ?? null }));
|
||||
}
|
||||
} catch {
|
||||
setResponseCount(null);
|
||||
setResponseCountBySurvey((prev) => ({ ...prev, [surveyId]: null }));
|
||||
}
|
||||
},
|
||||
[environmentId]
|
||||
[environmentId, responseCountBySurvey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurveyId && selectedType === "formbricks") {
|
||||
fetchResponseCount(selectedSurveyId);
|
||||
} else {
|
||||
setResponseCount(null);
|
||||
}
|
||||
}, [selectedSurveyId, selectedType, fetchResponseCount]);
|
||||
|
||||
@@ -190,9 +209,9 @@ export const CreateConnectorModal = ({
|
||||
setCsvParsedData([]);
|
||||
setEnumValidationErrors([]);
|
||||
setSelectedSurveyId(null);
|
||||
setSelectedElementIds([]);
|
||||
setResponseCount(null);
|
||||
setImportHistorical(false);
|
||||
setElementIdsBySurvey({});
|
||||
setResponseCountBySurvey({});
|
||||
setImportHistoricalBySurvey({});
|
||||
setIsImporting(false);
|
||||
setIsCreating(false);
|
||||
};
|
||||
@@ -217,28 +236,39 @@ export const CreateConnectorModal = ({
|
||||
|
||||
const handleSurveySelect = (surveyId: string | null) => {
|
||||
setSelectedSurveyId(surveyId);
|
||||
setImportHistorical(false);
|
||||
};
|
||||
|
||||
const handleElementToggle = (elementId: string) => {
|
||||
setSelectedElementIds((prev) =>
|
||||
prev.includes(elementId) ? prev.filter((id) => id !== elementId) : [...prev, elementId]
|
||||
);
|
||||
if (!selectedSurveyId) return;
|
||||
setElementIdsBySurvey((prev) => {
|
||||
const current = prev[selectedSurveyId] ?? [];
|
||||
return {
|
||||
...prev,
|
||||
[selectedSurveyId]: current.includes(elementId)
|
||||
? current.filter((id) => id !== elementId)
|
||||
: [...current, elementId],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllElements = (surveyId: string) => {
|
||||
const survey = surveys.find((s) => s.id === surveyId);
|
||||
if (survey) {
|
||||
setSelectedElementIds(
|
||||
survey.elements
|
||||
setElementIdsBySurvey((prev) => ({
|
||||
...prev,
|
||||
[surveyId]: survey.elements
|
||||
.filter((e) => !(UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(e.type))
|
||||
.map((e) => e.id)
|
||||
);
|
||||
.map((e) => e.id),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselectAllElements = () => {
|
||||
setSelectedElementIds([]);
|
||||
if (!selectedSurveyId) return;
|
||||
setElementIdsBySurvey((prev) => ({
|
||||
...prev,
|
||||
[selectedSurveyId]: [],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -249,21 +279,49 @@ export const CreateConnectorModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleHistoricalImport = async (connectorId: string, surveyId: string) => {
|
||||
const getSurveyMappings = () =>
|
||||
Object.entries(elementIdsBySurvey)
|
||||
.filter(([, ids]) => ids.length > 0)
|
||||
.map(([surveyId, elementIds]) => ({ surveyId, elementIds }));
|
||||
|
||||
const handleHistoricalImports = async (connectorId: string) => {
|
||||
const surveysToImport = Object.entries(importHistoricalBySurvey)
|
||||
.filter(([surveyId, checked]) => checked && (elementIdsBySurvey[surveyId]?.length ?? 0) > 0)
|
||||
.map(([surveyId]) => surveyId);
|
||||
|
||||
if (surveysToImport.length === 0) return;
|
||||
|
||||
setIsImporting(true);
|
||||
const importResult = await importHistoricalResponsesAction({ connectorId, environmentId, surveyId });
|
||||
let totalSuccesses = 0;
|
||||
let totalFailures = 0;
|
||||
let totalSkipped = 0;
|
||||
|
||||
for (const surveyId of surveysToImport) {
|
||||
const importResult = await importHistoricalResponsesAction({
|
||||
connectorId,
|
||||
environmentId,
|
||||
surveyId,
|
||||
});
|
||||
|
||||
if (importResult?.data) {
|
||||
totalSuccesses += importResult.data.successes;
|
||||
totalFailures += importResult.data.failures;
|
||||
totalSkipped += importResult.data.skipped;
|
||||
} else {
|
||||
toast.error(getFormattedErrorMessage(importResult));
|
||||
}
|
||||
}
|
||||
|
||||
setIsImporting(false);
|
||||
|
||||
if (importResult?.data) {
|
||||
if (totalSuccesses > 0 || totalFailures > 0) {
|
||||
toast.success(
|
||||
t("environments.unify.historical_import_complete", {
|
||||
successes: importResult.data.successes,
|
||||
failures: importResult.data.failures,
|
||||
skipped: importResult.data.skipped,
|
||||
successes: totalSuccesses,
|
||||
failures: totalFailures,
|
||||
skipped: totalSkipped,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.error(getFormattedErrorMessage(importResult));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -303,16 +361,17 @@ export const CreateConnectorModal = ({
|
||||
|
||||
setIsCreating(true);
|
||||
|
||||
const surveyMappings = getSurveyMappings();
|
||||
|
||||
const connectorId = await onCreateConnector({
|
||||
name: connectorName.trim(),
|
||||
type: selectedType,
|
||||
surveyId: selectedType === "formbricks" ? (selectedSurveyId ?? undefined) : undefined,
|
||||
elementIds: selectedType === "formbricks" ? selectedElementIds : undefined,
|
||||
surveyMappings: selectedType === "formbricks" && surveyMappings.length > 0 ? surveyMappings : undefined,
|
||||
fieldMappings: selectedType !== "formbricks" && mappings.length > 0 ? mappings : undefined,
|
||||
});
|
||||
|
||||
if (connectorId && importHistorical && selectedSurveyId && selectedType === "formbricks") {
|
||||
await handleHistoricalImport(connectorId, selectedSurveyId);
|
||||
if (connectorId && selectedType === "formbricks") {
|
||||
await handleHistoricalImports(connectorId);
|
||||
}
|
||||
|
||||
if (connectorId && selectedType === "csv" && csvParsedData.length > 0) {
|
||||
@@ -329,8 +388,8 @@ export const CreateConnectorModal = ({
|
||||
mappings.some((m) => m.targetFieldId === field.id && (m.sourceFieldId || m.staticValue))
|
||||
);
|
||||
|
||||
const isFormbricksValid =
|
||||
selectedType === "formbricks" && selectedSurveyId && selectedElementIds.length > 0;
|
||||
const hasAnyElementSelections = Object.values(elementIdsBySurvey).some((ids) => ids.length > 0);
|
||||
const isFormbricksValid = selectedType === "formbricks" && hasAnyElementSelections;
|
||||
const isCsvValid = selectedType === "csv" && sourceFields.length > 0;
|
||||
|
||||
const handleLoadSourceFields = () => {
|
||||
@@ -340,11 +399,6 @@ export const CreateConnectorModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const totalFeedbackRecords =
|
||||
responseCount !== null && selectedElementIds.length > 0
|
||||
? responseCount * selectedElementIds.length
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => onOpenChange(true)} size="sm">
|
||||
@@ -358,9 +412,7 @@ export const CreateConnectorModal = ({
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg bg-white/80">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2Icon className="h-8 w-8 animate-spin text-slate-500" />
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
{t("environments.unify.importing_data")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-slate-700">{t("environments.unify.importing_data")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -399,19 +451,30 @@ export const CreateConnectorModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{responseCount !== null &&
|
||||
responseCount > 0 &&
|
||||
selectedElementIds.length > 0 &&
|
||||
totalFeedbackRecords !== null && (
|
||||
<HistoricalImportSection
|
||||
responseCount={responseCount}
|
||||
elementCount={selectedElementIds.length}
|
||||
totalFeedbackRecords={totalFeedbackRecords}
|
||||
importHistorical={importHistorical}
|
||||
onImportHistoricalChange={setImportHistorical}
|
||||
{(() => {
|
||||
const entries = Object.entries(elementIdsBySurvey)
|
||||
.filter(([, ids]) => ids.length > 0)
|
||||
.map(([surveyId, ids]) => ({
|
||||
surveyId,
|
||||
surveyName: surveys.find((s) => s.id === surveyId)?.name ?? surveyId,
|
||||
responseCount: responseCountBySurvey[surveyId] ?? 0,
|
||||
elementCount: ids.length,
|
||||
importHistorical: importHistoricalBySurvey[surveyId] ?? false,
|
||||
}))
|
||||
.filter((e) => e.responseCount > 0);
|
||||
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<AggregateImportSection
|
||||
surveyEntries={entries}
|
||||
onImportHistoricalChange={(surveyId, checked) => {
|
||||
setImportHistoricalBySurvey((prev) => ({ ...prev, [surveyId]: checked }));
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,15 +29,13 @@ interface EditConnectorModalProps {
|
||||
connectorId: string;
|
||||
environmentId: string;
|
||||
name: string;
|
||||
surveyId?: string;
|
||||
elementIds?: string[];
|
||||
surveyMappings?: { surveyId: string; elementIds: string[] }[];
|
||||
fieldMappings?: TFieldMapping[];
|
||||
}) => Promise<void>;
|
||||
onDeleteConnector: (connectorId: string) => Promise<void>;
|
||||
surveys: TUnifySurvey[];
|
||||
}
|
||||
|
||||
function getConnectorIcon(type: TConnectorType) {
|
||||
const getConnectorIcon = (type: TConnectorType) => {
|
||||
switch (type) {
|
||||
case "formbricks":
|
||||
return <GlobeIcon className="h-5 w-5 text-slate-500" />;
|
||||
@@ -46,9 +44,9 @@ function getConnectorIcon(type: TConnectorType) {
|
||||
default:
|
||||
return <GlobeIcon className="h-5 w-5 text-slate-500" />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getConnectorTypeLabelKey(type: TConnectorType): string {
|
||||
const getConnectorTypeLabelKey = (type: TConnectorType): string => {
|
||||
switch (type) {
|
||||
case "formbricks":
|
||||
return "environments.unify.formbricks_surveys";
|
||||
@@ -57,24 +55,35 @@ function getConnectorTypeLabelKey(type: TConnectorType): string {
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function EditConnectorModal({
|
||||
const groupMappingsBySurvey = (
|
||||
mappings: { surveyId: string; elementId: string }[]
|
||||
): Record<string, string[]> => {
|
||||
const grouped: Record<string, string[]> = {};
|
||||
for (const m of mappings) {
|
||||
if (!grouped[m.surveyId]) grouped[m.surveyId] = [];
|
||||
grouped[m.surveyId].push(m.elementId);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
export const EditConnectorModal = ({
|
||||
connector,
|
||||
open,
|
||||
onOpenChange,
|
||||
onUpdateConnector,
|
||||
onDeleteConnector,
|
||||
surveys,
|
||||
}: EditConnectorModalProps) {
|
||||
}: EditConnectorModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [connectorName, setConnectorName] = useState("");
|
||||
const [mappings, setMappings] = useState<TFieldMapping[]>([]);
|
||||
const [sourceFields, setSourceFields] = useState<TSourceField[]>([]);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const [selectedSurveyId, setSelectedSurveyId] = useState<string | null>(null);
|
||||
const [selectedElementIds, setSelectedElementIds] = useState<string[]>([]);
|
||||
const [elementIdsBySurvey, setElementIdsBySurvey] = useState<Record<string, string[]>>({});
|
||||
|
||||
const selectedElementIds = selectedSurveyId ? (elementIdsBySurvey[selectedSurveyId] ?? []) : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (connector) {
|
||||
@@ -83,7 +92,7 @@ export function EditConnectorModal({
|
||||
if (connector.type === "formbricks") {
|
||||
const fbMappings = connector.formbricksMappings;
|
||||
setSelectedSurveyId(fbMappings.length > 0 ? fbMappings[0].surveyId : null);
|
||||
setSelectedElementIds(fbMappings.map((m) => m.elementId));
|
||||
setElementIdsBySurvey(groupMappingsBySurvey(fbMappings));
|
||||
setSourceFields([]);
|
||||
setMappings([]);
|
||||
} else if (connector.type === "csv") {
|
||||
@@ -103,12 +112,12 @@ export function EditConnectorModal({
|
||||
}))
|
||||
);
|
||||
setSelectedSurveyId(null);
|
||||
setSelectedElementIds([]);
|
||||
setElementIdsBySurvey({});
|
||||
} else {
|
||||
setSourceFields([]);
|
||||
setMappings([]);
|
||||
setSelectedSurveyId(null);
|
||||
setSelectedElementIds([]);
|
||||
setElementIdsBySurvey({});
|
||||
}
|
||||
}
|
||||
}, [connector]);
|
||||
@@ -117,9 +126,8 @@ export function EditConnectorModal({
|
||||
setConnectorName("");
|
||||
setMappings([]);
|
||||
setSourceFields([]);
|
||||
setShowDeleteConfirm(false);
|
||||
setSelectedSurveyId(null);
|
||||
setSelectedElementIds([]);
|
||||
setElementIdsBySurvey({});
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
@@ -134,42 +142,54 @@ export function EditConnectorModal({
|
||||
};
|
||||
|
||||
const handleElementToggle = (elementId: string) => {
|
||||
setSelectedElementIds((prev) =>
|
||||
prev.includes(elementId) ? prev.filter((id) => id !== elementId) : [...prev, elementId]
|
||||
);
|
||||
if (!selectedSurveyId) return;
|
||||
setElementIdsBySurvey((prev) => {
|
||||
const current = prev[selectedSurveyId] ?? [];
|
||||
return {
|
||||
...prev,
|
||||
[selectedSurveyId]: current.includes(elementId)
|
||||
? current.filter((id) => id !== elementId)
|
||||
: [...current, elementId],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllElements = (surveyId: string) => {
|
||||
const survey = surveys.find((s) => s.id === surveyId);
|
||||
if (survey) {
|
||||
setSelectedElementIds(survey.elements.map((e) => e.id));
|
||||
setElementIdsBySurvey((prev) => ({
|
||||
...prev,
|
||||
[surveyId]: survey.elements.map((e) => e.id),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselectAllElements = () => {
|
||||
setSelectedElementIds([]);
|
||||
if (!selectedSurveyId) return;
|
||||
setElementIdsBySurvey((prev) => ({
|
||||
...prev,
|
||||
[selectedSurveyId]: [],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!connector || !connectorName.trim()) return;
|
||||
|
||||
const surveyMappings = Object.entries(elementIdsBySurvey)
|
||||
.filter(([, ids]) => ids.length > 0)
|
||||
.map(([surveyId, elementIds]) => ({ surveyId, elementIds }));
|
||||
|
||||
await onUpdateConnector({
|
||||
connectorId: connector.id,
|
||||
environmentId: connector.environmentId,
|
||||
name: connectorName.trim(),
|
||||
surveyId: connector.type === "formbricks" ? (selectedSurveyId ?? undefined) : undefined,
|
||||
elementIds: connector.type === "formbricks" ? selectedElementIds : undefined,
|
||||
surveyMappings:
|
||||
connector.type === "formbricks" && surveyMappings.length > 0 ? surveyMappings : undefined,
|
||||
fieldMappings: connector.type !== "formbricks" && mappings.length > 0 ? mappings : undefined,
|
||||
});
|
||||
handleOpenChange(false);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!connector) return;
|
||||
await onDeleteConnector(connector.id);
|
||||
handleOpenChange(false);
|
||||
};
|
||||
|
||||
if (!connector) return null;
|
||||
|
||||
return (
|
||||
@@ -233,29 +253,13 @@ export function EditConnectorModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<div>
|
||||
{showDeleteConfirm ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-red-600">{t("environments.unify.are_you_sure")}</span>
|
||||
<Button variant="destructive" size="sm" onClick={handleDelete}>
|
||||
{t("environments.unify.yes_delete")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setShowDeleteConfirm(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => setShowDeleteConfirm(true)}>
|
||||
{t("environments.unify.delete_source")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleUpdate}
|
||||
disabled={
|
||||
!connectorName.trim() ||
|
||||
(connector.type === "formbricks" && (!selectedSurveyId || selectedElementIds.length === 0))
|
||||
(connector.type === "formbricks" &&
|
||||
!Object.values(elementIdsBySurvey).some((ids) => ids.length > 0))
|
||||
}>
|
||||
{t("environments.unify.save_changes")}
|
||||
</Button>
|
||||
@@ -263,4 +267,4 @@ export function EditConnectorModal({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
CircleIcon,
|
||||
FileTextIcon,
|
||||
MessageSquareTextIcon,
|
||||
StarIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ChevronRightIcon, FileTextIcon, MessageSquareTextIcon, StarIcon } from "lucide-react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { UNSUPPORTED_CONNECTOR_ELEMENT_TYPES } from "@formbricks/types/connector";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import { getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
@@ -28,7 +19,7 @@ interface FormbricksSurveySelectorProps {
|
||||
onDeselectAllElements: () => void;
|
||||
}
|
||||
|
||||
function getElementIcon(type: string) {
|
||||
const getElementIcon = (type: TSurveyElementTypeEnum) => {
|
||||
switch (type) {
|
||||
case "openText":
|
||||
return <MessageSquareTextIcon className="h-4 w-4 text-slate-500" />;
|
||||
@@ -38,13 +29,13 @@ function getElementIcon(type: string) {
|
||||
default:
|
||||
return <FileTextIcon className="h-4 w-4 text-slate-500" />;
|
||||
}
|
||||
}
|
||||
|
||||
const isUnsupportedType = (type: string): boolean => {
|
||||
return (UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(type);
|
||||
};
|
||||
|
||||
export function FormbricksSurveySelector({
|
||||
const isUnsupportedType = (type: TSurveyElementTypeEnum): boolean => {
|
||||
return UNSUPPORTED_CONNECTOR_ELEMENT_TYPES.includes(type);
|
||||
};
|
||||
|
||||
export const FormbricksSurveySelector = ({
|
||||
surveys,
|
||||
selectedSurveyId,
|
||||
selectedElementIds,
|
||||
@@ -52,9 +43,8 @@ export function FormbricksSurveySelector({
|
||||
onElementToggle,
|
||||
onSelectAllElements,
|
||||
onDeselectAllElements,
|
||||
}: FormbricksSurveySelectorProps) {
|
||||
}: FormbricksSurveySelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [expandedSurveyId, setExpandedSurveyId] = useState<string | null>(null);
|
||||
|
||||
const selectedSurvey = surveys.find((s) => s.id === selectedSurveyId);
|
||||
const supportedElements = selectedSurvey?.elements.filter((e) => !isUnsupportedType(e.type)) ?? [];
|
||||
@@ -62,12 +52,8 @@ export function FormbricksSurveySelector({
|
||||
supportedElements.length > 0 && supportedElements.every((e) => selectedElementIds.includes(e.id));
|
||||
|
||||
const handleSurveyClick = (survey: TUnifySurvey) => {
|
||||
if (selectedSurveyId === survey.id) {
|
||||
setExpandedSurveyId(expandedSurveyId === survey.id ? null : survey.id);
|
||||
} else {
|
||||
if (selectedSurveyId !== survey.id) {
|
||||
onSurveySelect(survey.id);
|
||||
onDeselectAllElements();
|
||||
setExpandedSurveyId(survey.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +94,6 @@ export function FormbricksSurveySelector({
|
||||
) : (
|
||||
surveys.map((survey) => {
|
||||
const isSelected = selectedSurveyId === survey.id;
|
||||
const isExpanded = expandedSurveyId === survey.id;
|
||||
|
||||
return (
|
||||
<div key={survey.id}>
|
||||
@@ -120,25 +105,18 @@ export function FormbricksSurveySelector({
|
||||
? "border-brand-dark bg-slate-50"
|
||||
: "border-slate-200 bg-white hover:border-slate-300"
|
||||
}`}>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-slate-100">
|
||||
{isExpanded ? (
|
||||
<ChevronDownIcon className="h-4 w-4 text-slate-600" />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-4 w-4 text-slate-600" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-slate-900">{survey.name}</span>
|
||||
{getStatusBadge(survey.status)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("environments.unify.n_supported_elements", {
|
||||
{t("environments.unify.n_supported_questions", {
|
||||
count: getSupportedElementCount(survey),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{isSelected && <CheckCircle2Icon className="text-brand-dark h-5 w-5" />}
|
||||
{isSelected && <ChevronRightIcon className="text-brand-dark h-5 w-5 shrink-0" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -150,7 +128,7 @@ export function FormbricksSurveySelector({
|
||||
{/* Right: Element Selection */}
|
||||
<div className="flex flex-col gap-3 overflow-hidden">
|
||||
<div className="flex shrink-0 items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-slate-700">{t("environments.unify.select_elements")}</h4>
|
||||
<h4 className="text-sm font-medium text-slate-700">{t("environments.unify.select_questions")}</h4>
|
||||
{selectedSurvey && supportedElements.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -168,12 +146,12 @@ export function FormbricksSurveySelector({
|
||||
{!selectedSurvey ? (
|
||||
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-300 bg-slate-50">
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("environments.unify.select_a_survey_to_see_elements")}
|
||||
{t("environments.unify.select_a_survey_to_see_questions")}
|
||||
</p>
|
||||
</div>
|
||||
) : selectedSurvey.elements.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-300 bg-slate-50">
|
||||
<p className="text-sm text-slate-500">{t("environments.unify.survey_has_no_elements")}</p>
|
||||
<p className="text-sm text-slate-500">{t("environments.unify.survey_has_no_questions")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 overflow-y-auto pr-1">
|
||||
@@ -210,17 +188,9 @@ export function FormbricksSurveySelector({
|
||||
<p className={`text-sm ${unsupported ? "text-slate-400" : "text-slate-900"}`}>
|
||||
{element.headline}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs ${unsupported ? "text-slate-300" : "text-slate-500"}`}>
|
||||
{getTSurveyElementTypeEnumName(element.type, t) ?? element.type}
|
||||
</span>
|
||||
{element.required && (
|
||||
<span className="text-xs text-red-500">
|
||||
<CircleIcon className="inline h-1.5 w-1.5 fill-current" />{" "}
|
||||
{t("environments.unify.required")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={`text-xs ${unsupported ? "text-slate-300" : "text-slate-500"}`}>
|
||||
{getTSurveyElementTypeEnumName(element.type, t) ?? element.type}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -244,8 +214,8 @@ export function FormbricksSurveySelector({
|
||||
<Trans
|
||||
i18nKey={
|
||||
selectedElementIds.length === 1
|
||||
? "environments.unify.element_selected"
|
||||
: "environments.unify.elements_selected"
|
||||
? "environments.unify.question_selected"
|
||||
: "environments.unify.questions_selected"
|
||||
}
|
||||
values={{ count: selectedElementIds.length }}
|
||||
components={{ strong: <strong /> }}
|
||||
@@ -258,4 +228,4 @@ export function FormbricksSurveySelector({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1935,7 +1935,6 @@ checksums:
|
||||
environments/unify/add_feedback_source: d046fb437ac478ca30b7b59d6afa8e45
|
||||
environments/unify/add_source: 4cc055cbd6312cf0a5db1edf537ce65e
|
||||
environments/unify/allowed_values: 430e0721aa2c52745ef8f8b6918bb7d2
|
||||
environments/unify/are_you_sure: 6d5cd13628a7887711fd0c29f1123652
|
||||
environments/unify/change_file: c5163ac18bf443370228a8ecbb0b07da
|
||||
environments/unify/click_load_sample_csv: 0ee0bf93f10f02863fc658b359706316
|
||||
environments/unify/click_to_upload: 74a7e7d79a88b6bbfd9f22084bffdb9b
|
||||
@@ -1961,42 +1960,38 @@ checksums:
|
||||
environments/unify/csv_max_records: 21ce7adae30821d40a553bcf37f39bbf
|
||||
environments/unify/default_connector_name_csv: ef4060fef24c4fec064987b9d2a9fa4b
|
||||
environments/unify/default_connector_name_formbricks: e7afdf7cc1cd7bcf75e7b5d64903a110
|
||||
environments/unify/delete_source: f1efd5e1c403192a063b761ddfeaf34a
|
||||
environments/unify/deselect_all: facf8871b2e84a454c6bfe40c2821922
|
||||
environments/unify/drop_a_field_here: 884f3025e618e0a5dcbcb5567335d1bb
|
||||
environments/unify/drop_field_or: 5287a8af30f2961ce5a8f14f73ddc353
|
||||
environments/unify/edit_source_connection: eb42476becc8de3de4ca9626828573f0
|
||||
environments/unify/element_selected: f194010dff50242e6f123e0a7da2094c
|
||||
environments/unify/elements_selected: 058a38789415da7fc08b976cdcc1ac66
|
||||
environments/unify/enter_name_for_source: de6d02a0a8ccc99204ad831ca6dcdbd3
|
||||
environments/unify/enter_value: 4f068bb59617975c1e546218373122cd
|
||||
environments/unify/enum: 96fc644f35edd6b1c09d1d503f078acc
|
||||
environments/unify/existing_responses_info: b2c0ed5b06e3be6ea034733ce4967d23
|
||||
environments/unify/feedback_date: 4ada116cc8375dd67483108eeb0ddfe8
|
||||
environments/unify/formbricks_surveys: eba2fce04ee68f02626e5509adf7d66a
|
||||
environments/unify/historical_import_complete: f46f98bf4db63bf2993bfb234dc95f62
|
||||
environments/unify/hub_feedback_record_fields: d8e7b6bb8b7c45d8bd69e5f32193dde4
|
||||
environments/unify/import_csv_data: e5f873b0e6116c5144677acf38607f2e
|
||||
environments/unify/import_existing_responses: bac1f2f27e987fd02b127c6546fc45be
|
||||
environments/unify/import_rows: d2963498a7d2766264c4d67db677e8ff
|
||||
environments/unify/importing_data: a6d4478379a0faee05cd2c10ffe74984
|
||||
environments/unify/importing_historical_data: f5be578704ec26dc4ec573309e9fff20
|
||||
environments/unify/invalid_enum_values: e6ca8740dab72f64e8dc5780b5cffcc6
|
||||
environments/unify/invalid_values_found: 5011dc9c0294a222033f9910ea919b8a
|
||||
environments/unify/load_sample_csv: ad21fa63f4a3df96a5939c753be21f4e
|
||||
environments/unify/n_supported_elements: 6b4d24c8c1da55825529bb890506137e
|
||||
environments/unify/n_supported_questions: d75413d386441b5eb137a1ea191e4bd9
|
||||
environments/unify/no_source_fields_loaded: a597b1d16262cbe897001046eb3ff640
|
||||
environments/unify/no_sources_connected: 0e8a5612530bfc82091091f40f95012f
|
||||
environments/unify/no_surveys_found: 649a2f29b4c34525778d9177605fb326
|
||||
environments/unify/optional: 396fb9a0472daf401c392bdc3e248943
|
||||
environments/unify/or_drag_and_drop: 6c7d6b05d39dcbfc710d35fcab25cb8c
|
||||
environments/unify/question_selected: b9ff13b6212874258da911867932dc7d
|
||||
environments/unify/question_type_not_supported: 8d9f7554e3b509dfd5307d8d1fef08d7
|
||||
environments/unify/questions_selected: 1f13d6fecafa2ce5ea9e6d07078a1d38
|
||||
environments/unify/required: 04d7fb6f37ffe0a6ca97d49e2a8b6eb5
|
||||
environments/unify/save_changes: 53dd9f4f0a4accc822fa5c1f2f6d118a
|
||||
environments/unify/select_a_survey_to_see_elements: e549e92e8e2fda4fc6cfc62661a4b328
|
||||
environments/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20
|
||||
environments/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862
|
||||
environments/unify/select_all: eedc7cdb02de467c15dc418a066a77f2
|
||||
environments/unify/select_elements: c336db5308ff54b1dd8b717fad7dbaff
|
||||
environments/unify/select_questions: 13c79b8c284423eb6140534bf2137e56
|
||||
environments/unify/select_source_type_description: fd7e3c49b81f8e89f294c8fd94efcdfc
|
||||
environments/unify/select_source_type_prompt: c3fce7d908ee62b9e1b7fab1b17606d7
|
||||
environments/unify/select_survey: bac52e59c7847417bef6fe7b7096b475
|
||||
@@ -2012,18 +2007,19 @@ checksums:
|
||||
environments/unify/source_name: 157675beca12efcd8ec512c5256b1a61
|
||||
environments/unify/source_type_cannot_be_changed: bb5232c6e92df7f88731310fabbb1eb1
|
||||
environments/unify/sources: ecbbe6e49baa335c5afd7b04b609d006
|
||||
environments/unify/status_active: 3e1ec025c4a50830bbb9ad57a176630a
|
||||
environments/unify/status_active: 3de9afebcb9d4ce8ac42e14995f79ffd
|
||||
environments/unify/status_completed: 0e4bbce9985f25eb673d9a054c8d5334
|
||||
environments/unify/status_draft: e8a92958ad300aacfe46c2bf6644927e
|
||||
environments/unify/status_error: 3c95bcb32c2104b99a46f5b3dd015248
|
||||
environments/unify/status_paused: edb1f7b7219e1c9b7aa67159090d6991
|
||||
environments/unify/survey_has_no_elements: 0379106932976c0a61119a20992d4b18
|
||||
environments/unify/survey_has_no_questions: c08514b6bce5eb464a4492239be5934d
|
||||
environments/unify/survey_import_line: 63fa0ea1d7daa3ba333436fbc65f8b19
|
||||
environments/unify/total_feedback_records: 8962087650b62e4a12b81e7d09317ffa
|
||||
environments/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5
|
||||
environments/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396
|
||||
environments/unify/updated_at: 8fdb85248e591254973403755dcc3724
|
||||
environments/unify/upload_csv_data_description: 777ed9a77b45cf399f389a73ac499560
|
||||
environments/unify/upload_csv_file: b77797b68cb46a614b3adaa4db24d4c2
|
||||
environments/unify/yes_delete: 7a260e784409a9112f77d213754cd3e0
|
||||
environments/workspace/api_keys/add_api_key: 3c7633bae18a6e19af7a5af12f9bc3da
|
||||
environments/workspace/api_keys/api_key: ce825fec5b3e1f8e27c45b1a63619985
|
||||
environments/workspace/api_keys/api_key_copied_to_clipboard: daeeac786ba09ffa650e206609b88f9c
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logger } from "@formbricks/logger";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import {
|
||||
TConnectorWithMappings,
|
||||
THubFieldType,
|
||||
ZConnectorCreateInput,
|
||||
ZConnectorFieldMappingCreateInput,
|
||||
ZConnectorUpdateInput,
|
||||
@@ -71,10 +72,10 @@ export const deleteConnectorAction = authenticatedActionClient
|
||||
}
|
||||
);
|
||||
|
||||
const resolveFormbricksMappingsInput = async (
|
||||
const resolveSurveyMappings = async (
|
||||
surveyId: string,
|
||||
elementIds: string[]
|
||||
): Promise<TMappingsInput> => {
|
||||
): Promise<{ surveyId: string; elementId: string; hubFieldType: THubFieldType }[]> => {
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
@@ -83,7 +84,7 @@ const resolveFormbricksMappingsInput = async (
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const elementMap = new Map(elements.map((el) => [el.id, el]));
|
||||
|
||||
const mappings = elementIds
|
||||
return elementIds
|
||||
.filter((elementId) => {
|
||||
if (elementMap.has(elementId)) return true;
|
||||
logger.warn({ surveyId, elementId }, "Skipping unknown elementId when building connector mappings");
|
||||
@@ -97,19 +98,26 @@ const resolveFormbricksMappingsInput = async (
|
||||
hubFieldType: getHubFieldTypeFromElementType(element.type),
|
||||
};
|
||||
});
|
||||
|
||||
return { type: "formbricks", mappings };
|
||||
};
|
||||
|
||||
const resolveFormbricksMappingsInput = async (
|
||||
entries: { surveyId: string; elementIds: string[] }[]
|
||||
): Promise<TMappingsInput> => {
|
||||
const allMappings = await Promise.all(
|
||||
entries.map(({ surveyId, elementIds }) => resolveSurveyMappings(surveyId, elementIds))
|
||||
);
|
||||
return { type: "formbricks", mappings: allMappings.flat() };
|
||||
};
|
||||
|
||||
const ZFormbricksSurveyMapping = z.object({
|
||||
surveyId: ZId,
|
||||
elementIds: z.array(z.string()).min(1),
|
||||
});
|
||||
|
||||
const ZCreateConnectorWithMappingsAction = z.object({
|
||||
environmentId: ZId,
|
||||
connectorInput: ZConnectorCreateInput,
|
||||
formbricksMappings: z
|
||||
.object({
|
||||
surveyId: ZId,
|
||||
elementIds: z.array(z.string()).min(1),
|
||||
})
|
||||
.optional(),
|
||||
formbricksMappings: z.array(ZFormbricksSurveyMapping).min(1).optional(),
|
||||
fieldMappings: z.array(ZConnectorFieldMappingCreateInput).optional(),
|
||||
});
|
||||
|
||||
@@ -144,16 +152,17 @@ export const createConnectorWithMappingsAction = authenticatedActionClient
|
||||
|
||||
const { formbricksMappings, fieldMappings } = parsedInput;
|
||||
|
||||
if (formbricksMappings) {
|
||||
const organizationIdFromSurvey = await getOrganizationIdFromSurveyId(formbricksMappings.surveyId);
|
||||
if (organizationIdFromSurvey !== organizationId) {
|
||||
throw new AuthorizationError("You are not authorized to access this survey");
|
||||
}
|
||||
|
||||
mappingsInput = await resolveFormbricksMappingsInput(
|
||||
formbricksMappings.surveyId,
|
||||
formbricksMappings.elementIds
|
||||
if (formbricksMappings?.length) {
|
||||
await Promise.all(
|
||||
formbricksMappings.map(async ({ surveyId }) => {
|
||||
const orgId = await getOrganizationIdFromSurveyId(surveyId);
|
||||
if (orgId !== organizationId) {
|
||||
throw new AuthorizationError("You are not authorized to access this survey");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
mappingsInput = await resolveFormbricksMappingsInput(formbricksMappings);
|
||||
} else if (fieldMappings?.length) {
|
||||
mappingsInput = { type: "field", mappings: fieldMappings };
|
||||
}
|
||||
@@ -170,12 +179,7 @@ const ZUpdateConnectorWithMappingsAction = z.object({
|
||||
connectorId: ZId,
|
||||
environmentId: ZId,
|
||||
connectorInput: ZConnectorUpdateInput,
|
||||
formbricksMappings: z
|
||||
.object({
|
||||
surveyId: ZId,
|
||||
elementIds: z.array(z.string()).min(1),
|
||||
})
|
||||
.optional(),
|
||||
formbricksMappings: z.array(ZFormbricksSurveyMapping).min(1).optional(),
|
||||
fieldMappings: z.array(ZConnectorFieldMappingCreateInput).optional(),
|
||||
});
|
||||
|
||||
@@ -208,18 +212,17 @@ export const updateConnectorWithMappingsAction = authenticatedActionClient
|
||||
|
||||
let mappingsInput: TMappingsInput | undefined;
|
||||
|
||||
if (parsedInput.formbricksMappings) {
|
||||
const organizationIdFromSurvey = await getOrganizationIdFromSurveyId(
|
||||
parsedInput.formbricksMappings.surveyId
|
||||
if (parsedInput.formbricksMappings?.length) {
|
||||
await Promise.all(
|
||||
parsedInput.formbricksMappings.map(async ({ surveyId }) => {
|
||||
const orgId = await getOrganizationIdFromSurveyId(surveyId);
|
||||
if (orgId !== organizationId) {
|
||||
throw new AuthorizationError("You are not authorized to access this survey");
|
||||
}
|
||||
})
|
||||
);
|
||||
if (organizationIdFromSurvey !== organizationId) {
|
||||
throw new AuthorizationError("You are not authorized to access this survey");
|
||||
}
|
||||
|
||||
mappingsInput = await resolveFormbricksMappingsInput(
|
||||
parsedInput.formbricksMappings.surveyId,
|
||||
parsedInput.formbricksMappings.elementIds
|
||||
);
|
||||
mappingsInput = await resolveFormbricksMappingsInput(parsedInput.formbricksMappings);
|
||||
} else if (parsedInput.fieldMappings && parsedInput.fieldMappings.length > 0) {
|
||||
mappingsInput = { type: "field", mappings: parsedInput.fieldMappings };
|
||||
}
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
import "server-only";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { HUB_API_KEY, HUB_API_URL } from "@/lib/constants";
|
||||
|
||||
// Hub field types (from OpenAPI spec)
|
||||
export type THubFieldType =
|
||||
| "text"
|
||||
| "categorical"
|
||||
| "nps"
|
||||
| "csat"
|
||||
| "ces"
|
||||
| "rating"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "date";
|
||||
|
||||
// Create FeedbackRecord input
|
||||
export interface TCreateFeedbackRecordInput {
|
||||
collected_at?: string;
|
||||
source_type: string;
|
||||
field_id: string;
|
||||
field_type: THubFieldType;
|
||||
field_label?: string;
|
||||
field_group_id?: string;
|
||||
field_group_label?: string;
|
||||
tenant_id?: string;
|
||||
source_id?: string;
|
||||
source_name?: string;
|
||||
value_text?: string;
|
||||
value_number?: number;
|
||||
value_boolean?: boolean;
|
||||
value_date?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
language?: string;
|
||||
user_identifier?: string;
|
||||
}
|
||||
|
||||
// FeedbackRecord data (response from Hub)
|
||||
export interface TFeedbackRecordData {
|
||||
id: string;
|
||||
collected_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
source_type: string;
|
||||
field_id: string;
|
||||
field_type: THubFieldType;
|
||||
field_label?: string;
|
||||
field_group_id?: string;
|
||||
field_group_label?: string;
|
||||
tenant_id?: string;
|
||||
source_id?: string;
|
||||
source_name?: string;
|
||||
value_text?: string;
|
||||
value_number?: number;
|
||||
value_boolean?: boolean;
|
||||
value_date?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
language?: string;
|
||||
user_identifier?: string;
|
||||
}
|
||||
|
||||
// List FeedbackRecords response
|
||||
export interface TListFeedbackRecordsResponse {
|
||||
data: TFeedbackRecordData[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
// Update FeedbackRecord input
|
||||
export interface TUpdateFeedbackRecordInput {
|
||||
value_text?: string;
|
||||
value_number?: number;
|
||||
value_boolean?: boolean;
|
||||
value_date?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
language?: string;
|
||||
user_identifier?: string;
|
||||
}
|
||||
|
||||
// List FeedbackRecords filters
|
||||
export interface TListFeedbackRecordsFilters {
|
||||
tenant_id?: string;
|
||||
source_type?: string;
|
||||
source_id?: string;
|
||||
field_id?: string;
|
||||
field_group_id?: string;
|
||||
field_type?: THubFieldType;
|
||||
user_identifier?: string;
|
||||
since?: string;
|
||||
until?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// Error response from Hub
|
||||
export interface THubErrorResponse {
|
||||
type?: string;
|
||||
title: string;
|
||||
status: number;
|
||||
detail: string;
|
||||
instance?: string;
|
||||
errors?: Array<{
|
||||
location?: string;
|
||||
message?: string;
|
||||
value?: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Hub API Error class
|
||||
export class HubApiError extends Error {
|
||||
status: number;
|
||||
detail: string;
|
||||
errors?: THubErrorResponse["errors"];
|
||||
|
||||
constructor(response: THubErrorResponse) {
|
||||
super(response.detail || response.title);
|
||||
this.name = "HubApiError";
|
||||
this.status = response.status;
|
||||
this.detail = response.detail;
|
||||
this.errors = response.errors;
|
||||
}
|
||||
}
|
||||
|
||||
// Make authenticated request to Hub API
|
||||
async function hubFetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<{ data: T | null; error: HubApiError | null }> {
|
||||
const url = `${HUB_API_URL}${path}`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
...(HUB_API_KEY && { Authorization: `Bearer ${HUB_API_KEY}` }),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle no content response (e.g., DELETE)
|
||||
if (response.status === 204) {
|
||||
return { data: null, error: null };
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to parse error response
|
||||
if (contentType?.includes("application/problem+json") || contentType?.includes("application/json")) {
|
||||
const errorBody = (await response.json()) as THubErrorResponse;
|
||||
return { data: null, error: new HubApiError(errorBody) };
|
||||
}
|
||||
|
||||
// Fallback for non-JSON errors
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: null,
|
||||
error: new HubApiError({
|
||||
title: "Error",
|
||||
status: response.status,
|
||||
detail: errorText || `HTTP ${response.status}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Parse successful response
|
||||
if (contentType?.includes("application/json")) {
|
||||
const data = (await response.json()) as T;
|
||||
return { data, error: null };
|
||||
}
|
||||
|
||||
return { data: null, error: null };
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ url, error: error instanceof Error ? error.message : "Unknown error" },
|
||||
"Hub API request failed"
|
||||
);
|
||||
return {
|
||||
data: null,
|
||||
error: new HubApiError({
|
||||
title: "Network Error",
|
||||
status: 0,
|
||||
detail: error instanceof Error ? error.message : "Failed to connect to Hub API",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new FeedbackRecord in the Hub
|
||||
*/
|
||||
export async function createFeedbackRecord(
|
||||
input: TCreateFeedbackRecordInput
|
||||
): Promise<{ data: TFeedbackRecordData | null; error: HubApiError | null }> {
|
||||
return hubFetch<TFeedbackRecordData>("/v1/feedback-records", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple FeedbackRecords in the Hub (batch)
|
||||
*/
|
||||
export async function createFeedbackRecordsBatch(
|
||||
inputs: TCreateFeedbackRecordInput[]
|
||||
): Promise<{ results: Array<{ data: TFeedbackRecordData | null; error: HubApiError | null }> }> {
|
||||
// Hub doesn't have a batch endpoint, so we'll do parallel requests
|
||||
// In production, you might want to implement rate limiting or chunking
|
||||
const results = await Promise.all(inputs.map((input) => createFeedbackRecord(input)));
|
||||
return { results };
|
||||
}
|
||||
|
||||
/**
|
||||
* List FeedbackRecords from the Hub with optional filters
|
||||
*/
|
||||
export async function listFeedbackRecords(
|
||||
filters: TListFeedbackRecordsFilters = {}
|
||||
): Promise<{ data: TListFeedbackRecordsResponse | null; error: HubApiError | null }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (filters.tenant_id) searchParams.set("tenant_id", filters.tenant_id);
|
||||
if (filters.source_type) searchParams.set("source_type", filters.source_type);
|
||||
if (filters.source_id) searchParams.set("source_id", filters.source_id);
|
||||
if (filters.field_id) searchParams.set("field_id", filters.field_id);
|
||||
if (filters.field_group_id) searchParams.set("field_group_id", filters.field_group_id);
|
||||
if (filters.field_type) searchParams.set("field_type", filters.field_type);
|
||||
if (filters.user_identifier) searchParams.set("user_identifier", filters.user_identifier);
|
||||
if (filters.since) searchParams.set("since", filters.since);
|
||||
if (filters.until) searchParams.set("until", filters.until);
|
||||
if (filters.limit !== undefined) searchParams.set("limit", String(filters.limit));
|
||||
if (filters.offset !== undefined) searchParams.set("offset", String(filters.offset));
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const path = queryString ? `/v1/feedback-records?${queryString}` : "/v1/feedback-records";
|
||||
|
||||
return hubFetch<TListFeedbackRecordsResponse>(path, { method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single FeedbackRecord from the Hub by ID
|
||||
*/
|
||||
export async function getFeedbackRecord(
|
||||
id: string
|
||||
): Promise<{ data: TFeedbackRecordData | null; error: HubApiError | null }> {
|
||||
return hubFetch<TFeedbackRecordData>(`/v1/feedback-records/${id}`, { method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a FeedbackRecord in the Hub
|
||||
*/
|
||||
export async function updateFeedbackRecord(
|
||||
id: string,
|
||||
input: TUpdateFeedbackRecordInput
|
||||
): Promise<{ data: TFeedbackRecordData | null; error: HubApiError | null }> {
|
||||
return hubFetch<TFeedbackRecordData>(`/v1/feedback-records/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a FeedbackRecord from the Hub
|
||||
*/
|
||||
export async function deleteFeedbackRecord(id: string): Promise<{ error: HubApiError | null }> {
|
||||
const result = await hubFetch<null>(`/v1/feedback-records/${id}`, { method: "DELETE" });
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete FeedbackRecords by user identifier (GDPR compliance)
|
||||
*/
|
||||
export async function bulkDeleteFeedbackRecordsByUser(
|
||||
userIdentifier: string,
|
||||
tenantId?: string
|
||||
): Promise<{ data: { deleted_count: number; message: string } | null; error: HubApiError | null }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("user_identifier", userIdentifier);
|
||||
if (tenantId) searchParams.set("tenant_id", tenantId);
|
||||
|
||||
return hubFetch<{ deleted_count: number; message: string }>(
|
||||
`/v1/feedback-records?${searchParams.toString()}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Hub API health
|
||||
*/
|
||||
export async function checkHubHealth(): Promise<{ healthy: boolean; error: HubApiError | null }> {
|
||||
try {
|
||||
const response = await fetch(`${HUB_API_URL}/health`);
|
||||
if (response.ok) {
|
||||
return { healthy: true, error: null };
|
||||
}
|
||||
return {
|
||||
healthy: false,
|
||||
error: new HubApiError({
|
||||
title: "Health Check Failed",
|
||||
status: response.status,
|
||||
detail: "Hub API health check failed",
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: new HubApiError({
|
||||
title: "Network Error",
|
||||
status: 0,
|
||||
detail: error instanceof Error ? error.message : "Failed to connect to Hub API",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ vi.mock("../response/service", () => ({
|
||||
getResponses: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./hub-client", () => ({
|
||||
vi.mock("@/modules/hub", () => ({
|
||||
createFeedbackRecordsBatch: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock("./transform", () => ({
|
||||
}));
|
||||
|
||||
const { getResponses } = vi.mocked(await import("../response/service"));
|
||||
const { createFeedbackRecordsBatch } = vi.mocked(await import("./hub-client"));
|
||||
const { createFeedbackRecordsBatch } = vi.mocked(await import("@/modules/hub"));
|
||||
const { transformResponseToFeedbackRecords } = vi.mocked(await import("./transform"));
|
||||
|
||||
const ENV_ID = "clxxxxxxxxxxxxxxxx001";
|
||||
|
||||
@@ -2,8 +2,8 @@ import "server-only";
|
||||
import { TConnectorFormbricksMapping, TConnectorWithMappings } from "@formbricks/types/connector";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { createFeedbackRecordsBatch } from "@/modules/hub";
|
||||
import { getResponses } from "../response/service";
|
||||
import { createFeedbackRecordsBatch } from "./hub-client";
|
||||
import { transformResponseToFeedbackRecords } from "./transform";
|
||||
|
||||
const IMPORT_BATCH_SIZE = 50;
|
||||
|
||||
225
apps/web/lib/connector/pipeline-handler.test.ts
Normal file
225
apps/web/lib/connector/pipeline-handler.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TConnectorWithMappings } from "@formbricks/types/connector";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
const mockCreateFeedbackRecordsBatch = vi.fn();
|
||||
|
||||
vi.mock("@/modules/hub", () => ({
|
||||
createFeedbackRecordsBatch: (...args: unknown[]) => mockCreateFeedbackRecordsBatch(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./service", () => ({
|
||||
getConnectorsBySurveyId: vi.fn(),
|
||||
updateConnector: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./transform", () => ({
|
||||
transformResponseToFeedbackRecords: vi.fn(),
|
||||
}));
|
||||
|
||||
const { getConnectorsBySurveyId, updateConnector } = await import("./service");
|
||||
const { transformResponseToFeedbackRecords } = await import("./transform");
|
||||
const { handleConnectorPipeline } = await import("./pipeline-handler");
|
||||
|
||||
const mockResponse = {
|
||||
id: "resp-1",
|
||||
createdAt: new Date("2026-02-24T10:00:00.000Z"),
|
||||
surveyId: "survey-1",
|
||||
data: { "el-1": "answer" },
|
||||
} as unknown as TResponse;
|
||||
|
||||
const mockSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
blocks: [{ id: "block-1", name: "Block", elements: [{ id: "el-1", headline: { default: "Question?" } }] }],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
function createConnector(
|
||||
overrides: Partial<Pick<TConnectorWithMappings, "id" | "formbricksMappings">> = {}
|
||||
): TConnectorWithMappings {
|
||||
return {
|
||||
id: "conn-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Connector",
|
||||
type: "formbricks",
|
||||
status: "active",
|
||||
environmentId: "env-1",
|
||||
lastSyncAt: null,
|
||||
errorMessage: null,
|
||||
formbricksMappings: [
|
||||
{
|
||||
id: "map-1",
|
||||
createdAt: new Date(),
|
||||
connectorId: "conn-1",
|
||||
environmentId: "env-1",
|
||||
surveyId: "survey-1",
|
||||
elementId: "el-1",
|
||||
hubFieldType: "rating",
|
||||
customFieldLabel: null,
|
||||
},
|
||||
],
|
||||
fieldMappings: [],
|
||||
...overrides,
|
||||
} as TConnectorWithMappings;
|
||||
}
|
||||
|
||||
const oneFeedbackRecord = [
|
||||
{
|
||||
field_id: "el-1",
|
||||
field_type: "rating" as const,
|
||||
source_type: "formbricks",
|
||||
source_id: "survey-1",
|
||||
source_name: "Test Survey",
|
||||
field_label: "Question?",
|
||||
value_number: 5,
|
||||
collected_at: "2026-02-24T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const noConfigError = {
|
||||
status: 0,
|
||||
message: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
detail: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
};
|
||||
|
||||
describe("handleConnectorPipeline", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("returns early when no connectors for survey", async () => {
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([]);
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(transformResponseToFeedbackRecords).not.toHaveBeenCalled();
|
||||
expect(mockCreateFeedbackRecordsBatch).not.toHaveBeenCalled();
|
||||
expect(updateConnector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("continues when transform returns no feedback records", async () => {
|
||||
const connector = createConnector();
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([connector]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockReturnValue([]);
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(transformResponseToFeedbackRecords).toHaveBeenCalledWith(
|
||||
mockResponse,
|
||||
mockSurvey,
|
||||
connector.formbricksMappings,
|
||||
"env-1"
|
||||
);
|
||||
expect(mockCreateFeedbackRecordsBatch).not.toHaveBeenCalled();
|
||||
expect(updateConnector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("updates connector to error when Hub returns no-config (HUB_API_KEY not set)", async () => {
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any);
|
||||
mockCreateFeedbackRecordsBatch.mockResolvedValue({
|
||||
results: oneFeedbackRecord.map(() => ({ data: null, error: noConfigError })),
|
||||
});
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(mockCreateFeedbackRecordsBatch).toHaveBeenCalledWith(oneFeedbackRecord);
|
||||
expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", {
|
||||
status: "error",
|
||||
errorMessage: expect.stringContaining("HUB_API_KEY"),
|
||||
});
|
||||
});
|
||||
|
||||
test("sends records to Hub and updates connector to active on full success", async () => {
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any);
|
||||
mockCreateFeedbackRecordsBatch.mockResolvedValue({
|
||||
results: [{ data: { id: "hub-1", ...oneFeedbackRecord[0] }, error: null }],
|
||||
});
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(mockCreateFeedbackRecordsBatch).toHaveBeenCalledWith(oneFeedbackRecord);
|
||||
expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", {
|
||||
status: "active",
|
||||
errorMessage: null,
|
||||
lastSyncAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
test("updates connector to error when all Hub creates fail", async () => {
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any);
|
||||
mockCreateFeedbackRecordsBatch.mockResolvedValue({
|
||||
results: [
|
||||
{ data: null, error: { status: 500, message: "Hub unavailable", detail: "Hub unavailable" } },
|
||||
],
|
||||
});
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", {
|
||||
status: "error",
|
||||
errorMessage: expect.stringContaining("Failed to send FeedbackRecords"),
|
||||
});
|
||||
});
|
||||
|
||||
test("updates connector to active with partial message when some creates fail", async () => {
|
||||
const twoRecords = [...oneFeedbackRecord, { ...oneFeedbackRecord[0], field_id: "el-2", value_number: 3 }];
|
||||
const baseMapping = {
|
||||
createdAt: new Date(),
|
||||
connectorId: "conn-1",
|
||||
environmentId: "env-1",
|
||||
surveyId: "survey-1",
|
||||
hubFieldType: "rating" as const,
|
||||
customFieldLabel: null as string | null,
|
||||
};
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([
|
||||
createConnector({
|
||||
formbricksMappings: [
|
||||
{ ...baseMapping, id: "m1", elementId: "el-1" },
|
||||
{ ...baseMapping, id: "m2", elementId: "el-2" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(twoRecords as any);
|
||||
mockCreateFeedbackRecordsBatch.mockResolvedValue({
|
||||
results: [
|
||||
{ data: { id: "hub-1" }, error: null },
|
||||
{ data: null, error: { status: 429, message: "Rate limited", detail: "Rate limited" } },
|
||||
],
|
||||
});
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", {
|
||||
status: "active",
|
||||
errorMessage: "Partial failure: 1/2 records sent",
|
||||
lastSyncAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
test("updates connector to error when transform throws", async () => {
|
||||
vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]);
|
||||
vi.mocked(transformResponseToFeedbackRecords).mockImplementation(() => {
|
||||
throw new Error("Transform failed");
|
||||
});
|
||||
|
||||
await handleConnectorPipeline(mockResponse, mockSurvey, "env-1");
|
||||
|
||||
expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", {
|
||||
status: "error",
|
||||
errorMessage: "Transform failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import "server-only";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { createFeedbackRecordsBatch } from "./hub-client";
|
||||
import { createFeedbackRecordsBatch } from "@/modules/hub";
|
||||
import { getConnectorsBySurveyId, updateConnector } from "./service";
|
||||
import { transformResponseToFeedbackRecords } from "./transform";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { TCreateFeedbackRecordInput } from "./hub-client";
|
||||
import type { FeedbackRecordCreateParams } from "@/modules/hub";
|
||||
|
||||
const getHeadlineFromElement = (element?: TSurveyElement): string => {
|
||||
if (!element?.headline) return "Untitled";
|
||||
@@ -23,7 +23,7 @@ const convertValueToHubFields = (
|
||||
value: TResponseDataValue,
|
||||
hubFieldType: THubFieldType
|
||||
): Partial<
|
||||
Pick<TCreateFeedbackRecordInput, "value_text" | "value_number" | "value_boolean" | "value_date">
|
||||
Pick<FeedbackRecordCreateParams, "value_text" | "value_number" | "value_boolean" | "value_date">
|
||||
> => {
|
||||
if (value === undefined || value === null) {
|
||||
return {};
|
||||
@@ -82,14 +82,14 @@ export function transformResponseToFeedbackRecords(
|
||||
survey: TSurvey,
|
||||
mappings: TConnectorFormbricksMapping[],
|
||||
tenantId?: string
|
||||
): TCreateFeedbackRecordInput[] {
|
||||
): FeedbackRecordCreateParams[] {
|
||||
const responseData = response.data;
|
||||
if (!responseData) return [];
|
||||
|
||||
const surveyMappings = mappings.filter((m) => m.surveyId === survey.id);
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const elementMap = new Map(elements.map((el) => [el.id, el]));
|
||||
const feedbackRecords: TCreateFeedbackRecordInput[] = [];
|
||||
const feedbackRecords: FeedbackRecordCreateParams[] = [];
|
||||
|
||||
for (const mapping of surveyMappings) {
|
||||
const value = extractResponseValue(responseData, mapping.elementId);
|
||||
@@ -98,7 +98,7 @@ export function transformResponseToFeedbackRecords(
|
||||
const fieldLabel = mapping.customFieldLabel || getHeadlineFromElement(elementMap.get(mapping.elementId));
|
||||
const valueFields = convertValueToHubFields(value, mapping.hubFieldType);
|
||||
|
||||
const feedbackRecord: TCreateFeedbackRecordInput = {
|
||||
const feedbackRecord: FeedbackRecordCreateParams = {
|
||||
collected_at:
|
||||
response.createdAt instanceof Date ? response.createdAt.toISOString() : String(response.createdAt),
|
||||
source_type: "formbricks",
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Feedback-Quelle hinzufügen",
|
||||
"add_source": "Quelle hinzufügen",
|
||||
"allowed_values": "Erlaubte Werte: {values}",
|
||||
"are_you_sure": "Bist Du sicher?",
|
||||
"change_file": "Datei ändern",
|
||||
"click_load_sample_csv": "Klicke auf 'Beispiel-CSV laden', um Spalten zu sehen",
|
||||
"click_to_upload": "Klicke zum Hochladen",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximal {max} Datensätze erlaubt.",
|
||||
"default_connector_name_csv": "CSV-Import",
|
||||
"default_connector_name_formbricks": "Formbricks Umfrage-Verbindung",
|
||||
"delete_source": "Quelle löschen",
|
||||
"deselect_all": "Alle abwählen",
|
||||
"drop_a_field_here": "Feld hier ablegen",
|
||||
"drop_field_or": "Feld ablegen oder",
|
||||
"edit_source_connection": "Quellverbindung bearbeiten",
|
||||
"element_selected": "<strong>{count}</strong> Element ausgewählt. Jede Antwort auf dieses Element erstellt einen FeedbackRecord im Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> Elemente ausgewählt. Jede Antwort auf diese Elemente erstellt einen FeedbackRecord im Hub.",
|
||||
"enter_name_for_source": "Gib einen Namen für diese Quelle ein",
|
||||
"enter_value": "Wert eingeben...",
|
||||
"enum": "Enum",
|
||||
"existing_responses_info": "{responseCount} vorhandene Antworten × {elementCount} Elemente = {total} Feedback-Datensätze",
|
||||
"feedback_date": "Feedback-Datum",
|
||||
"formbricks_surveys": "Formbricks Umfragen",
|
||||
"historical_import_complete": "Import abgeschlossen: {successes} erfolgreich, {failures} fehlgeschlagen, {skipped} übersprungen (keine Daten)",
|
||||
"hub_feedback_record_fields": "Hub Feedback-Record-Felder",
|
||||
"import_csv_data": "CSV-Daten importieren",
|
||||
"import_existing_responses": "Vorhandene Antworten importieren",
|
||||
"import_rows": "{count, plural, one {1 Zeile importieren} other {# Zeilen importieren}}",
|
||||
"importing_data": "Importiere Daten...",
|
||||
"importing_historical_data": "Importiere historische Daten...",
|
||||
"importing_data": "Daten werden importiert...",
|
||||
"invalid_enum_values": "Ungültige Werte in Spalte, die auf {field} gemappt ist",
|
||||
"invalid_values_found": "Gefunden: {values} (Zeilen: {rows}) {extra}",
|
||||
"load_sample_csv": "Beispiel-CSV laden",
|
||||
"n_supported_elements": "{count} unterstützte Elemente",
|
||||
"n_supported_questions": "{count} unterstützte Fragen",
|
||||
"no_source_fields_loaded": "Noch keine Quellfelder geladen",
|
||||
"no_sources_connected": "Noch keine Quellen verbunden. Füge eine Quelle hinzu, um loszulegen.",
|
||||
"no_surveys_found": "Keine Umfragen in dieser Umgebung gefunden",
|
||||
"optional": "Optional",
|
||||
"or_drag_and_drop": "oder per Drag & Drop",
|
||||
"question_selected": "<strong>{count}</strong> Frage ausgewählt. Jede Antwort auf diese Frage erstellt einen neuen Feedback-Datensatz.",
|
||||
"question_type_not_supported": "Dieser Fragetyp wird nicht unterstützt",
|
||||
"questions_selected": "<strong>{count}</strong> Fragen ausgewählt. Jede Antwort auf diese Fragen erstellt einen neuen Feedback-Datensatz.",
|
||||
"required": "Erforderlich",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"select_a_survey_to_see_elements": "Wähle eine Umfrage aus, um ihre Elemente zu sehen",
|
||||
"select_a_survey_to_see_questions": "Wähle eine Umfrage aus, um ihre Fragen zu sehen",
|
||||
"select_a_value": "Wähle einen Wert aus...",
|
||||
"select_all": "Alles auswählen",
|
||||
"select_elements": "Elemente auswählen",
|
||||
"select_questions": "Fragen auswählen",
|
||||
"select_source_type_description": "Wähle den Typ der Feedbackquelle aus, die du verbinden möchtest.",
|
||||
"select_source_type_prompt": "Wähle den Typ der Feedbackquelle, die du verbinden möchtest:",
|
||||
"select_survey": "Umfrage auswählen",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Quellenname",
|
||||
"source_type_cannot_be_changed": "Quellentyp kann nicht geändert werden",
|
||||
"sources": "Quellen",
|
||||
"status_active": "Aktiv",
|
||||
"status_active": "Im Gange",
|
||||
"status_completed": "Abgeschlossen",
|
||||
"status_draft": "Entwurf",
|
||||
"status_error": "Fehler",
|
||||
"status_paused": "Pausiert",
|
||||
"survey_has_no_elements": "Diese Umfrage hat keine Fragen",
|
||||
"survey_has_no_questions": "Diese Umfrage hat keine Fragen",
|
||||
"survey_import_line": "{surveyName}: {responseCount} Antworten × {questionCount} Fragen = {total} Feedback-Datensätze",
|
||||
"total_feedback_records": "Gesamt: {checked} von {total} Feedback-Datensätzen ausgewählt über {surveyCount} Umfragen",
|
||||
"unify_feedback": "Feedback vereinheitlichen",
|
||||
"update_mapping_description": "Aktualisiere die Mapping-Konfiguration für diese Quelle.",
|
||||
"updated_at": "Aktualisiert am",
|
||||
"upload_csv_data_description": "Lade eine CSV-Datei hoch, um Feedback-Daten in den Hub zu importieren.",
|
||||
"upload_csv_file": "CSV-Datei hochladen",
|
||||
"yes_delete": "Ja, löschen"
|
||||
"upload_csv_file": "CSV-Datei hochladen"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Add Feedback Source",
|
||||
"add_source": "Add source",
|
||||
"allowed_values": "Allowed values: {values}",
|
||||
"are_you_sure": "Are you sure?",
|
||||
"change_file": "Change file",
|
||||
"click_load_sample_csv": "Click 'Load sample CSV' to see columns",
|
||||
"click_to_upload": "Click to upload",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximum {max} records allowed.",
|
||||
"default_connector_name_csv": "CSV Import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey Connection",
|
||||
"delete_source": "Delete source",
|
||||
"deselect_all": "Deselect all",
|
||||
"drop_a_field_here": "Drop a field here",
|
||||
"drop_field_or": "Drop field or",
|
||||
"edit_source_connection": "Edit Source Connection",
|
||||
"element_selected": "<strong>{count}</strong> element selected. Each response to these elements will create a FeedbackRecord in the Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elements selected. Each response to these elements will create a FeedbackRecord in the Hub.",
|
||||
"enter_name_for_source": "Enter a name for this source",
|
||||
"enter_value": "Enter value...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} existing responses \u00d7 {elementCount} elements = {total} Feedback Records",
|
||||
"feedback_date": "Feedback date",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
|
||||
"hub_feedback_record_fields": "Hub Feedback Record Fields",
|
||||
"import_csv_data": "Import CSV Data",
|
||||
"import_existing_responses": "Import existing responses",
|
||||
"import_rows": "Import {count} rows",
|
||||
"importing_data": "Importing data...",
|
||||
"importing_historical_data": "Importing historical data...",
|
||||
"invalid_enum_values": "Invalid values in column mapped to {field}",
|
||||
"invalid_values_found": "Found: {values} (rows: {rows}) {extra}",
|
||||
"load_sample_csv": "Load sample CSV",
|
||||
"n_supported_elements": "{count} supported elements",
|
||||
"n_supported_questions": "{count} supported questions",
|
||||
"no_source_fields_loaded": "No source fields loaded yet",
|
||||
"no_sources_connected": "No sources connected yet. Add a source to get started.",
|
||||
"no_surveys_found": "No surveys found in this environment",
|
||||
"optional": "Optional",
|
||||
"or_drag_and_drop": "or drag and drop",
|
||||
"question_selected": "<strong>{count}</strong> question selected. Each response to these questions will create a new Feedback Record.",
|
||||
"question_type_not_supported": "This question type is not supported",
|
||||
"questions_selected": "<strong>{count}</strong> questions selected. Each response to these questions will create a new Feedback Record.",
|
||||
"required": "Required",
|
||||
"save_changes": "Save changes",
|
||||
"select_a_survey_to_see_elements": "Select a survey to see its elements",
|
||||
"select_a_survey_to_see_questions": "Select a survey to see its questions",
|
||||
"select_a_value": "Select a value...",
|
||||
"select_all": "Select all",
|
||||
"select_elements": "Select Elements",
|
||||
"select_questions": "Select questions",
|
||||
"select_source_type_description": "Select the type of feedback source you want to connect.",
|
||||
"select_source_type_prompt": "Select the type of feedback source you want to connect:",
|
||||
"select_survey": "Select Survey",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Source Name",
|
||||
"source_type_cannot_be_changed": "Source type cannot be changed",
|
||||
"sources": "Sources",
|
||||
"status_active": "Active",
|
||||
"status_active": "In Progress",
|
||||
"status_completed": "Completed",
|
||||
"status_draft": "Draft",
|
||||
"status_error": "Error",
|
||||
"status_paused": "Paused",
|
||||
"survey_has_no_elements": "This survey has no question elements",
|
||||
"survey_has_no_questions": "This survey has no questions",
|
||||
"survey_import_line": "{surveyName}: {responseCount} responses × {questionCount} questions = {total} Feedback Records",
|
||||
"total_feedback_records": "Total: {checked} of {total} Feedback Records selected across {surveyCount} surveys",
|
||||
"unify_feedback": "Unify Feedback",
|
||||
"update_mapping_description": "Update the mapping configuration for this source.",
|
||||
"updated_at": "Updated at",
|
||||
"upload_csv_data_description": "Upload a CSV file to import feedback data into the Hub.",
|
||||
"upload_csv_file": "Upload CSV File",
|
||||
"yes_delete": "Yes, delete"
|
||||
"upload_csv_file": "Upload CSV File"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Añadir fuente de feedback",
|
||||
"add_source": "Añadir fuente",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"are_you_sure": "¿Estás seguro?",
|
||||
"change_file": "Cambiar archivo",
|
||||
"click_load_sample_csv": "Haz clic en 'Cargar CSV de muestra' para ver las columnas",
|
||||
"click_to_upload": "Haz clic para subir",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Máximo de {max} registros permitidos.",
|
||||
"default_connector_name_csv": "Importación CSV",
|
||||
"default_connector_name_formbricks": "Conexión de encuesta de Formbricks",
|
||||
"delete_source": "Eliminar fuente",
|
||||
"deselect_all": "Deseleccionar todo",
|
||||
"drop_a_field_here": "Suelta un campo aquí",
|
||||
"drop_field_or": "Suelta el campo o",
|
||||
"edit_source_connection": "Editar conexión de origen",
|
||||
"element_selected": "<strong>{count}</strong> elemento seleccionado. Cada respuesta a este elemento creará un FeedbackRecord en el Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elementos seleccionados. Cada respuesta a estos elementos creará un FeedbackRecord en el Hub.",
|
||||
"enter_name_for_source": "Introduce un nombre para este origen",
|
||||
"enter_value": "Introduce un valor...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} respuestas existentes × {elementCount} elementos = {total} registros de feedback",
|
||||
"feedback_date": "Fecha del feedback",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "Importación completada: {successes} correctas, {failures} fallidas, {skipped} omitidas (sin datos)",
|
||||
"hub_feedback_record_fields": "Campos de FeedbackRecord del Hub",
|
||||
"import_csv_data": "Importar datos CSV",
|
||||
"import_existing_responses": "Importar respuestas existentes",
|
||||
"import_rows": "Importar {count} filas",
|
||||
"importing_data": "Importando datos...",
|
||||
"importing_historical_data": "Importando datos históricos...",
|
||||
"invalid_enum_values": "Valores no válidos en la columna asignada a {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (filas: {rows}) {extra}",
|
||||
"load_sample_csv": "Cargar CSV de muestra",
|
||||
"n_supported_elements": "{count} elementos compatibles",
|
||||
"n_supported_questions": "{count} preguntas compatibles",
|
||||
"no_source_fields_loaded": "Aún no se han cargado campos de origen",
|
||||
"no_sources_connected": "Aún no hay fuentes conectadas. Añade una fuente para empezar.",
|
||||
"no_surveys_found": "No se encontraron encuestas en este entorno",
|
||||
"optional": "Opcional",
|
||||
"or_drag_and_drop": "o arrastra y suelta",
|
||||
"question_selected": "<strong>{count}</strong> pregunta seleccionada. Cada respuesta a esta pregunta creará un registro de feedback nuevo.",
|
||||
"question_type_not_supported": "Este tipo de pregunta no es compatible",
|
||||
"questions_selected": "<strong>{count}</strong> preguntas seleccionadas. Cada respuesta a estas preguntas creará un registro de feedback nuevo.",
|
||||
"required": "Obligatorio",
|
||||
"save_changes": "Guardar cambios",
|
||||
"select_a_survey_to_see_elements": "Selecciona una encuesta para ver sus elementos",
|
||||
"select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas",
|
||||
"select_a_value": "Selecciona un valor...",
|
||||
"select_all": "Seleccionar todo",
|
||||
"select_elements": "Seleccionar elementos",
|
||||
"select_questions": "Seleccionar preguntas",
|
||||
"select_source_type_description": "Selecciona el tipo de fuente de feedback que quieres conectar.",
|
||||
"select_source_type_prompt": "Selecciona el tipo de fuente de feedback que quieres conectar:",
|
||||
"select_survey": "Seleccionar encuesta",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Nombre de origen",
|
||||
"source_type_cannot_be_changed": "El tipo de origen no se puede cambiar",
|
||||
"sources": "Orígenes",
|
||||
"status_active": "Activo",
|
||||
"status_active": "En progreso",
|
||||
"status_completed": "Completado",
|
||||
"status_draft": "Borrador",
|
||||
"status_error": "Error",
|
||||
"status_paused": "Pausado",
|
||||
"survey_has_no_elements": "Esta encuesta no tiene elementos de pregunta",
|
||||
"survey_has_no_questions": "Esta encuesta no tiene preguntas",
|
||||
"survey_import_line": "{surveyName}: {responseCount} respuestas × {questionCount} preguntas = {total} registros de feedback",
|
||||
"total_feedback_records": "Total: {checked} de {total} registros de feedback seleccionados en {surveyCount} encuestas",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
"update_mapping_description": "Actualiza la configuración de mapeo para esta fuente.",
|
||||
"updated_at": "Actualizado el",
|
||||
"upload_csv_data_description": "Sube un archivo CSV para importar datos de feedback en el Hub.",
|
||||
"upload_csv_file": "Subir archivo CSV",
|
||||
"yes_delete": "Sí, eliminar"
|
||||
"upload_csv_file": "Subir archivo CSV"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Ajouter une source de feedback",
|
||||
"add_source": "Ajouter une source",
|
||||
"allowed_values": "Valeurs autorisées : {values}",
|
||||
"are_you_sure": "Es-tu sûr ?",
|
||||
"change_file": "Changer de fichier",
|
||||
"click_load_sample_csv": "Clique sur « Charger un exemple CSV » pour voir les colonnes",
|
||||
"click_to_upload": "Clique pour charger",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximum {max} enregistrements autorisés.",
|
||||
"default_connector_name_csv": "Importation CSV",
|
||||
"default_connector_name_formbricks": "Connexion de sondage Formbricks",
|
||||
"delete_source": "Supprimer la source",
|
||||
"deselect_all": "Tout désélectionner",
|
||||
"drop_a_field_here": "Déposez un champ ici",
|
||||
"drop_field_or": "Déposez un champ ou",
|
||||
"edit_source_connection": "Modifier la connexion source",
|
||||
"element_selected": "<strong>{count}</strong> élément sélectionné. Chaque réponse à cet élément créera un FeedbackRecord dans le Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> éléments sélectionnés. Chaque réponse à ces éléments créera un FeedbackRecord dans le Hub.",
|
||||
"enter_name_for_source": "Entrez un nom pour cette source",
|
||||
"enter_value": "Saisir une valeur...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} réponses existantes × {elementCount} éléments = {total} enregistrements de feedback",
|
||||
"feedback_date": "Date du feedback",
|
||||
"formbricks_surveys": "Sondages Formbricks",
|
||||
"historical_import_complete": "Importation terminée : {successes} réussies, {failures} échouées, {skipped} ignorées (aucune donnée)",
|
||||
"hub_feedback_record_fields": "Champs d'enregistrement de feedback du Hub",
|
||||
"import_csv_data": "Importer des données CSV",
|
||||
"import_existing_responses": "Importer les réponses existantes",
|
||||
"import_rows": "Importer {count} lignes",
|
||||
"importing_data": "Importation des données...",
|
||||
"importing_historical_data": "Importation des données historiques...",
|
||||
"invalid_enum_values": "Valeurs non valides dans la colonne mappée à {field}",
|
||||
"invalid_values_found": "Trouvées : {values} (lignes : {rows}) {extra}",
|
||||
"load_sample_csv": "Charger un exemple de CSV",
|
||||
"n_supported_elements": "{count} éléments pris en charge",
|
||||
"n_supported_questions": "{count} questions prises en charge",
|
||||
"no_source_fields_loaded": "Aucun champ source chargé pour le moment",
|
||||
"no_sources_connected": "Aucune source connectée pour le moment. Ajoutez une source pour commencer.",
|
||||
"no_surveys_found": "Aucune enquête trouvée dans cet environnement",
|
||||
"optional": "Facultatif",
|
||||
"or_drag_and_drop": "ou glisser-déposer",
|
||||
"question_selected": "<strong>{count}</strong> question sélectionnée. Chaque réponse à cette question créera un nouvel enregistrement de feedback.",
|
||||
"question_type_not_supported": "Ce type de question n'est pas pris en charge",
|
||||
"questions_selected": "<strong>{count}</strong> questions sélectionnées. Chaque réponse à ces questions créera un nouvel enregistrement de feedback.",
|
||||
"required": "Requis",
|
||||
"save_changes": "Enregistrer les modifications",
|
||||
"select_a_survey_to_see_elements": "Sélectionnez une enquête pour voir ses éléments",
|
||||
"select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions",
|
||||
"select_a_value": "Sélectionnez une valeur...",
|
||||
"select_all": "Sélectionner tout",
|
||||
"select_elements": "Sélectionner les éléments",
|
||||
"select_questions": "Sélectionner les questions",
|
||||
"select_source_type_description": "Sélectionnez le type de source de feedback que vous souhaitez connecter.",
|
||||
"select_source_type_prompt": "Sélectionnez le type de source de feedback que vous souhaitez connecter :",
|
||||
"select_survey": "Sélectionner l'enquête",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Nom de la source",
|
||||
"source_type_cannot_be_changed": "Le type de source ne peut pas être modifié",
|
||||
"sources": "Sources",
|
||||
"status_active": "Active",
|
||||
"status_active": "En cours",
|
||||
"status_completed": "Terminé",
|
||||
"status_draft": "Brouillon",
|
||||
"status_error": "Erreur",
|
||||
"status_paused": "En pause",
|
||||
"survey_has_no_elements": "Cette enquête n'a aucun élément de question",
|
||||
"survey_has_no_questions": "Ce sondage n'a pas de questions",
|
||||
"survey_import_line": "{surveyName} : {responseCount} réponses × {questionCount} questions = {total} enregistrements de feedback",
|
||||
"total_feedback_records": "Total : {checked} sur {total} enregistrements de feedback sélectionnés parmi {surveyCount} sondages",
|
||||
"unify_feedback": "Unifier les retours",
|
||||
"update_mapping_description": "Mettre à jour la configuration de mappage pour cette source.",
|
||||
"updated_at": "Mis à jour à",
|
||||
"upload_csv_data_description": "Téléchargez un fichier CSV pour importer des données de feedback dans le Hub.",
|
||||
"upload_csv_file": "Télécharger un fichier CSV",
|
||||
"yes_delete": "Oui, supprimer"
|
||||
"upload_csv_file": "Télécharger un fichier CSV"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Visszajelzési forrás hozzáadása",
|
||||
"add_source": "Forrás hozzáadása",
|
||||
"allowed_values": "Engedélyezett értékek: {values}",
|
||||
"are_you_sure": "Biztos benne?",
|
||||
"change_file": "Fájl módosítása",
|
||||
"click_load_sample_csv": "Kattintson a 'Minta CSV betöltése' gombra az oszlopok megtekintéséhez",
|
||||
"click_to_upload": "Kattintson a feltöltéshez",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximum {max} rekord engedélyezett.",
|
||||
"default_connector_name_csv": "CSV importálás",
|
||||
"default_connector_name_formbricks": "Formbricks kérdőív kapcsolat",
|
||||
"delete_source": "Forrás törlése",
|
||||
"deselect_all": "Összes kijelölés törlése",
|
||||
"drop_a_field_here": "Húzz ide egy mezőt",
|
||||
"drop_field_or": "Húzz ide egy mezőt vagy",
|
||||
"edit_source_connection": "Forráskapcsolat szerkesztése",
|
||||
"element_selected": "<strong>{count}</strong> elem kiválasztva. Az ezekre az elemekre adott minden válasz létrehoz egy FeedbackRecord-ot a központban.",
|
||||
"elements_selected": "<strong>{count}</strong> elem kiválasztva. Az ezekre az elemekre adott minden válasz létrehoz egy FeedbackRecord-ot a központban.",
|
||||
"enter_name_for_source": "Adj nevet ennek a forrásnak",
|
||||
"enter_value": "Érték megadása...",
|
||||
"enum": "felsorolás",
|
||||
"existing_responses_info": "{responseCount} meglévő válasz × {elementCount} elem = {total} visszajelzési rekord",
|
||||
"feedback_date": "Visszajelzés dátuma",
|
||||
"formbricks_surveys": "Formbricks kérdőívek",
|
||||
"historical_import_complete": "Importálás befejezve: {successes} sikeres, {failures} sikertelen, {skipped} kihagyva (nincs adat)",
|
||||
"hub_feedback_record_fields": "Központi visszajelzési rekord mezők",
|
||||
"import_csv_data": "CSV adatok importálása",
|
||||
"import_existing_responses": "Meglévő válaszok importálása",
|
||||
"import_rows": "{count} sor importálása",
|
||||
"importing_data": "Adatok importálása...",
|
||||
"importing_historical_data": "Történeti adatok importálása...",
|
||||
"invalid_enum_values": "Érvénytelen értékek a(z) {field} mezőhöz rendelt oszlopban",
|
||||
"invalid_values_found": "Talált értékek: {values} (sorok: {rows}) {extra}",
|
||||
"load_sample_csv": "Minta CSV betöltése",
|
||||
"n_supported_elements": "{count} támogatott elem",
|
||||
"n_supported_questions": "{count} támogatott kérdés",
|
||||
"no_source_fields_loaded": "Még nincsenek forrás mezők betöltve",
|
||||
"no_sources_connected": "Még nincsenek források csatlakoztatva. Adj hozzá egy forrást a kezdéshez.",
|
||||
"no_surveys_found": "Nem találhatók kérdőívek ebben a környezetben",
|
||||
"optional": "Elhagyható",
|
||||
"or_drag_and_drop": "vagy húzd ide",
|
||||
"question_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
|
||||
"question_type_not_supported": "Ez a kérdéstípus nem támogatott",
|
||||
"questions_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
|
||||
"required": "Kötelező",
|
||||
"save_changes": "Változtatások mentése",
|
||||
"select_a_survey_to_see_elements": "Válassz egy kérdőívet az elemek megtekintéséhez",
|
||||
"select_a_survey_to_see_questions": "Válassz egy kérdőívet a kérdések megtekintéséhez",
|
||||
"select_a_value": "Válassz egy értéket...",
|
||||
"select_all": "Összes kiválasztása",
|
||||
"select_elements": "Elemek kiválasztása",
|
||||
"select_questions": "Kérdések kiválasztása",
|
||||
"select_source_type_description": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát.",
|
||||
"select_source_type_prompt": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát:",
|
||||
"select_survey": "Kérdőív kiválasztása",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Forrásnév",
|
||||
"source_type_cannot_be_changed": "A forrástípus nem módosítható",
|
||||
"sources": "Források",
|
||||
"status_active": "Aktív",
|
||||
"status_active": "Folyamatban",
|
||||
"status_completed": "Befejezve",
|
||||
"status_draft": "Piszkozat",
|
||||
"status_error": "Hiba",
|
||||
"status_paused": "Szüneteltetve",
|
||||
"survey_has_no_elements": "Ez a kérdőív nem tartalmaz kérdéselemeket",
|
||||
"survey_has_no_questions": "Ez a felmérés nem tartalmaz kérdéseket",
|
||||
"survey_import_line": "{surveyName}: {responseCount} válasz × {questionCount} kérdés = {total} visszajelzési rekord",
|
||||
"total_feedback_records": "Összesen: {checked} / {total} visszajelzési rekord kiválasztva {surveyCount} felmérésből",
|
||||
"unify_feedback": "Visszajelzések egyesítése",
|
||||
"update_mapping_description": "Frissítse a leképezési konfigurációt ehhez a forráshoz.",
|
||||
"updated_at": "Frissítve",
|
||||
"upload_csv_data_description": "Tölts fel egy CSV fájlt, hogy visszajelzési adatokat importálj a központba.",
|
||||
"upload_csv_file": "CSV fájl feltöltése",
|
||||
"yes_delete": "Igen, törlés"
|
||||
"upload_csv_file": "CSV fájl feltöltése"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "フィードバックソースを追加",
|
||||
"add_source": "ソースを追加",
|
||||
"allowed_values": "許可される値: {values}",
|
||||
"are_you_sure": "よろしいですか?",
|
||||
"change_file": "ファイルを変更",
|
||||
"click_load_sample_csv": "「サンプルCSVを読み込む」をクリックして列を表示",
|
||||
"click_to_upload": "クリックしてアップロード",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "最大 {max} 件のレコードまで許可されています。",
|
||||
"default_connector_name_csv": "CSVインポート",
|
||||
"default_connector_name_formbricks": "Formbricks フォーム接続",
|
||||
"delete_source": "ソースを削除",
|
||||
"deselect_all": "すべて選択解除",
|
||||
"drop_a_field_here": "ここにフィールドをドロップ",
|
||||
"drop_field_or": "フィールドをドロップまたは",
|
||||
"edit_source_connection": "ソース接続を編集",
|
||||
"element_selected": "<strong>{count}</strong>個の要素が選択されています。これらの要素への各回答は、ハブにフィードバックレコードを作成します。",
|
||||
"elements_selected": "<strong>{count}</strong>個の要素が選択されています。これらの要素への各回答は、ハブにフィードバックレコードを作成します。",
|
||||
"enter_name_for_source": "このソースの名前を入力",
|
||||
"enter_value": "値を入力...",
|
||||
"enum": "列挙型",
|
||||
"existing_responses_info": "{responseCount} 件の既存の回答 × {elementCount} 個の要素 = {total} 件のフィードバックレコード",
|
||||
"feedback_date": "フィードバック日時",
|
||||
"formbricks_surveys": "Formbricks フォーム",
|
||||
"historical_import_complete": "インポート完了: {successes}件成功、{failures}件失敗、{skipped}件スキップ(データなし)",
|
||||
"hub_feedback_record_fields": "ハブフィードバックレコードフィールド",
|
||||
"import_csv_data": "CSVデータをインポート",
|
||||
"import_existing_responses": "既存の回答をインポート",
|
||||
"import_rows": "{count}行をインポート",
|
||||
"importing_data": "データをインポート中...",
|
||||
"importing_historical_data": "過去のデータをインポート中...",
|
||||
"invalid_enum_values": "{field}にマッピングされた列に無効な値があります",
|
||||
"invalid_values_found": "検出された値: {values}(行: {rows}){extra}",
|
||||
"load_sample_csv": "サンプルCSVを読み込む",
|
||||
"n_supported_elements": "{count} 個のサポートされている要素",
|
||||
"n_supported_questions": "{count} 件のサポートされている質問",
|
||||
"no_source_fields_loaded": "ソースフィールドがまだ読み込まれていません",
|
||||
"no_sources_connected": "ソースがまだ接続されていません。開始するにはソースを追加してください。",
|
||||
"no_surveys_found": "この環境にフォームが見つかりません",
|
||||
"optional": "任意",
|
||||
"or_drag_and_drop": "またはドラッグ&ドロップ",
|
||||
"question_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
|
||||
"question_type_not_supported": "この質問タイプはサポートされていません",
|
||||
"questions_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
|
||||
"required": "必須",
|
||||
"save_changes": "変更を保存",
|
||||
"select_a_survey_to_see_elements": "フォームを選択して要素を表示",
|
||||
"select_a_survey_to_see_questions": "フォームを選択して質問を表示",
|
||||
"select_a_value": "値を選択...",
|
||||
"select_all": "すべて選択",
|
||||
"select_elements": "要素を選択",
|
||||
"select_questions": "質問を選択",
|
||||
"select_source_type_description": "接続するフィードバックソースの種類を選択してください。",
|
||||
"select_source_type_prompt": "接続するフィードバックソースの種類を選択してください:",
|
||||
"select_survey": "フォームを選択",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "ソース名",
|
||||
"source_type_cannot_be_changed": "ソースタイプは変更できません",
|
||||
"sources": "ソース",
|
||||
"status_active": "有効",
|
||||
"status_active": "進行中",
|
||||
"status_completed": "完了",
|
||||
"status_draft": "下書き",
|
||||
"status_error": "エラー",
|
||||
"status_paused": "一時停止",
|
||||
"survey_has_no_elements": "このフォームには質問要素がありません",
|
||||
"survey_has_no_questions": "このアンケートには質問がありません",
|
||||
"survey_import_line": "{surveyName}: {responseCount}件の回答 × {questionCount}件の質問 = {total}件のフィードバックレコード",
|
||||
"total_feedback_records": "合計: {surveyCount}件のアンケート全体で{total}件中{checked}件のフィードバックレコードが選択されています",
|
||||
"unify_feedback": "フィードバックを統合",
|
||||
"update_mapping_description": "このソースのマッピング設定を更新します。",
|
||||
"updated_at": "更新日時",
|
||||
"upload_csv_data_description": "CSVファイルをアップロードして、フィードバックデータをハブにインポートします。",
|
||||
"upload_csv_file": "CSVファイルをアップロード",
|
||||
"yes_delete": "はい、削除します"
|
||||
"upload_csv_file": "CSVファイルをアップロード"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Feedbackbron toevoegen",
|
||||
"add_source": "Bron toevoegen",
|
||||
"allowed_values": "Toegestane waarden: {values}",
|
||||
"are_you_sure": "Weet je het zeker?",
|
||||
"change_file": "Bestand wijzigen",
|
||||
"click_load_sample_csv": "Klik op 'Voorbeeld CSV laden' om kolommen te zien",
|
||||
"click_to_upload": "Klik om te uploaden",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximaal {max} records toegestaan.",
|
||||
"default_connector_name_csv": "CSV import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey verbinding",
|
||||
"delete_source": "Bron verwijderen",
|
||||
"deselect_all": "Alles deselecteren",
|
||||
"drop_a_field_here": "Zet hier een veld neer",
|
||||
"drop_field_or": "Zet veld neer of",
|
||||
"edit_source_connection": "Bronverbinding bewerken",
|
||||
"element_selected": "<strong>{count}</strong> element geselecteerd. Elke reactie op dit element zal een FeedbackRecord aanmaken in de Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elementen geselecteerd. Elke reactie op deze elementen zal een FeedbackRecord aanmaken in de Hub.",
|
||||
"enter_name_for_source": "Voer een naam in voor deze bron",
|
||||
"enter_value": "Voer waarde in...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} bestaande antwoorden × {elementCount} elementen = {total} feedbackrecords",
|
||||
"feedback_date": "Feedbackdatum",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "Import voltooid: {successes} geslaagd, {failures} mislukt, {skipped} overgeslagen (geen data)",
|
||||
"hub_feedback_record_fields": "Hub feedbackrecordvelden",
|
||||
"import_csv_data": "CSV-gegevens importeren",
|
||||
"import_existing_responses": "Bestaande antwoorden importeren",
|
||||
"import_rows": "Importeer {count} rijen",
|
||||
"import_rows": "{count, plural, one {Importeer 1 rij} other {Importeer # rijen}}",
|
||||
"importing_data": "Gegevens importeren...",
|
||||
"importing_historical_data": "Historische gegevens importeren...",
|
||||
"invalid_enum_values": "Ongeldige waarden in kolom gekoppeld aan {field}",
|
||||
"invalid_values_found": "Gevonden: {values} (rijen: {rows}) {extra}",
|
||||
"load_sample_csv": "Voorbeeld-CSV laden",
|
||||
"n_supported_elements": "{count} ondersteunde elementen",
|
||||
"n_supported_questions": "{count} ondersteunde vragen",
|
||||
"no_source_fields_loaded": "Nog geen bronvelden geladen",
|
||||
"no_sources_connected": "Nog geen bronnen verbonden. Voeg een bron toe om te beginnen.",
|
||||
"no_surveys_found": "Geen enquêtes gevonden in deze omgeving",
|
||||
"optional": "Optioneel",
|
||||
"or_drag_and_drop": "of sleep en zet neer",
|
||||
"question_selected": "<strong>{count}</strong> vraag geselecteerd. Elk antwoord op deze vraag zal een nieuw feedbackrecord aanmaken.",
|
||||
"question_type_not_supported": "Dit vraagtype wordt niet ondersteund",
|
||||
"questions_selected": "<strong>{count}</strong> vragen geselecteerd. Elk antwoord op deze vragen zal een nieuw feedbackrecord aanmaken.",
|
||||
"required": "Vereist",
|
||||
"save_changes": "Wijzigingen opslaan",
|
||||
"select_a_survey_to_see_elements": "Selecteer een enquête om de elementen te zien",
|
||||
"select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien",
|
||||
"select_a_value": "Selecteer een waarde...",
|
||||
"select_all": "Selecteer alles",
|
||||
"select_elements": "Selecteer elementen",
|
||||
"select_questions": "Selecteer vragen",
|
||||
"select_source_type_description": "Selecteer het type feedbackbron dat je wilt verbinden.",
|
||||
"select_source_type_prompt": "Selecteer het type feedbackbron dat je wilt verbinden:",
|
||||
"select_survey": "Selecteer enquête",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Bronnaam",
|
||||
"source_type_cannot_be_changed": "Brontype kan niet worden gewijzigd",
|
||||
"sources": "Bronnen",
|
||||
"status_active": "Actief",
|
||||
"status_active": "In uitvoering",
|
||||
"status_completed": "Voltooid",
|
||||
"status_draft": "Voorlopige versie",
|
||||
"status_error": "Fout",
|
||||
"status_paused": "Gepauzeerd",
|
||||
"survey_has_no_elements": "Deze enquête heeft geen vraagelementen",
|
||||
"survey_has_no_questions": "Deze enquête heeft geen vragen",
|
||||
"survey_import_line": "{surveyName}: {responseCount} antwoorden × {questionCount} vragen = {total} feedbackrecords",
|
||||
"total_feedback_records": "Totaal: {checked} van {total} feedbackrecords geselecteerd over {surveyCount} enquêtes",
|
||||
"unify_feedback": "Feedback verenigen",
|
||||
"update_mapping_description": "Werk de mappingconfiguratie voor deze bron bij.",
|
||||
"updated_at": "Bijgewerkt op",
|
||||
"upload_csv_data_description": "Upload een CSV-bestand om feedbackgegevens te importeren in de Hub.",
|
||||
"upload_csv_file": "CSV-bestand uploaden",
|
||||
"yes_delete": "Ja, verwijderen"
|
||||
"upload_csv_file": "CSV-bestand uploaden"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Adicionar fonte de feedback",
|
||||
"add_source": "Adicionar fonte",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"are_you_sure": "Certeza?",
|
||||
"change_file": "Alterar arquivo",
|
||||
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
|
||||
"click_to_upload": "Clique para fazer upload",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Máximo de {max} registros permitidos.",
|
||||
"default_connector_name_csv": "Importação CSV",
|
||||
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
|
||||
"delete_source": "Excluir fonte",
|
||||
"deselect_all": "Desmarcar tudo",
|
||||
"drop_a_field_here": "Solte um campo aqui",
|
||||
"drop_field_or": "Solte o campo ou",
|
||||
"edit_source_connection": "Editar conexão de origem",
|
||||
"element_selected": "<strong>{count}</strong> elemento selecionado. Cada resposta a este elemento criará um FeedbackRecord no Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elementos selecionados. Cada resposta a estes elementos criará um FeedbackRecord no Hub.",
|
||||
"enter_name_for_source": "Digite um nome para esta origem",
|
||||
"enter_value": "Digite o valor...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} respostas existentes × {elementCount} elementos = {total} registros de feedback",
|
||||
"feedback_date": "Data do feedback",
|
||||
"formbricks_surveys": "Pesquisas Formbricks",
|
||||
"historical_import_complete": "Importação concluída: {successes} bem-sucedidas, {failures} falharam, {skipped} ignoradas (sem dados)",
|
||||
"hub_feedback_record_fields": "Campos de registro de feedback do Hub",
|
||||
"import_csv_data": "Importar dados CSV",
|
||||
"import_existing_responses": "Importar respostas existentes",
|
||||
"import_rows": "Importar {count} linhas",
|
||||
"importing_data": "Importando dados...",
|
||||
"importing_historical_data": "Importando dados históricos...",
|
||||
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
|
||||
"load_sample_csv": "Carregar CSV de exemplo",
|
||||
"n_supported_elements": "{count, plural, one {# elemento suportado} other {# elementos suportados}}",
|
||||
"n_supported_questions": "{count} perguntas suportadas",
|
||||
"no_source_fields_loaded": "Nenhum campo de origem carregado ainda",
|
||||
"no_sources_connected": "Nenhuma origem conectada ainda. Adicione uma origem para começar.",
|
||||
"no_surveys_found": "Nenhuma pesquisa encontrada neste ambiente",
|
||||
"optional": "Opcional",
|
||||
"or_drag_and_drop": "ou arraste e solte",
|
||||
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registro de feedback.",
|
||||
"question_type_not_supported": "Este tipo de pergunta não é suportado",
|
||||
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registro de feedback.",
|
||||
"required": "Obrigatório",
|
||||
"save_changes": "Salvar alterações",
|
||||
"select_a_survey_to_see_elements": "Selecione uma pesquisa para ver seus elementos",
|
||||
"select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas",
|
||||
"select_a_value": "Selecione um valor...",
|
||||
"select_all": "Selecionar tudo",
|
||||
"select_elements": "Selecionar elementos",
|
||||
"select_questions": "Selecionar perguntas",
|
||||
"select_source_type_description": "Selecione o tipo de fonte de feedback que você deseja conectar.",
|
||||
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que você deseja conectar:",
|
||||
"select_survey": "Selecionar pesquisa",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Nome da origem",
|
||||
"source_type_cannot_be_changed": "O tipo de origem não pode ser alterado",
|
||||
"sources": "Origens",
|
||||
"status_active": "Ativa",
|
||||
"status_active": "Em andamento",
|
||||
"status_completed": "Concluído",
|
||||
"status_draft": "Rascunho",
|
||||
"status_error": "Erro",
|
||||
"status_paused": "Pausado",
|
||||
"survey_has_no_elements": "Esta pesquisa não possui elementos de pergunta",
|
||||
"survey_has_no_questions": "Esta pesquisa não possui perguntas",
|
||||
"survey_import_line": "{surveyName}: {responseCount} respostas × {questionCount} perguntas = {total} registros de feedback",
|
||||
"total_feedback_records": "Total: {checked} de {total} registros de feedback selecionados em {surveyCount} pesquisas",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
"update_mapping_description": "Atualize a configuração de mapeamento para esta fonte.",
|
||||
"updated_at": "Atualizado em",
|
||||
"upload_csv_data_description": "Faça upload de um arquivo CSV para importar dados de feedback no Hub.",
|
||||
"upload_csv_file": "Fazer upload de arquivo CSV",
|
||||
"yes_delete": "Sim, deletar"
|
||||
"upload_csv_file": "Fazer upload de arquivo CSV"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Adicionar fonte de feedback",
|
||||
"add_source": "Adicionar fonte",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"are_you_sure": "Tem a certeza?",
|
||||
"change_file": "Alterar ficheiro",
|
||||
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
|
||||
"click_to_upload": "Clique para carregar",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Máximo de {max} registos permitidos.",
|
||||
"default_connector_name_csv": "Importação CSV",
|
||||
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
|
||||
"delete_source": "Eliminar fonte",
|
||||
"deselect_all": "Desselecionar tudo",
|
||||
"drop_a_field_here": "Solte um campo aqui",
|
||||
"drop_field_or": "Solte o campo ou",
|
||||
"edit_source_connection": "Editar ligação de origem",
|
||||
"element_selected": "<strong>{count}</strong> elemento selecionado. Cada resposta a este elemento criará um FeedbackRecord no Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elementos selecionados. Cada resposta a estes elementos criará um FeedbackRecord no Hub.",
|
||||
"enter_name_for_source": "Introduz um nome para esta origem",
|
||||
"enter_value": "Introduzir valor...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} respostas existentes × {elementCount} elementos = {total} registos de feedback",
|
||||
"feedback_date": "Data do feedback",
|
||||
"formbricks_surveys": "Pesquisas Formbricks",
|
||||
"historical_import_complete": "Importação concluída: {successes} com sucesso, {failures} falharam, {skipped} ignorados (sem dados)",
|
||||
"hub_feedback_record_fields": "Campos de registo de feedback do Hub",
|
||||
"import_csv_data": "Importar dados CSV",
|
||||
"import_existing_responses": "Importar respostas existentes",
|
||||
"import_rows": "Importar {count} linhas",
|
||||
"importing_data": "A importar dados...",
|
||||
"importing_historical_data": "A importar dados históricos...",
|
||||
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
|
||||
"load_sample_csv": "Carregar CSV de exemplo",
|
||||
"n_supported_elements": "{count} elementos suportados",
|
||||
"n_supported_questions": "{count} perguntas suportadas",
|
||||
"no_source_fields_loaded": "Ainda não foram carregados campos de origem",
|
||||
"no_sources_connected": "Ainda não há origens ligadas. Adicione uma origem para começar.",
|
||||
"no_surveys_found": "Nenhum inquérito encontrado neste ambiente",
|
||||
"optional": "Opcional",
|
||||
"or_drag_and_drop": "ou arraste e largue",
|
||||
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registo de feedback.",
|
||||
"question_type_not_supported": "Este tipo de pergunta não é suportado",
|
||||
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registo de feedback.",
|
||||
"required": "Obrigatório",
|
||||
"save_changes": "Guardar alterações",
|
||||
"select_a_survey_to_see_elements": "Selecione um inquérito para ver os seus elementos",
|
||||
"select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas",
|
||||
"select_a_value": "Selecione um valor...",
|
||||
"select_all": "Selecionar tudo",
|
||||
"select_elements": "Selecionar elementos",
|
||||
"select_questions": "Selecionar perguntas",
|
||||
"select_source_type_description": "Selecione o tipo de fonte de feedback que pretende conectar.",
|
||||
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que pretende conectar:",
|
||||
"select_survey": "Selecionar inquérito",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Nome da fonte",
|
||||
"source_type_cannot_be_changed": "O tipo de fonte não pode ser alterado",
|
||||
"sources": "Fontes",
|
||||
"status_active": "Ativa",
|
||||
"status_active": "Em progresso",
|
||||
"status_completed": "Concluído",
|
||||
"status_draft": "Rascunho",
|
||||
"status_error": "Erro",
|
||||
"status_paused": "Em pausa",
|
||||
"survey_has_no_elements": "Este inquérito não tem elementos de pergunta",
|
||||
"survey_has_no_questions": "Este inquérito não tem perguntas",
|
||||
"survey_import_line": "{surveyName}: {responseCount} respostas × {questionCount} perguntas = {total} registos de feedback",
|
||||
"total_feedback_records": "Total: {checked} de {total} registos de feedback selecionados em {surveyCount} inquéritos",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
"update_mapping_description": "Atualiza a configuração de mapeamento para esta origem.",
|
||||
"updated_at": "Atualizado em",
|
||||
"upload_csv_data_description": "Carregue um ficheiro CSV para importar dados de feedback para o Hub.",
|
||||
"upload_csv_file": "Carregar ficheiro CSV",
|
||||
"yes_delete": "Sim, eliminar"
|
||||
"upload_csv_file": "Carregar ficheiro CSV"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Adaugă sursă de feedback",
|
||||
"add_source": "Adaugă sursă",
|
||||
"allowed_values": "Valori permise: {values}",
|
||||
"are_you_sure": "Ești sigur?",
|
||||
"change_file": "Schimbă fișierul",
|
||||
"click_load_sample_csv": "Apasă pe „Încarcă CSV de exemplu” pentru a vedea coloanele",
|
||||
"click_to_upload": "Apasă pentru a încărca",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Sunt permise maximum {max} înregistrări.",
|
||||
"default_connector_name_csv": "Import CSV",
|
||||
"default_connector_name_formbricks": "Conexiune chestionar Formbricks",
|
||||
"delete_source": "Șterge sursa",
|
||||
"deselect_all": "Deselectează tot",
|
||||
"drop_a_field_here": "Trage un câmp aici",
|
||||
"drop_field_or": "Trage câmpul sau",
|
||||
"edit_source_connection": "Editează conexiunea sursei",
|
||||
"element_selected": "<strong>{count}</strong> element selectat. Fiecare răspuns la aceste elemente va crea un FeedbackRecord în Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> elemente selectate. Fiecare răspuns la aceste elemente va crea un FeedbackRecord în Hub.",
|
||||
"enter_name_for_source": "Introdu un nume pentru această sursă",
|
||||
"enter_value": "Introdu valoarea...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} răspunsuri existente × {elementCount} elemente = {total} înregistrări de feedback",
|
||||
"feedback_date": "Data feedbackului",
|
||||
"formbricks_surveys": "Chestionare Formbricks",
|
||||
"historical_import_complete": "Import finalizat: {successes} reușite, {failures} eșuate, {skipped} omise (fără date)",
|
||||
"hub_feedback_record_fields": "Câmpuri FeedbackRecord din Hub",
|
||||
"import_csv_data": "Importă date CSV",
|
||||
"import_existing_responses": "Importă răspunsuri existente",
|
||||
"import_rows": "Importă {count} rânduri",
|
||||
"import_rows": "Importă {count, plural, one {# rând} few {# rânduri} other {# de rânduri}}",
|
||||
"importing_data": "Se importă datele...",
|
||||
"importing_historical_data": "Se importă date istorice...",
|
||||
"invalid_enum_values": "Valori invalide în coloana mapată la {field}",
|
||||
"invalid_values_found": "Găsite: {values} (rânduri: {rows}) {extra}",
|
||||
"load_sample_csv": "Încarcă un CSV de exemplu",
|
||||
"n_supported_elements": "{count} elemente suportate",
|
||||
"n_supported_questions": "{count} întrebări acceptate",
|
||||
"no_source_fields_loaded": "Nu au fost încă încărcate câmpuri sursă",
|
||||
"no_sources_connected": "Nicio sursă conectată încă. Adaugă o sursă pentru a începe.",
|
||||
"no_surveys_found": "Nu s-au găsit sondaje în acest mediu",
|
||||
"optional": "Opțional",
|
||||
"or_drag_and_drop": "sau trage și lasă aici",
|
||||
"question_selected": "<strong>{count}</strong> întrebare selectată. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
|
||||
"question_type_not_supported": "Acest tip de întrebare nu este suportat",
|
||||
"questions_selected": "<strong>{count}</strong> întrebări selectate. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
|
||||
"required": "Obligatoriu",
|
||||
"save_changes": "Salvează modificările",
|
||||
"select_a_survey_to_see_elements": "Selectează un sondaj pentru a vedea elementele",
|
||||
"select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările",
|
||||
"select_a_value": "Selectează o valoare...",
|
||||
"select_all": "Selectează tot",
|
||||
"select_elements": "Selectează elemente",
|
||||
"select_questions": "Selectează întrebări",
|
||||
"select_source_type_description": "Selectează tipul sursei de feedback pe care vrei să o conectezi.",
|
||||
"select_source_type_prompt": "Selectează tipul sursei de feedback pe care vrei să o conectezi:",
|
||||
"select_survey": "Selectează chestionar",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Nume sursă",
|
||||
"source_type_cannot_be_changed": "Tipul sursei nu poate fi schimbat",
|
||||
"sources": "Surse",
|
||||
"status_active": "Activ",
|
||||
"status_active": "În progres",
|
||||
"status_completed": "Finalizat",
|
||||
"status_draft": "Schiță",
|
||||
"status_error": "Eroare",
|
||||
"status_paused": "Pauzat",
|
||||
"survey_has_no_elements": "Acest chestionar nu are elemente de întrebare",
|
||||
"survey_has_no_questions": "Acest sondaj nu are întrebări",
|
||||
"survey_import_line": "{surveyName}: {responseCount} răspunsuri × {questionCount} întrebări = {total} Feedback Records",
|
||||
"total_feedback_records": "Total: {checked} din {total} Feedback Records selectate în {surveyCount} sondaje",
|
||||
"unify_feedback": "Unify Feedback",
|
||||
"update_mapping_description": "Actualizează configurația de mapare pentru această sursă.",
|
||||
"updated_at": "Actualizat la",
|
||||
"upload_csv_data_description": "Încarcă un fișier CSV pentru a importa date de feedback în Hub.",
|
||||
"upload_csv_file": "Încarcă fișier CSV",
|
||||
"yes_delete": "Da, șterge"
|
||||
"upload_csv_file": "Încarcă fișier CSV"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Добавить источник отзывов",
|
||||
"add_source": "Добавить источник",
|
||||
"allowed_values": "Допустимые значения: {values}",
|
||||
"are_you_sure": "Вы уверены?",
|
||||
"change_file": "Изменить файл",
|
||||
"click_load_sample_csv": "Нажмите «Загрузить пример CSV», чтобы увидеть столбцы",
|
||||
"click_to_upload": "Кликните для загрузки",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Допустимо не более {max} записей.",
|
||||
"default_connector_name_csv": "Импорт CSV",
|
||||
"default_connector_name_formbricks": "Подключение опроса Formbricks",
|
||||
"delete_source": "Удалить источник",
|
||||
"deselect_all": "Снять выделение со всех",
|
||||
"drop_a_field_here": "Перетащи сюда поле",
|
||||
"drop_field_or": "Перетащи поле или",
|
||||
"edit_source_connection": "Редактировать подключение источника",
|
||||
"element_selected": "<strong>{count}</strong> элемент выбран. Каждый ответ на эти элементы создаст FeedbackRecord в Hub.",
|
||||
"elements_selected": "<strong>{count}</strong> элементов выбрано. Каждый ответ на эти элементы создаст FeedbackRecord в Hub.",
|
||||
"enter_name_for_source": "Введи имя для этого источника",
|
||||
"enter_value": "Введите значение...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} существующих ответов × {elementCount} элементов = {total} записей обратной связи",
|
||||
"feedback_date": "Дата отзыва",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "Импорт завершён: {successes} успешно, {failures} с ошибками, {skipped} пропущено (нет данных)",
|
||||
"hub_feedback_record_fields": "Поля FeedbackRecord в Hub",
|
||||
"import_csv_data": "Импортировать данные CSV",
|
||||
"import_existing_responses": "Импортировать существующие ответы",
|
||||
"import_rows": "Импортировать {count} строк",
|
||||
"importing_data": "Импортируем данные...",
|
||||
"importing_historical_data": "Импортируем исторические данные...",
|
||||
"import_rows": "Импортировать {count, plural, one {# строку} few {# строки} many {# строк} other {# строки}}",
|
||||
"importing_data": "Импорт данных...",
|
||||
"invalid_enum_values": "Недопустимые значения в столбце, сопоставленном с {field}",
|
||||
"invalid_values_found": "Найдено: {values} (строки: {rows}) {extra}",
|
||||
"load_sample_csv": "Загрузить пример CSV",
|
||||
"n_supported_elements": "{count} поддерживаемых элементов",
|
||||
"n_supported_questions": "Поддерживается {count} вопрос(ов)",
|
||||
"no_source_fields_loaded": "Поля источника ещё не загружены",
|
||||
"no_sources_connected": "Нет подключённых источников. Добавьте источник, чтобы начать.",
|
||||
"no_surveys_found": "В этой среде не найдено опросов",
|
||||
"optional": "Необязательно",
|
||||
"or_drag_and_drop": "или перетащите файл",
|
||||
"question_selected": "<strong>{count}</strong> выбранный вопрос. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
|
||||
"question_type_not_supported": "Этот тип вопроса не поддерживается",
|
||||
"questions_selected": "<strong>{count}</strong> выбранных вопроса. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
|
||||
"required": "Обязательно",
|
||||
"save_changes": "Сохранить изменения",
|
||||
"select_a_survey_to_see_elements": "Выберите опрос, чтобы увидеть его элементы",
|
||||
"select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы",
|
||||
"select_a_value": "Выберите значение...",
|
||||
"select_all": "Выбрать все",
|
||||
"select_elements": "Выбрать элементы",
|
||||
"select_questions": "Выберите вопросы",
|
||||
"select_source_type_description": "Выберите тип источника отзывов, который хотите подключить.",
|
||||
"select_source_type_prompt": "Выберите тип источника отзывов, который хотите подключить:",
|
||||
"select_survey": "Выбрать опрос",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Имя источника",
|
||||
"source_type_cannot_be_changed": "Тип источника нельзя изменить",
|
||||
"sources": "Источники",
|
||||
"status_active": "Активен",
|
||||
"status_active": "В процессе",
|
||||
"status_completed": "Завершён",
|
||||
"status_draft": "Черновик",
|
||||
"status_error": "Ошибка",
|
||||
"status_paused": "Приостановлен",
|
||||
"survey_has_no_elements": "В этом опросе нет вопросов",
|
||||
"survey_has_no_questions": "В этом опросе нет вопросов",
|
||||
"survey_import_line": "{surveyName}: {responseCount} ответов × {questionCount} вопросов = {total} записей обратной связи",
|
||||
"total_feedback_records": "Всего: выбрано {checked} из {total} записей обратной связи в {surveyCount} опросах",
|
||||
"unify_feedback": "Обратная связь Unify",
|
||||
"update_mapping_description": "Обнови настройки сопоставления для этого источника.",
|
||||
"updated_at": "Обновлено",
|
||||
"upload_csv_data_description": "Загрузите CSV-файл, чтобы импортировать данные обратной связи в Hub.",
|
||||
"upload_csv_file": "Загрузить CSV-файл",
|
||||
"yes_delete": "Да, удалить"
|
||||
"upload_csv_file": "Загрузить CSV-файл"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "Lägg till feedbackkälla",
|
||||
"add_source": "Lägg till källa",
|
||||
"allowed_values": "Tillåtna värden: {values}",
|
||||
"are_you_sure": "Är du säker?",
|
||||
"change_file": "Byt fil",
|
||||
"click_load_sample_csv": "Klicka på 'Ladda exempel-CSV' för att se kolumner",
|
||||
"click_to_upload": "Klicka för att ladda upp",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "Maximalt {max} poster tillåtna.",
|
||||
"default_connector_name_csv": "CSV-import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey-anslutning",
|
||||
"delete_source": "Ta bort källa",
|
||||
"deselect_all": "Avmarkera alla",
|
||||
"drop_a_field_here": "Släpp ett fält här",
|
||||
"drop_field_or": "Släpp fält eller",
|
||||
"edit_source_connection": "Redigera källans anslutning",
|
||||
"element_selected": "<strong>{count}</strong> element vald. Varje svar på dessa element skapar en FeedbackRecord i Hubben.",
|
||||
"elements_selected": "<strong>{count}</strong> element valda. Varje svar på dessa element skapar en FeedbackRecord i Hubben.",
|
||||
"enter_name_for_source": "Ange ett namn för denna källa",
|
||||
"enter_value": "Ange värde...",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} befintliga svar × {elementCount} element = {total} feedbackposter",
|
||||
"feedback_date": "Feedbackdatum",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "Importen klar: {successes} lyckades, {failures} misslyckades, {skipped} hoppades över (ingen data)",
|
||||
"hub_feedback_record_fields": "Fält för Hub Feedback Record",
|
||||
"import_csv_data": "Importera CSV-data",
|
||||
"import_existing_responses": "Importera befintliga svar",
|
||||
"import_rows": "Importera {count} rader",
|
||||
"importing_data": "Importerar data...",
|
||||
"importing_historical_data": "Importerar historiska data...",
|
||||
"invalid_enum_values": "Ogiltiga värden i kolumnen som är kopplad till {field}",
|
||||
"invalid_values_found": "Hittade: {values} (rader: {rows}) {extra}",
|
||||
"load_sample_csv": "Ladda exempel-CSV",
|
||||
"n_supported_elements": "{count} stödda element",
|
||||
"n_supported_questions": "{count} stödda frågor",
|
||||
"no_source_fields_loaded": "Inga källfält har laddats än",
|
||||
"no_sources_connected": "Inga källor är anslutna än. Lägg till en källa för att komma igång.",
|
||||
"no_surveys_found": "Inga enkäter hittades i denna miljö",
|
||||
"optional": "Valfritt",
|
||||
"or_drag_and_drop": "eller dra och släpp",
|
||||
"question_selected": "<strong>{count}</strong> fråga vald. Varje svar på dessa frågor skapar en ny feedbackpost.",
|
||||
"question_type_not_supported": "Den här frågetypen stöds inte",
|
||||
"questions_selected": "<strong>{count}</strong> frågor valda. Varje svar på dessa frågor skapar en ny feedbackpost.",
|
||||
"required": "Obligatoriskt",
|
||||
"save_changes": "Spara ändringar",
|
||||
"select_a_survey_to_see_elements": "Välj en enkät för att se dess element",
|
||||
"select_a_survey_to_see_questions": "Välj en enkät för att se dess frågor",
|
||||
"select_a_value": "Välj ett värde...",
|
||||
"select_all": "Välj alla",
|
||||
"select_elements": "Välj element",
|
||||
"select_questions": "Välj frågor",
|
||||
"select_source_type_description": "Välj vilken typ av feedbackkälla du vill ansluta.",
|
||||
"select_source_type_prompt": "Välj vilken typ av feedbackkälla du vill ansluta:",
|
||||
"select_survey": "Välj enkät",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "Källnamn",
|
||||
"source_type_cannot_be_changed": "Källtyp kan inte ändras",
|
||||
"sources": "Källor",
|
||||
"status_active": "Aktiv",
|
||||
"status_active": "Pågående",
|
||||
"status_completed": "Slutförd",
|
||||
"status_draft": "Utkast",
|
||||
"status_error": "Fel",
|
||||
"status_paused": "Pausad",
|
||||
"survey_has_no_elements": "Den här enkäten har inga frågeelement",
|
||||
"survey_has_no_questions": "Den här enkäten har inga frågor",
|
||||
"survey_import_line": "{surveyName}: {responseCount} svar × {questionCount} frågor = {total} feedbackposter",
|
||||
"total_feedback_records": "Totalt: {checked} av {total} feedbackposter valda i {surveyCount} enkäter",
|
||||
"unify_feedback": "Samla feedback",
|
||||
"update_mapping_description": "Uppdatera mappningskonfigurationen för den här källan.",
|
||||
"updated_at": "Uppdaterad",
|
||||
"upload_csv_data_description": "Ladda upp en CSV-fil för att importera feedbackdata till Hub.",
|
||||
"upload_csv_file": "Ladda upp CSV-fil",
|
||||
"yes_delete": "Ja, ta bort"
|
||||
"upload_csv_file": "Ladda upp CSV-fil"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "添加反馈来源",
|
||||
"add_source": "添加来源",
|
||||
"allowed_values": "允许的值:{values}",
|
||||
"are_you_sure": "你确定吗?",
|
||||
"change_file": "更换文件",
|
||||
"click_load_sample_csv": "点击“加载示例 CSV”查看列",
|
||||
"click_to_upload": "点击上传",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "最多允许 {max} 条记录。",
|
||||
"default_connector_name_csv": "CSV 导入",
|
||||
"default_connector_name_formbricks": "Formbricks 调查连接",
|
||||
"delete_source": "删除来源",
|
||||
"deselect_all": "取消全选",
|
||||
"drop_a_field_here": "将字段拖到这里",
|
||||
"drop_field_or": "拖放字段或",
|
||||
"edit_source_connection": "编辑源连接",
|
||||
"element_selected": "已选择 <strong>{count}</strong> 个元素。每个元素的反馈都会在 Hub 中创建一个 FeedbackRecord。",
|
||||
"elements_selected": "已选择 <strong>{count}</strong> 个元素。每个元素的反馈都会在 Hub 中创建一个 FeedbackRecord。",
|
||||
"enter_name_for_source": "为此来源输入名称",
|
||||
"enter_value": "请输入值...",
|
||||
"enum": "枚举",
|
||||
"existing_responses_info": "{responseCount} 条现有回复 × {elementCount} 个元素 = {total} 条反馈记录",
|
||||
"feedback_date": "反馈日期",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
"historical_import_complete": "导入完成:{successes} 个成功,{failures} 个失败,{skipped} 个跳过(无数据)",
|
||||
"hub_feedback_record_fields": "Hub 反馈记录字段",
|
||||
"import_csv_data": "导入 CSV 数据",
|
||||
"import_existing_responses": "导入现有回复",
|
||||
"import_rows": "导入 {count} 行",
|
||||
"importing_data": "正在导入数据...",
|
||||
"importing_historical_data": "正在导入历史数据...",
|
||||
"import_rows": "导入{count}行数据",
|
||||
"importing_data": "正在导入数据…",
|
||||
"invalid_enum_values": "映射到 {field} 的列中存在无效值",
|
||||
"invalid_values_found": "发现:{values}(行:{rows}){extra}",
|
||||
"load_sample_csv": "加载示例 CSV",
|
||||
"n_supported_elements": "{count} 个支持的元素",
|
||||
"n_supported_questions": "{count} 个支持的问题",
|
||||
"no_source_fields_loaded": "尚未加载源字段",
|
||||
"no_sources_connected": "还没有连接数据源。添加一个数据源开始吧。",
|
||||
"no_surveys_found": "此环境下未找到调查",
|
||||
"optional": "可选",
|
||||
"or_drag_and_drop": "或拖放",
|
||||
"question_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
|
||||
"question_type_not_supported": "不支持此问题类型",
|
||||
"questions_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
|
||||
"required": "必填",
|
||||
"save_changes": "保存更改",
|
||||
"select_a_survey_to_see_elements": "选择一个调查以查看其元素",
|
||||
"select_a_survey_to_see_questions": "请选择一个调查以查看其问题",
|
||||
"select_a_value": "选择一个值...",
|
||||
"select_all": "全选",
|
||||
"select_elements": "选择元素",
|
||||
"select_questions": "选择问题",
|
||||
"select_source_type_description": "请选择你想要连接的反馈来源类型。",
|
||||
"select_source_type_prompt": "请选择你想要连接的反馈来源类型:",
|
||||
"select_survey": "选择调查",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "来源名称",
|
||||
"source_type_cannot_be_changed": "来源类型无法更改",
|
||||
"sources": "来源",
|
||||
"status_active": "已激活",
|
||||
"status_active": "进行中",
|
||||
"status_completed": "已完成",
|
||||
"status_draft": "草稿",
|
||||
"status_error": "错误",
|
||||
"status_paused": "已暂停",
|
||||
"survey_has_no_elements": "此调查没有问题元素",
|
||||
"survey_has_no_questions": "该调查没有任何问题",
|
||||
"survey_import_line": "{surveyName}:{responseCount} 份答卷 × {questionCount} 个问题 = {total} 条反馈记录",
|
||||
"total_feedback_records": "总计:{surveyCount} 个调查中已选 {checked} / {total} 条反馈记录",
|
||||
"unify_feedback": "统一反馈",
|
||||
"update_mapping_description": "更新此来源的映射配置。",
|
||||
"updated_at": "更新于",
|
||||
"upload_csv_data_description": "上传 CSV 文件,将反馈数据导入 Hub。",
|
||||
"upload_csv_file": "上传 CSV 文件",
|
||||
"yes_delete": "是的,删除"
|
||||
"upload_csv_file": "上传 CSV 文件"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -2043,7 +2043,6 @@
|
||||
"add_feedback_source": "新增回饋來源",
|
||||
"add_source": "新增來源",
|
||||
"allowed_values": "允許的值:{values}",
|
||||
"are_you_sure": "您確定嗎?",
|
||||
"change_file": "更換檔案",
|
||||
"click_load_sample_csv": "點擊「載入範例 CSV」以查看欄位",
|
||||
"click_to_upload": "點擊以上傳",
|
||||
@@ -2069,42 +2068,38 @@
|
||||
"csv_max_records": "最多允許 {max} 筆紀錄。",
|
||||
"default_connector_name_csv": "CSV 匯入",
|
||||
"default_connector_name_formbricks": "Formbricks 問卷連線",
|
||||
"delete_source": "刪除來源",
|
||||
"deselect_all": "取消全選",
|
||||
"drop_a_field_here": "請將欄位拖曳到這裡",
|
||||
"drop_field_or": "拖曳欄位或",
|
||||
"edit_source_connection": "編輯來源連線",
|
||||
"element_selected": "已選取 <strong>{count}</strong> 個元素。每個對這些元素的回應都會在 Hub 中建立一個 FeedbackRecord。",
|
||||
"elements_selected": "已選取 <strong>{count}</strong> 個元素。每個對這些元素的回應都會在 Hub 中建立一個 FeedbackRecord。",
|
||||
"enter_name_for_source": "請輸入此來源的名稱",
|
||||
"enter_value": "請輸入值……",
|
||||
"enum": "enum",
|
||||
"existing_responses_info": "{responseCount} 答覆 × {elementCount} 元素 = {total} 筆回饋紀錄",
|
||||
"feedback_date": "回饋日期",
|
||||
"formbricks_surveys": "Formbricks 問卷",
|
||||
"historical_import_complete": "匯入完成:{successes} 筆成功,{failures} 筆失敗,{skipped} 筆略過(無資料)",
|
||||
"hub_feedback_record_fields": "Hub 回饋紀錄欄位",
|
||||
"import_csv_data": "匯入 CSV 資料",
|
||||
"import_existing_responses": "匯入現有答覆",
|
||||
"import_rows": "匯入 {count} 筆資料",
|
||||
"importing_data": "正在匯入資料⋯⋯",
|
||||
"importing_historical_data": "正在匯入歷史資料⋯⋯",
|
||||
"importing_data": "正在匯入資料…",
|
||||
"invalid_enum_values": "對應到 {field} 欄位的值無效",
|
||||
"invalid_values_found": "發現:{values}(列:{rows}){extra}",
|
||||
"load_sample_csv": "載入範例 CSV",
|
||||
"n_supported_elements": "支援 {count} 個元素",
|
||||
"n_supported_questions": "{count} 個支援的問題",
|
||||
"no_source_fields_loaded": "尚未載入來源欄位",
|
||||
"no_sources_connected": "尚未連接任何來源。請新增來源以開始使用。",
|
||||
"no_surveys_found": "此環境中找不到問卷",
|
||||
"optional": "選填",
|
||||
"or_drag_and_drop": "或拖曳檔案",
|
||||
"question_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
|
||||
"question_type_not_supported": "不支援此題型",
|
||||
"questions_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
|
||||
"required": "必填",
|
||||
"save_changes": "儲存變更",
|
||||
"select_a_survey_to_see_elements": "請選擇問卷以查看其元素",
|
||||
"select_a_survey_to_see_questions": "請選擇問卷以查看其問題",
|
||||
"select_a_value": "請選擇一個值...",
|
||||
"select_all": "全選",
|
||||
"select_elements": "選取元素",
|
||||
"select_questions": "選擇問題",
|
||||
"select_source_type_description": "請選擇你想要連接的回饋來源類型。",
|
||||
"select_source_type_prompt": "請選擇你想要連接的回饋來源類型:",
|
||||
"select_survey": "選擇問卷",
|
||||
@@ -2120,18 +2115,19 @@
|
||||
"source_name": "來源名稱",
|
||||
"source_type_cannot_be_changed": "來源類型無法變更",
|
||||
"sources": "來源",
|
||||
"status_active": "啟用中",
|
||||
"status_active": "進行中",
|
||||
"status_completed": "已完成",
|
||||
"status_draft": "草稿",
|
||||
"status_error": "錯誤",
|
||||
"status_paused": "已暫停",
|
||||
"survey_has_no_elements": "此問卷沒有任何問題",
|
||||
"survey_has_no_questions": "此問卷沒有任何題目",
|
||||
"survey_import_line": "{surveyName}:{responseCount} 份回應 × {questionCount} 題 = {total} 筆意見紀錄",
|
||||
"total_feedback_records": "總計:{surveyCount} 份問卷中已選擇 {checked} / {total} 筆意見紀錄",
|
||||
"unify_feedback": "整合回饋",
|
||||
"update_mapping_description": "更新此來源的對應設定。",
|
||||
"updated_at": "更新時間",
|
||||
"upload_csv_data_description": "上傳 CSV 檔案,將回饋資料匯入 Hub。",
|
||||
"upload_csv_file": "上傳 CSV 檔案",
|
||||
"yes_delete": "確定刪除"
|
||||
"upload_csv_file": "上傳 CSV 檔案"
|
||||
},
|
||||
"workspace": {
|
||||
"api_keys": {
|
||||
|
||||
@@ -30,16 +30,16 @@ export const ContactsSecondaryNavigation = async ({
|
||||
label: t("common.contacts"),
|
||||
href: `/environments/${environmentId}/contacts`,
|
||||
},
|
||||
{
|
||||
id: "segments",
|
||||
label: t("common.segments"),
|
||||
href: `/environments/${environmentId}/segments`,
|
||||
},
|
||||
{
|
||||
id: "attributes",
|
||||
label: t("common.attributes"),
|
||||
href: `/environments/${environmentId}/attributes`,
|
||||
},
|
||||
{
|
||||
id: "segments",
|
||||
label: t("common.segments"),
|
||||
href: `/environments/${environmentId}/segments`,
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
|
||||
|
||||
80
apps/web/modules/hub/hub-client.test.ts
Normal file
80
apps/web/modules/hub/hub-client.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import FormbricksHub from "@formbricks/hub";
|
||||
|
||||
vi.mock("@formbricks/hub", () => {
|
||||
const MockFormbricksHub = vi.fn();
|
||||
return { default: MockFormbricksHub };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
HUB_API_KEY: "",
|
||||
HUB_API_URL: "https://hub.test",
|
||||
},
|
||||
}));
|
||||
|
||||
const { env } = await import("@/lib/env");
|
||||
|
||||
const mutableEnv = env as unknown as Record<string, string>;
|
||||
|
||||
const globalForHub = globalThis as unknown as {
|
||||
formbricksHubClient: FormbricksHub | undefined;
|
||||
};
|
||||
|
||||
describe("getHubClient", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalForHub.formbricksHubClient = undefined;
|
||||
});
|
||||
|
||||
test("returns null when HUB_API_KEY is not set", async () => {
|
||||
mutableEnv.HUB_API_KEY = "";
|
||||
|
||||
const { getHubClient } = await import("./hub-client");
|
||||
const client = getHubClient();
|
||||
|
||||
expect(client).toBeNull();
|
||||
expect(FormbricksHub).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("creates and caches a new client when HUB_API_KEY is set", async () => {
|
||||
mutableEnv.HUB_API_KEY = "test-key";
|
||||
const mockInstance = { feedbackRecords: {} } as unknown as FormbricksHub;
|
||||
vi.mocked(FormbricksHub).mockReturnValue(mockInstance);
|
||||
|
||||
const { getHubClient } = await import("./hub-client");
|
||||
const client = getHubClient();
|
||||
|
||||
expect(FormbricksHub).toHaveBeenCalledWith({ apiKey: "test-key", baseURL: "https://hub.test" });
|
||||
expect(client).toBe(mockInstance);
|
||||
expect(globalForHub.formbricksHubClient).toBe(mockInstance);
|
||||
});
|
||||
|
||||
test("returns cached client on subsequent calls", async () => {
|
||||
const cachedInstance = { feedbackRecords: {} } as unknown as FormbricksHub;
|
||||
globalForHub.formbricksHubClient = cachedInstance;
|
||||
|
||||
const { getHubClient } = await import("./hub-client");
|
||||
const client = getHubClient();
|
||||
|
||||
expect(client).toBe(cachedInstance);
|
||||
expect(FormbricksHub).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not cache null result so a later call with the key set can create the client", async () => {
|
||||
mutableEnv.HUB_API_KEY = "";
|
||||
|
||||
const { getHubClient } = await import("./hub-client");
|
||||
const first = getHubClient();
|
||||
expect(first).toBeNull();
|
||||
expect(globalForHub.formbricksHubClient).toBeUndefined();
|
||||
|
||||
mutableEnv.HUB_API_KEY = "now-set";
|
||||
const mockInstance = { feedbackRecords: {} } as unknown as FormbricksHub;
|
||||
vi.mocked(FormbricksHub).mockReturnValue(mockInstance);
|
||||
|
||||
const second = getHubClient();
|
||||
expect(second).toBe(mockInstance);
|
||||
expect(globalForHub.formbricksHubClient).toBe(mockInstance);
|
||||
});
|
||||
});
|
||||
25
apps/web/modules/hub/hub-client.ts
Normal file
25
apps/web/modules/hub/hub-client.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import "server-only";
|
||||
import FormbricksHub from "@formbricks/hub";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
const globalForHub = globalThis as unknown as {
|
||||
formbricksHubClient: FormbricksHub | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a shared Formbricks Hub API client when HUB_API_KEY is set.
|
||||
* Uses a global singleton so the same instance is reused across the process
|
||||
* (and across Next.js HMR in development). When the key is not set, returns
|
||||
* null and does not cache that result so a later call with the key set
|
||||
* can create the client.
|
||||
*/
|
||||
export const getHubClient = (): FormbricksHub | null => {
|
||||
if (globalForHub.formbricksHubClient) {
|
||||
return globalForHub.formbricksHubClient;
|
||||
}
|
||||
const apiKey = env.HUB_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
const client = new FormbricksHub({ apiKey, baseURL: env.HUB_API_URL });
|
||||
globalForHub.formbricksHubClient = client;
|
||||
return client;
|
||||
};
|
||||
3
apps/web/modules/hub/index.ts
Normal file
3
apps/web/modules/hub/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { getHubClient } from "./hub-client";
|
||||
export { createFeedbackRecord, createFeedbackRecordsBatch, type CreateFeedbackRecordResult } from "./service";
|
||||
export type { FeedbackRecordCreateParams, FeedbackRecordData } from "./types";
|
||||
121
apps/web/modules/hub/service.test.ts
Normal file
121
apps/web/modules/hub/service.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { createFeedbackRecord, createFeedbackRecordsBatch } from "./service";
|
||||
import type { FeedbackRecordCreateParams } from "./types";
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("./hub-client", () => ({
|
||||
getHubClient: vi.fn(),
|
||||
}));
|
||||
|
||||
const { getHubClient } = await import("./hub-client");
|
||||
|
||||
const sampleInput: FeedbackRecordCreateParams = {
|
||||
field_id: "el-1",
|
||||
field_type: "rating",
|
||||
source_type: "formbricks",
|
||||
source_id: "survey-1",
|
||||
source_name: "Test Survey",
|
||||
field_label: "Question?",
|
||||
value_number: 5,
|
||||
collected_at: "2026-02-24T10:00:00.000Z",
|
||||
};
|
||||
|
||||
describe("hub service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createFeedbackRecord", () => {
|
||||
test("returns error result when getHubClient returns null", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue(null);
|
||||
|
||||
const result = await createFeedbackRecord(sampleInput);
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toMatchObject({
|
||||
status: 0,
|
||||
message: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns data when client.create succeeds", async () => {
|
||||
const created = { id: "hub-1", ...sampleInput };
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { create: vi.fn().mockResolvedValue(created) },
|
||||
} as any);
|
||||
|
||||
const result = await createFeedbackRecord(sampleInput);
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.data).toEqual(created);
|
||||
});
|
||||
|
||||
test("returns error result when client.create throws", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { create: vi.fn().mockRejectedValue(new Error("Network error")) },
|
||||
} as any);
|
||||
|
||||
const result = await createFeedbackRecord(sampleInput);
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toMatchObject({ message: "Network error" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFeedbackRecordsBatch", () => {
|
||||
test("returns all errors when getHubClient returns null", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue(null);
|
||||
|
||||
const result = await createFeedbackRecordsBatch([sampleInput, { ...sampleInput, field_id: "el-2" }]);
|
||||
|
||||
expect(result.results).toHaveLength(2);
|
||||
result.results.forEach((r) => {
|
||||
expect(r.data).toBeNull();
|
||||
expect(r.error?.message).toContain("HUB_API_KEY is not set");
|
||||
});
|
||||
});
|
||||
|
||||
test("returns results per input when client creates succeed", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: {
|
||||
create: vi
|
||||
.fn()
|
||||
.mockImplementation((input: FeedbackRecordCreateParams) =>
|
||||
Promise.resolve({ id: `hub-${input.field_id}`, ...input })
|
||||
),
|
||||
},
|
||||
} as any);
|
||||
|
||||
const inputs = [sampleInput, { ...sampleInput, field_id: "el-2" }];
|
||||
const result = await createFeedbackRecordsBatch(inputs);
|
||||
|
||||
expect(result.results).toHaveLength(2);
|
||||
expect(result.results[0].data).toMatchObject({ field_id: "el-1" });
|
||||
expect(result.results[0].error).toBeNull();
|
||||
expect(result.results[1].data).toMatchObject({ field_id: "el-2" });
|
||||
expect(result.results[1].error).toBeNull();
|
||||
});
|
||||
|
||||
test("returns mixed results when some creates fail", async () => {
|
||||
const create = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: "hub-1", ...sampleInput })
|
||||
.mockRejectedValueOnce(new Error("Rate limited"));
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { create },
|
||||
} as any);
|
||||
|
||||
const inputs = [sampleInput, { ...sampleInput, field_id: "el-2" }];
|
||||
const result = await createFeedbackRecordsBatch(inputs);
|
||||
|
||||
expect(result.results).toHaveLength(2);
|
||||
expect(result.results[0].data).not.toBeNull();
|
||||
expect(result.results[0].error).toBeNull();
|
||||
expect(result.results[1].data).toBeNull();
|
||||
expect(result.results[1].error).toMatchObject({ message: "Rate limited" });
|
||||
});
|
||||
});
|
||||
});
|
||||
70
apps/web/modules/hub/service.ts
Normal file
70
apps/web/modules/hub/service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import "server-only";
|
||||
import FormbricksHub from "@formbricks/hub";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { getHubClient } from "./hub-client";
|
||||
import type { FeedbackRecordCreateParams, FeedbackRecordData } from "./types";
|
||||
|
||||
export type CreateFeedbackRecordResult = {
|
||||
data: FeedbackRecordData | null;
|
||||
error: { status: number; message: string; detail: string } | null;
|
||||
};
|
||||
|
||||
const NO_CONFIG_ERROR = {
|
||||
status: 0,
|
||||
message: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
detail: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
} as const;
|
||||
|
||||
const createResultFromError = (err: unknown): CreateFeedbackRecordResult => {
|
||||
const status = err instanceof FormbricksHub.APIError ? err.status : 0;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { data: null, error: { status, message, detail: message } };
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a single feedback record in the Hub.
|
||||
* Returns a result shape with data or error; logs failures.
|
||||
*/
|
||||
export const createFeedbackRecord = async (
|
||||
input: FeedbackRecordCreateParams
|
||||
): Promise<CreateFeedbackRecordResult> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return { data: null, error: { ...NO_CONFIG_ERROR } };
|
||||
}
|
||||
try {
|
||||
const data = await client.feedbackRecords.create(input);
|
||||
return { data, error: null };
|
||||
} catch (err) {
|
||||
logger.warn({ err, fieldId: input.field_id }, "Hub: createFeedbackRecord failed");
|
||||
return createResultFromError(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create multiple feedback records in the Hub in parallel.
|
||||
* Returns an array of results (data or error) per input; logs failures.
|
||||
*/
|
||||
export const createFeedbackRecordsBatch = async (
|
||||
inputs: FeedbackRecordCreateParams[]
|
||||
): Promise<{ results: CreateFeedbackRecordResult[] }> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return {
|
||||
results: inputs.map(() => ({ data: null, error: { ...NO_CONFIG_ERROR } })),
|
||||
};
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
try {
|
||||
const data = await client.feedbackRecords.create(input);
|
||||
return { data, error: null as CreateFeedbackRecordResult["error"] };
|
||||
} catch (err) {
|
||||
logger.warn({ err, fieldId: input.field_id }, "Hub: createFeedbackRecord failed");
|
||||
return createResultFromError(err);
|
||||
}
|
||||
})
|
||||
);
|
||||
return { results };
|
||||
};
|
||||
4
apps/web/modules/hub/types.ts
Normal file
4
apps/web/modules/hub/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type FormbricksHub from "@formbricks/hub";
|
||||
|
||||
export type FeedbackRecordCreateParams = FormbricksHub.FeedbackRecordCreateParams;
|
||||
export type FeedbackRecordData = FormbricksHub.FeedbackRecordData;
|
||||
@@ -30,6 +30,7 @@
|
||||
"@formbricks/cache": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/email": "workspace:*",
|
||||
"@formbricks/hub": "0.3.0",
|
||||
"@formbricks/i18n-utils": "workspace:*",
|
||||
"@formbricks/js-core": "workspace:*",
|
||||
"@formbricks/logger": "workspace:*",
|
||||
|
||||
@@ -124,7 +124,7 @@ export const ZConnectorUpdateInput = z.object({
|
||||
export type TConnectorUpdateInput = z.infer<typeof ZConnectorUpdateInput>;
|
||||
|
||||
// Element types that cannot be mapped to Hub fields
|
||||
export const UNSUPPORTED_CONNECTOR_ELEMENT_TYPES = [
|
||||
export const UNSUPPORTED_CONNECTOR_ELEMENT_TYPES: readonly TSurveyElementTypeEnum[] = [
|
||||
TSurveyElementTypeEnum.ContactInfo,
|
||||
TSurveyElementTypeEnum.Address,
|
||||
TSurveyElementTypeEnum.Cal,
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -160,6 +160,9 @@ importers:
|
||||
'@formbricks/email':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/email
|
||||
'@formbricks/hub':
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0
|
||||
'@formbricks/i18n-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/i18n-utils
|
||||
@@ -2337,6 +2340,9 @@ packages:
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||
|
||||
'@formbricks/hub@0.3.0':
|
||||
resolution: {integrity: sha512-SfLQghsLILSiN/53mUHgkUbUcCar5l2bGC8DDSV9Y9NWdau+r+zFIYFEoSxhlMzlwhI+uvt/gkbVKShERBeoRQ==}
|
||||
|
||||
'@formkit/auto-animate@0.8.2':
|
||||
resolution: {integrity: sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==}
|
||||
|
||||
@@ -13885,6 +13891,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formbricks/hub@0.3.0': {}
|
||||
|
||||
'@formkit/auto-animate@0.8.2': {}
|
||||
|
||||
'@gar/promisify@1.1.3':
|
||||
|
||||
Reference in New Issue
Block a user