mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-05 00:48:03 -06:00
Merge branch 'fix/polish-formbricks-connector' into feat/csv-connectors
This commit is contained in:
@@ -79,6 +79,97 @@ export const FormbricksSurveySelector = ({
|
||||
const getSupportedElementCount = (survey: TUnifySurvey) =>
|
||||
survey.elements.filter((e) => !isUnsupportedType(e.type)).length;
|
||||
|
||||
const getElementButtonClassName = (unsupported: boolean, isSelected: boolean): string => {
|
||||
if (unsupported) return "cursor-not-allowed border-slate-100 bg-slate-50 opacity-50";
|
||||
if (isSelected) return "border-green-300 bg-green-50";
|
||||
return "border-slate-200 bg-white hover:border-slate-300";
|
||||
};
|
||||
|
||||
const getCheckboxClassName = (unsupported: boolean, isSelected: boolean): string => {
|
||||
if (unsupported) return "border border-slate-200 bg-slate-100";
|
||||
if (isSelected) return "bg-green-500 text-white";
|
||||
return "border border-slate-300 bg-white";
|
||||
};
|
||||
|
||||
const renderElementPanel = () => {
|
||||
if (!selectedSurvey) {
|
||||
return (
|
||||
<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_questions")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedSurvey.elements.length === 0) {
|
||||
return (
|
||||
<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_questions")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 overflow-y-auto pr-1">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{selectedSurvey.elements.map((element) => {
|
||||
const isSelected = selectedElementIds.includes(element.id);
|
||||
const unsupported = isUnsupportedType(element.type);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
key={element.id}
|
||||
type="button"
|
||||
disabled={unsupported}
|
||||
onClick={() => onElementToggle(element.id)}
|
||||
className={`flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-colors ${getElementButtonClassName(unsupported, isSelected)}`}>
|
||||
<div
|
||||
className={`flex h-5 w-5 items-center justify-center rounded ${getCheckboxClassName(unsupported, isSelected)}`}>
|
||||
{isSelected && !unsupported && <CheckIcon className="h-3 w-3" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">{getElementIcon(element.type)}</div>
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm ${unsupported ? "text-slate-400" : "text-slate-900"}`}>
|
||||
{element.headline}
|
||||
</p>
|
||||
<span className={`text-xs ${unsupported ? "text-slate-300" : "text-slate-500"}`}>
|
||||
{getTSurveyElementTypeEnumName(element.type, t) ?? element.type}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (unsupported) {
|
||||
return (
|
||||
<Tooltip key={element.id}>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>{t("environments.unify.question_type_not_supported")}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
})}
|
||||
</TooltipProvider>
|
||||
|
||||
{selectedElementIds.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<p className="text-xs text-blue-700">
|
||||
<Trans
|
||||
i18nKey={
|
||||
selectedElementIds.length === 1
|
||||
? "environments.unify.question_selected"
|
||||
: "environments.unify.questions_selected"
|
||||
}
|
||||
values={{ count: selectedElementIds.length }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid h-[50vh] grid-cols-2 gap-6">
|
||||
{/* Left: Survey List */}
|
||||
@@ -143,88 +234,7 @@ export const FormbricksSurveySelector = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!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_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_questions")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 overflow-y-auto pr-1">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{selectedSurvey.elements.map((element) => {
|
||||
const isSelected = selectedElementIds.includes(element.id);
|
||||
const unsupported = isUnsupportedType(element.type);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
key={element.id}
|
||||
type="button"
|
||||
disabled={unsupported}
|
||||
onClick={() => onElementToggle(element.id)}
|
||||
className={`flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-colors ${
|
||||
unsupported
|
||||
? "cursor-not-allowed border-slate-100 bg-slate-50 opacity-50"
|
||||
: isSelected
|
||||
? "border-green-300 bg-green-50"
|
||||
: "border-slate-200 bg-white hover:border-slate-300"
|
||||
}`}>
|
||||
<div
|
||||
className={`flex h-5 w-5 items-center justify-center rounded ${
|
||||
unsupported
|
||||
? "border border-slate-200 bg-slate-100"
|
||||
: isSelected
|
||||
? "bg-green-500 text-white"
|
||||
: "border border-slate-300 bg-white"
|
||||
}`}>
|
||||
{isSelected && !unsupported && <CheckIcon className="h-3 w-3" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">{getElementIcon(element.type)}</div>
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm ${unsupported ? "text-slate-400" : "text-slate-900"}`}>
|
||||
{element.headline}
|
||||
</p>
|
||||
<span className={`text-xs ${unsupported ? "text-slate-300" : "text-slate-500"}`}>
|
||||
{getTSurveyElementTypeEnumName(element.type, t) ?? element.type}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (unsupported) {
|
||||
return (
|
||||
<Tooltip key={element.id}>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>{t("environments.unify.question_type_not_supported")}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
})}
|
||||
</TooltipProvider>
|
||||
|
||||
{selectedElementIds.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<p className="text-xs text-blue-700">
|
||||
<Trans
|
||||
i18nKey={
|
||||
selectedElementIds.length === 1
|
||||
? "environments.unify.question_selected"
|
||||
: "environments.unify.questions_selected"
|
||||
}
|
||||
values={{ count: selectedElementIds.length }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderElementPanel()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,87 @@
|
||||
import "server-only";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TConnectorWithMappings } from "@formbricks/types/connector";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { createFeedbackRecordsBatch } from "@/modules/hub";
|
||||
import { getConnectorsBySurveyId, updateConnector } from "./service";
|
||||
import { transformResponseToFeedbackRecords } from "./transform";
|
||||
|
||||
const getErrorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
const logFailedRecords = (
|
||||
connectorId: string,
|
||||
results: Awaited<ReturnType<typeof createFeedbackRecordsBatch>>["results"]
|
||||
): void => {
|
||||
for (const [index, result] of results.entries()) {
|
||||
if (!result.error) continue;
|
||||
logger.error(
|
||||
{
|
||||
connectorId,
|
||||
feedbackRecordIndex: index,
|
||||
error: {
|
||||
status: result.error.status,
|
||||
message: result.error.message,
|
||||
detail: result.error.detail,
|
||||
},
|
||||
},
|
||||
"Failed to create FeedbackRecord in Hub"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const processConnector = async (
|
||||
connector: TConnectorWithMappings,
|
||||
response: TResponse,
|
||||
survey: TSurvey,
|
||||
environmentId: string
|
||||
): Promise<void> => {
|
||||
const feedbackRecords = transformResponseToFeedbackRecords(
|
||||
response,
|
||||
survey,
|
||||
connector.formbricksMappings,
|
||||
environmentId
|
||||
);
|
||||
|
||||
if (feedbackRecords.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { results } = await createFeedbackRecordsBatch(feedbackRecords);
|
||||
|
||||
const successes = results.filter((r) => r.data !== null).length;
|
||||
const failures = results.filter((r) => r.error !== null).length;
|
||||
|
||||
if (failures > 0) {
|
||||
logger.warn(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
successes,
|
||||
failures,
|
||||
},
|
||||
`Connector pipeline: ${failures}/${feedbackRecords.length} FeedbackRecords failed to send`
|
||||
);
|
||||
logFailedRecords(connector.id, results);
|
||||
} else {
|
||||
logger.info(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
feedbackRecordsCreated: successes,
|
||||
},
|
||||
`Connector pipeline: Successfully sent ${successes} FeedbackRecords to Hub`
|
||||
);
|
||||
}
|
||||
|
||||
if (successes > 0) {
|
||||
await updateConnector(connector.id, environmentId, { lastSyncAt: new Date() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle connector pipeline for a survey response
|
||||
*
|
||||
@@ -30,74 +106,14 @@ export const handleConnectorPipeline = async (
|
||||
|
||||
for (const connector of connectors) {
|
||||
try {
|
||||
const feedbackRecords = transformResponseToFeedbackRecords(
|
||||
response,
|
||||
survey,
|
||||
connector.formbricksMappings,
|
||||
environmentId
|
||||
);
|
||||
|
||||
if (feedbackRecords.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { results } = await createFeedbackRecordsBatch(feedbackRecords);
|
||||
|
||||
const successes = results.filter((r) => r.data !== null).length;
|
||||
const failures = results.filter((r) => r.error !== null).length;
|
||||
|
||||
if (failures > 0) {
|
||||
logger.warn(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
successes,
|
||||
failures,
|
||||
},
|
||||
`Connector pipeline: ${failures}/${feedbackRecords.length} FeedbackRecords failed to send`
|
||||
);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.error) {
|
||||
logger.error(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
feedbackRecordIndex: index,
|
||||
error: {
|
||||
status: result.error.status,
|
||||
message: result.error.message,
|
||||
detail: result.error.detail,
|
||||
},
|
||||
},
|
||||
"Failed to create FeedbackRecord in Hub"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (successes > 0) {
|
||||
await updateConnector(connector.id, environmentId, { lastSyncAt: new Date() });
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
feedbackRecordsCreated: successes,
|
||||
},
|
||||
`Connector pipeline: Successfully sent ${successes} FeedbackRecords to Hub`
|
||||
);
|
||||
|
||||
await updateConnector(connector.id, environmentId, { lastSyncAt: new Date() });
|
||||
}
|
||||
await processConnector(connector, response, survey, environmentId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
connectorId: connector.id,
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: getErrorMessage(error),
|
||||
},
|
||||
"Connector pipeline: Failed to process connector"
|
||||
);
|
||||
@@ -108,7 +124,7 @@ export const handleConnectorPipeline = async (
|
||||
{
|
||||
surveyId: survey.id,
|
||||
responseId: response.id,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: getErrorMessage(error),
|
||||
},
|
||||
"Connector pipeline: Failed to handle connectors"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user