Compare commits

..

69 Commits

Author SHA1 Message Date
pandeymangg
5c018a7346 feat: adds feedback records table 2026-03-05 15:04:01 +05:30
pandeymangg
7a06779181 chore: merge with epic 2026-03-05 14:30:29 +05:30
pandeymangg
d255264dce fixes sonarqube issues 2026-03-05 13:23:31 +05:30
pandeymangg
3360b1b6d1 chore: merge with epic 2026-03-05 11:24:38 +05:30
Johannes
15075335ed resolve build error 2026-03-04 20:40:21 -03:00
Johannes
9082c43e99 add translations 2026-03-04 20:29:48 -03:00
Johannes
8c92005026 refactor: update CSV import functionality and remove unused component
- Changed PostgreSQL volume path in docker-compose for better data management.
- Enhanced the ConnectorRowDropdown component to include an optional CSV import handler.
- Updated ConnectorsTable and related components to support CSV import functionality.
- Replaced the CsvImportSection component with a more streamlined CsvImportModal, improving the user experience for CSV uploads.
- Removed the obsolete CsvImportSection component to clean up the codebase.
2026-03-04 20:26:05 -03:00
Johannes
fc8d43f980 tweaked UX complete 2026-03-04 20:14:44 -03:00
Johannes
d3e26e29e1 update wording and icons 2026-03-03 20:13:57 -03:00
pandeymangg
ee1e71b572 adds tests for validateCSVFile 2026-03-03 12:27:59 +05:30
pandeymangg
d2fbbd2cd6 fixes 2026-03-02 17:48:56 +05:30
pandeymangg
578360422e feat: adds feedback records table 2026-03-02 15:49:23 +05:30
pandeymangg
eaea911a8b fixes some feedback 2026-02-27 11:47:39 +05:30
pandeymangg
624bba5d17 chore: merge with epic 2026-02-26 19:05:12 +05:30
pandeymangg
c9d162782f Merge branch 'fix/polish-formbricks-connector' into feat/csv-connectors 2026-02-26 17:23:38 +05:30
pandeymangg
14ca707fff Merge branch 'fix/polish-formbricks-connector' of https://github.com/formbricks/formbricks into fix/polish-formbricks-connector 2026-02-26 17:22:13 +05:30
pandeymangg
9ef80b7198 fixes 2026-02-26 17:21:06 +05:30
pandeymangg
1f71a7fddf fixes build 2026-02-26 16:55:50 +05:30
Harsh Bhat
bbe1140735 trigger cla recheck 2026-02-26 16:37:41 +05:30
pandeymangg
f1224650d4 Merge branch 'fix/polish-formbricks-connector' into feat/csv-connectors 2026-02-26 16:21:52 +05:30
pandeymangg
343ce4568a fixes tests 2026-02-26 16:18:22 +05:30
pandeymangg
23ab6858a5 fixes import 2026-02-26 16:14:19 +05:30
pandeymangg
a72cc48386 chore: merge with polish PR 2026-02-26 16:04:08 +05:30
pandeymangg
f7506d14b0 fixes import 2026-02-26 15:38:39 +05:30
pandeymangg
e1b2bda239 Merge branch 'epic/connectors' into fix/polish-formbricks-connector 2026-02-26 14:57:37 +05:30
pandeymangg
621cdada36 fix: build 2026-02-26 14:49:52 +05:30
pandeymangg
50310bb7f6 feedback 2026-02-26 13:39:29 +05:30
pandeymangg
c21b65e53f feedback 2026-02-26 13:35:26 +05:30
pandeymangg
48c7398173 feat: csv connector 2026-02-25 22:17:19 +05:30
pandeymangg
d7d23a90dd fixes sonarqube complexity 2026-02-25 13:00:35 +05:30
pandeymangg
6028623821 fixes feedback 2026-02-25 12:14:34 +05:30
pandeymangg
d76fc301d4 fix: adds HUB_API_URL to .env.exmaple 2026-02-25 10:32:05 +05:30
pandeymangg
314734ca86 chore: merge with epic 2026-02-25 10:22:52 +05:30
pandeymangg
02819998ca fix: formbricks connector changes 2026-02-24 22:22:23 +05:30
pandeymangg
7764011ddf adds tests 2026-02-24 14:23:42 +05:30
pandeymangg
0e5e03a77d merge 2026-02-24 13:01:52 +05:30
pandeymangg
746b02326a Merge branch 'epic/connectors' into feat/unify 2026-02-24 12:38:37 +05:30
pandeymangg
063af018fa fixes i18n issues 2026-02-24 12:35:08 +05:30
pandeymangg
1d74aadeec fixes 2026-02-24 12:11:37 +05:30
pandeymangg
4c3a240221 fixes feedback 2026-02-24 11:39:20 +05:30
pandeymangg
68e39afe61 fixes sonar issue 2026-02-23 16:58:07 +05:30
pandeymangg
a330026af8 fixes lint error 2026-02-23 16:54:58 +05:30
pandeymangg
964fcb3ab9 fixes feedback 2026-02-23 16:50:14 +05:30
pandeymangg
8e2f8302e4 csv parsing uses csv package 2026-02-23 16:02:40 +05:30
pandeymangg
cd280d7a77 fixes e2e tests 2026-02-23 13:45:53 +05:30
pandeymangg
071b0ded2a lint fix 2026-02-20 12:45:20 +05:30
pandeymangg
cc2303baac some more cleanup 2026-02-20 12:39:35 +05:30
pandeymangg
65e32ad0d4 more cleanup 2026-02-19 16:32:32 +05:30
pandeymangg
7284bc4c4c fixes 2026-02-19 13:54:53 +05:30
pandeymangg
e90b8228d0 fixes 2026-02-18 17:07:23 +05:30
pandeymangg
3233dec57f some fixes 2026-02-18 16:09:16 +05:30
pandeymangg
d6794382da removes unnecessary packages 2026-02-18 14:42:59 +05:30
pandeymangg
526ddf8d2e chore: remove chart.tsx 2026-02-18 14:18:10 +05:30
pandeymangg
c6600d4eca charts are back 2026-02-18 14:15:57 +05:30
pandeymangg
8ac73fe35c chore: merge with epic 2026-02-18 14:11:03 +05:30
pandeymangg
67a2e6074e cleanup 2026-02-18 13:23:31 +05:30
pandeymangg
1043a6f034 chore: merge with main 2026-02-03 22:14:41 +05:30
pandeymangg
f7a28f8cb3 feat: connectors 2026-02-03 22:12:37 +05:30
Dhruwang
ac8cf7c6b4 dashboard changes 2026-02-03 18:48:34 +05:30
Dhruwang
39e7de7e91 Merge branch 'dashboards' of https://github.com/formbricks/formbricks into feat/unify 2026-02-03 18:44:23 +05:30
Dhruwang
36955ddbb8 implement ui feedback 2026-02-02 10:15:22 +05:30
Dhruwang
ef56e97c95 clean ups 2026-01-29 10:46:32 +04:00
Dhruwang
ea3b4b9413 rfactors 2026-01-28 17:34:03 +04:00
Dhruwang
131a04b77c rfactors 2026-01-28 17:33:33 +04:00
TheodorTomas
ca7e2c64de feat: init commit for dashboard 2026-01-28 14:45:25 +04:00
Dhruwang
e4bd9a839a dashboard apis 2026-01-28 12:01:44 +04:00
Johannes
590c85d1ca add sources UI 2026-01-28 08:54:52 +04:00
Harsh Bhat
39c99baaac feat: Add mock data and UI for taxanomy & knowledge 2026-01-27 16:12:19 +04:00
Harsh Bhat
238b2adf3f feat: Unify POC hackathon 2026-01-27 14:58:20 +04:00
30 changed files with 823 additions and 248 deletions

View File

@@ -19,7 +19,14 @@ export const UnifyConfigNavigation = ({
const activeId = activeIdProp ?? "sources";
const navigation = [{ id: "sources", label: t("environments.unify.sources"), href: `${baseHref}/sources` }];
const navigation = [
{ id: "sources", label: t("environments.unify.sources"), href: `${baseHref}/sources` },
{
id: "feedback-records",
label: t("environments.unify.feedback_records"),
href: `${baseHref}/feedback-records`,
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
};

View File

@@ -0,0 +1,36 @@
"use client";
import { useTranslation } from "react-i18next";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UnifyConfigNavigation } from "../components/UnifyConfigNavigation";
import { FeedbackRecordsTable } from "./feedback-records-table";
interface FeedbackRecordsPageClientProps {
environmentId: string;
initialRecords: FeedbackRecordData[];
initialTotal: number;
}
export function FeedbackRecordsPageClient({
environmentId,
initialRecords,
initialTotal,
}: FeedbackRecordsPageClientProps) {
const { t } = useTranslation();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("environments.unify.unify_feedback")}>
<UnifyConfigNavigation environmentId={environmentId} activeId="feedback-records" />
</PageHeader>
<FeedbackRecordsTable
environmentId={environmentId}
initialRecords={initialRecords}
initialTotal={initialTotal}
/>
</PageContentWrapper>
);
}

View File

@@ -0,0 +1,231 @@
"use client";
import {
CalendarIcon,
HashIcon,
MessageSquareTextIcon,
RefreshCwIcon,
ToggleLeftIcon,
TypeIcon,
} from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { listFeedbackRecordsAction } from "@/lib/connector/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
const RECORDS_PER_PAGE = 50;
const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
text: <TypeIcon className="h-3.5 w-3.5" />,
categorical: <HashIcon className="h-3.5 w-3.5" />,
nps: <HashIcon className="h-3.5 w-3.5" />,
csat: <HashIcon className="h-3.5 w-3.5" />,
ces: <HashIcon className="h-3.5 w-3.5" />,
rating: <HashIcon className="h-3.5 w-3.5" />,
number: <HashIcon className="h-3.5 w-3.5" />,
boolean: <ToggleLeftIcon className="h-3.5 w-3.5" />,
date: <CalendarIcon className="h-3.5 w-3.5" />,
};
function formatValue(record: FeedbackRecordData): string {
if (record.value_text != null) return record.value_text;
if (record.value_number != null) return String(record.value_number);
if (record.value_boolean != null) return record.value_boolean ? "Yes" : "No";
if (record.value_date != null) return new Date(record.value_date).toLocaleDateString();
return "—";
}
function formatDate(isoString: string, locale: string): string {
return new Date(isoString).toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str;
return str.slice(0, maxLen) + "…";
}
interface FeedbackRecordsTableProps {
environmentId: string;
initialRecords: FeedbackRecordData[];
initialTotal: number;
}
export function FeedbackRecordsTable({
environmentId,
initialRecords,
initialTotal,
}: FeedbackRecordsTableProps) {
const { t, i18n } = useTranslation();
const [records, setRecords] = useState<FeedbackRecordData[]>(initialRecords);
const [total, setTotal] = useState(initialTotal);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchRecords = useCallback(
async (offset: number, append: boolean) => {
const setLoading = offset === 0 ? setIsRefreshing : setIsLoadingMore;
setLoading(true);
setError(null);
const result = await listFeedbackRecordsAction({
environmentId,
limit: RECORDS_PER_PAGE,
offset,
});
if (!result?.data) {
setError(getFormattedErrorMessage(result) ?? t("environments.unify.failed_to_load_feedback_records"));
setLoading(false);
return;
}
const response = result.data;
setRecords((prev) => (append ? [...prev, ...response.data] : response.data));
setTotal(response.total);
setLoading(false);
},
[environmentId, t]
);
const handleLoadMore = () => {
fetchRecords(records.length, true);
};
const handleRefresh = () => {
fetchRecords(0, false);
};
const hasMore = records.length < total;
if (error) {
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex h-48 flex-col items-center justify-center gap-3 px-4 text-center">
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">{error}</p>
<Button variant="secondary" size="sm" onClick={handleRefresh}>
{t("common.retry")}
</Button>
</div>
</div>
);
}
if (records.length === 0 && !isRefreshing) {
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex h-48 flex-col items-center justify-center gap-2 px-4 text-center">
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">{t("environments.unify.no_feedback_records")}</p>
</div>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm text-slate-500">
{t("environments.unify.showing_count", { count: records.length, total })}
</p>
<Button
variant="secondary"
size="sm"
onClick={handleRefresh}
loading={isRefreshing}
className="gap-1.5">
<RefreshCwIcon className="h-3.5 w-3.5" />
</Button>
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px]">
<thead>
<tr className="border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.collected_at")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.source_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.source_name")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.field_label")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.field_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.value")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("environments.unify.user_identifier")}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{records.map((record) => (
<FeedbackRecordRow key={record.id} record={record} locale={i18n.language} />
))}
</tbody>
</table>
</div>
</div>
{hasMore && (
<div className="flex justify-center">
<Button variant="secondary" size="sm" onClick={handleLoadMore} loading={isLoadingMore}>
{t("environments.unify.load_more")}
</Button>
</div>
)}
</div>
);
}
function FeedbackRecordRow({ record, locale }: { record: FeedbackRecordData; locale: string }) {
const value = formatValue(record);
const isLongValue = value.length > 60;
return (
<tr className="text-sm text-slate-700 transition-colors hover:bg-slate-50">
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
{formatDate(record.collected_at, locale)}
</td>
<td className="whitespace-nowrap px-4 py-3">
<Badge text={record.source_type} type="gray" size="tiny" />
</td>
<td className="max-w-[150px] truncate px-4 py-3" title={record.source_name ?? undefined}>
{record.source_name ?? "—"}
</td>
<td className="max-w-[200px] truncate px-4 py-3" title={record.field_label ?? undefined}>
{record.field_label ?? record.field_id}
</td>
<td className="whitespace-nowrap px-4 py-3">
<span className="inline-flex items-center gap-1 text-slate-600">
{FIELD_TYPE_ICONS[record.field_type] ?? <HashIcon className="h-3.5 w-3.5" />}
{record.field_type}
</span>
</td>
<td className="max-w-[250px] px-4 py-3">
{isLongValue ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default truncate">{truncate(value, 60)}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm whitespace-pre-wrap">
{value}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span>{value}</span>
)}
</td>
<td className="max-w-[120px] truncate px-4 py-3 text-slate-500" title={record.user_identifier}>
{record.user_identifier ?? "—"}
</td>
</tr>
);
}

View File

@@ -0,0 +1,29 @@
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { listFeedbackRecords } from "@/modules/hub/service";
import { FeedbackRecordsPageClient } from "./feedback-records-page-client";
const INITIAL_PAGE_SIZE = 50;
export default async function UnifyFeedbackRecordsPage(props: {
params: Promise<{ environmentId: string }>;
}) {
const params = await props.params;
await getEnvironmentAuth(params.environmentId);
const result = await listFeedbackRecords({
tenant_id: params.environmentId,
limit: INITIAL_PAGE_SIZE,
offset: 0,
});
const initialData = result.data ?? { data: [], total: 0, limit: INITIAL_PAGE_SIZE, offset: 0 };
return (
<FeedbackRecordsPageClient
environmentId={params.environmentId}
initialRecords={initialData.data}
initialTotal={initialData.total}
/>
);
}

View File

@@ -161,7 +161,7 @@ export function ConnectorsSection({
environmentId={environmentId}
/>
}>
<UnifyConfigNavigation environmentId={environmentId} />
<UnifyConfigNavigation environmentId={environmentId} activeId="sources" />
</PageHeader>
<div className="space-y-6">

View File

@@ -20,7 +20,7 @@ interface DraggableSourceFieldProps {
isMapped: boolean;
}
export const DraggableSourceField = ({ field, isMapped }: DraggableSourceFieldProps) => {
export function DraggableSourceField({ field, isMapped }: DraggableSourceFieldProps) {
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id: field.id,
data: field,
@@ -55,192 +55,8 @@ export const DraggableSourceField = ({ field, isMapped }: DraggableSourceFieldPr
)}
</div>
);
};
const getMappingStateClass = (isActive: boolean, hasMapping: unknown): string => {
if (isActive) return "border-brand-dark bg-slate-100";
if (hasMapping) return "border-green-300 bg-green-50";
return "border-dashed border-slate-300 bg-slate-50";
};
interface RemoveMappingButtonProps {
onClick: () => void;
variant: "green" | "blue";
}
const RemoveMappingButton = ({ onClick, variant }: RemoveMappingButtonProps) => {
const colorClass = variant === "green" ? "hover:bg-green-100" : "hover:bg-blue-100";
const iconClass = variant === "green" ? "text-green-600" : "text-blue-600";
return (
<button type="button" onClick={onClick} className={`ml-1 rounded p-0.5 ${colorClass}`}>
<XIcon className={`h-3 w-3 ${iconClass}`} />
</button>
);
};
interface EnumTargetFieldContentProps {
field: TTargetField;
mappedSourceField: TSourceField | null;
mapping: TFieldMapping | null;
onRemoveMapping: () => void;
onStaticValueChange: (value: string) => void;
t: (key: string) => string;
}
const EnumTargetFieldContent = ({
field,
mappedSourceField,
mapping,
onRemoveMapping,
onStaticValueChange,
t,
}: EnumTargetFieldContentProps) => {
return (
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{field.name}</span>
{field.required && <span className="text-xs text-red-500">*</span>}
<span className="text-xs text-slate-400">{t("environments.unify.enum")}</span>
</div>
{mappedSourceField && !mapping?.staticValue ? (
<div className="flex items-center gap-1">
<span className="text-xs text-green-700">&larr; {mappedSourceField.name}</span>
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
</div>
) : (
<Select value={mapping?.staticValue || ""} onValueChange={onStaticValueChange}>
<SelectTrigger className="h-8 w-full bg-white">
<SelectValue placeholder={t("environments.unify.select_a_value")} />
</SelectTrigger>
<SelectContent>
{field.enumValues?.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
);
};
interface StringTargetFieldContentProps {
field: TTargetField;
mappedSourceField: TSourceField | null;
mapping: TFieldMapping | null;
hasMapping: unknown;
onRemoveMapping: () => void;
onStaticValueChange: (value: string) => void;
t: (key: string) => string;
}
const StringTargetFieldContent = ({
field,
mappedSourceField,
mapping,
hasMapping,
onRemoveMapping,
onStaticValueChange,
t,
}: StringTargetFieldContentProps) => {
const [isEditingStatic, setIsEditingStatic] = useState(false);
const [customValue, setCustomValue] = useState("");
return (
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{field.name}</span>
{field.required && <span className="text-xs text-red-500">*</span>}
</div>
{mappedSourceField && !mapping?.staticValue && (
<div className="flex items-center gap-1">
<span className="text-xs text-green-700"> {mappedSourceField.name}</span>
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
</div>
)}
{mapping?.staticValue && !mappedSourceField && (
<div className="flex items-center gap-1">
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
= &ldquo;{mapping.staticValue}&rdquo;
</span>
<RemoveMappingButton onClick={onRemoveMapping} variant="blue" />
</div>
)}
{isEditingStatic && !hasMapping && (
<div className="flex items-center gap-1">
<Input
type="text"
value={customValue}
onChange={(e) => setCustomValue(e.target.value)}
placeholder={
field.exampleStaticValues
? `e.g., ${field.exampleStaticValues[0]}`
: t("environments.unify.enter_value")
}
className="h-7 text-xs"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && customValue.trim()) {
onStaticValueChange(customValue.trim());
setCustomValue("");
setIsEditingStatic(false);
}
if (e.key === "Escape") {
setCustomValue("");
setIsEditingStatic(false);
}
}}
/>
<button
type="button"
onClick={() => {
if (customValue.trim()) {
onStaticValueChange(customValue.trim());
setCustomValue("");
}
setIsEditingStatic(false);
}}
className="rounded p-1 text-slate-500 hover:bg-slate-200">
<ChevronDownIcon className="h-3 w-3" />
</button>
</div>
)}
{!hasMapping && !isEditingStatic && (
<div className="flex flex-wrap items-center gap-1">
<span className="text-xs text-slate-400">{t("environments.unify.drop_field_or")}</span>
<button
type="button"
onClick={() => setIsEditingStatic(true)}
className="flex items-center gap-1 rounded px-1 py-0.5 text-xs text-slate-500 hover:bg-slate-200">
<PencilIcon className="h-3 w-3" />
{t("environments.unify.set_value")}
</button>
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (
<>
<span className="text-xs text-slate-300">|</span>
{field.exampleStaticValues.slice(0, 3).map((val) => (
<button
key={val}
type="button"
onClick={() => onStaticValueChange(val)}
className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 hover:bg-slate-200">
{val}
</button>
))}
</>
)}
</div>
)}
</div>
);
};
interface DroppableTargetFieldProps {
field: TTargetField;
mappedSourceField: TSourceField | null;
@@ -264,40 +80,177 @@ export const DroppableTargetField = ({
data: field,
});
const [isEditingStatic, setIsEditingStatic] = useState(false);
const [customValue, setCustomValue] = useState("");
const isActive = isOver || isOverCurrent;
const hasMapping = mappedSourceField || mapping?.staticValue;
const containerClass = cn(
"flex items-center gap-2 rounded-md border p-2 text-sm transition-colors",
getMappingStateClass(!!isActive, hasMapping)
);
// Handle enum field type - support both column mapping and static dropdown
if (field.type === "enum" && field.enumValues) {
return (
<div ref={setNodeRef} className={containerClass}>
<EnumTargetFieldContent
field={field}
mappedSourceField={mappedSourceField}
mapping={mapping}
onRemoveMapping={onRemoveMapping}
onStaticValueChange={onStaticValueChange}
t={t}
/>
<div
ref={setNodeRef}
className={cn(
`flex items-center gap-2 rounded-md border p-2 text-sm transition-colors ${
isActive
? "border-brand-dark bg-slate-100"
: hasMapping
? "border-green-300 bg-green-50"
: "border-dashed border-slate-300 bg-slate-50"
}`
)}>
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{field.name}</span>
{field.required && <span className="text-xs text-red-500">*</span>}
<span className="text-xs text-slate-400">{t("environments.unify.enum")}</span>
</div>
{mappedSourceField && !mapping?.staticValue ? (
<div className="flex items-center gap-1">
<span className="text-xs text-green-700">&larr; {mappedSourceField.name}</span>
<button
type="button"
onClick={onRemoveMapping}
className="ml-1 rounded p-0.5 hover:bg-green-100">
<XIcon className="h-3 w-3 text-green-600" />
</button>
</div>
) : (
<Select value={mapping?.staticValue || ""} onValueChange={onStaticValueChange}>
<SelectTrigger className="h-8 w-full bg-white">
<SelectValue placeholder={t("environments.unify.select_a_value")} />
</SelectTrigger>
<SelectContent>
{field.enumValues.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</div>
);
}
// Handle string fields - allow drag & drop OR static value
if (field.type === "string") {
return (
<div ref={setNodeRef} className={containerClass}>
<StringTargetFieldContent
field={field}
mappedSourceField={mappedSourceField}
mapping={mapping}
hasMapping={hasMapping}
onRemoveMapping={onRemoveMapping}
onStaticValueChange={onStaticValueChange}
t={t}
/>
<div
ref={setNodeRef}
className={cn(
`flex items-center gap-2 rounded-md border p-2 text-sm transition-colors`,
isActive && "border-brand-dark bg-slate-100",
!isActive && hasMapping
? "border-green-300 bg-green-50"
: "border-dashed border-slate-300 bg-slate-50"
)}>
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{field.name}</span>
{field.required && <span className="text-xs text-red-500">*</span>}
</div>
{/* Show mapped source field */}
{mappedSourceField && !mapping?.staticValue && (
<div className="flex items-center gap-1">
<span className="text-xs text-green-700"> {mappedSourceField.name}</span>
<button
type="button"
onClick={onRemoveMapping}
className="ml-1 rounded p-0.5 hover:bg-green-100">
<XIcon className="h-3 w-3 text-green-600" />
</button>
</div>
)}
{/* Show static value */}
{mapping?.staticValue && !mappedSourceField && (
<div className="flex items-center gap-1">
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
= &ldquo;{mapping.staticValue}&rdquo;
</span>
<button
type="button"
onClick={onRemoveMapping}
className="ml-1 rounded p-0.5 hover:bg-blue-100">
<XIcon className="h-3 w-3 text-blue-600" />
</button>
</div>
)}
{/* Show input for entering static value when editing */}
{isEditingStatic && !hasMapping && (
<div className="flex items-center gap-1">
<Input
type="text"
value={customValue}
onChange={(e) => setCustomValue(e.target.value)}
placeholder={
field.exampleStaticValues
? `e.g., ${field.exampleStaticValues[0]}`
: t("environments.unify.enter_value")
}
className="h-7 text-xs"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && customValue.trim()) {
onStaticValueChange(customValue.trim());
setCustomValue("");
setIsEditingStatic(false);
}
if (e.key === "Escape") {
setCustomValue("");
setIsEditingStatic(false);
}
}}
/>
<button
type="button"
onClick={() => {
if (customValue.trim()) {
onStaticValueChange(customValue.trim());
setCustomValue("");
}
setIsEditingStatic(false);
}}
className="rounded p-1 text-slate-500 hover:bg-slate-200">
<ChevronDownIcon className="h-3 w-3" />
</button>
</div>
)}
{/* Show example values as quick select OR drop zone */}
{!hasMapping && !isEditingStatic && (
<div className="flex flex-wrap items-center gap-1">
<span className="text-xs text-slate-400">{t("environments.unify.drop_field_or")}</span>
<button
type="button"
onClick={() => setIsEditingStatic(true)}
className="flex items-center gap-1 rounded px-1 py-0.5 text-xs text-slate-500 hover:bg-slate-200">
<PencilIcon className="h-3 w-3" />
{t("environments.unify.set_value")}
</button>
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (
<>
<span className="text-xs text-slate-300">|</span>
{field.exampleStaticValues.slice(0, 3).map((val) => (
<button
key={val}
type="button"
onClick={() => onStaticValueChange(val)}
className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 hover:bg-slate-200">
{val}
</button>
))}
</>
)}
</div>
)}
</div>
</div>
);
}
@@ -308,8 +261,19 @@ export const DroppableTargetField = ({
return value;
};
// Default behavior for other field types (timestamp, float64, boolean, jsonb, etc.)
const hasDefaultMapping = mappedSourceField || mapping?.staticValue;
return (
<div ref={setNodeRef} className={containerClass}>
<div
ref={setNodeRef}
className={cn(
"flex items-center gap-2 rounded-md border p-2 text-sm transition-colors",
isActive && "border-brand-dark bg-slate-100",
!isActive && hasDefaultMapping
? "border-green-300 bg-green-50"
: "border-dashed border-slate-300 bg-slate-50"
)}>
<div className="flex flex-1 flex-col">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{field.name}</span>
@@ -317,23 +281,30 @@ export const DroppableTargetField = ({
<span className="text-xs text-slate-400">({field.type})</span>
</div>
{/* Show mapped source field */}
{mappedSourceField && !mapping?.staticValue && (
<div className="mt-1 flex items-center gap-1">
<span className="text-xs text-green-700"> {mappedSourceField.name}</span>
<RemoveMappingButton onClick={onRemoveMapping} variant="green" />
<button type="button" onClick={onRemoveMapping} className="ml-1 rounded p-0.5 hover:bg-green-100">
<XIcon className="h-3 w-3 text-green-600" />
</button>
</div>
)}
{/* Show static value */}
{mapping?.staticValue && !mappedSourceField && (
<div className="mt-1 flex items-center gap-1">
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
= {getStaticValueLabel(mapping.staticValue)}
</span>
<RemoveMappingButton onClick={onRemoveMapping} variant="blue" />
<button type="button" onClick={onRemoveMapping} className="ml-1 rounded p-0.5 hover:bg-blue-100">
<XIcon className="h-3 w-3 text-blue-600" />
</button>
</div>
)}
{!hasMapping && (
{/* Show drop zone with preset options */}
{!hasDefaultMapping && (
<div className="mt-1 flex flex-wrap items-center gap-1">
<span className="text-xs text-slate-400">{t("environments.unify.drop_a_field_here")}</span>
{field.exampleStaticValues && field.exampleStaticValues.length > 0 && (

View File

@@ -52,7 +52,7 @@ export const validateEnumMappings = (
if (!mapping.sourceFieldId || mapping.staticValue) continue;
const targetField = FEEDBACK_RECORD_FIELDS.find((f) => f.id === mapping.targetFieldId);
if (targetField?.type !== "enum" || !targetField?.enumValues) continue;
if (!targetField || targetField.type !== "enum" || !targetField.enumValues) continue;
const allowedValues = new Set(targetField.enumValues);
const invalidEntries: { row: number; value: string }[] = [];

View File

@@ -334,6 +334,7 @@ checksums:
common/response_id: 73375099cc976dc7203b8e27f5f709e0
common/responses: 14bb6c69f906d7bbd1359f7ef1bb3c28
common/restart: bab6232e89f24e3129f8e48268739d5b
common/retry: 6e44d18639560596569a1278f9c83676
common/role: 53743bbb6ca938f5b893552e839d067f
common/saas: f01686245bcfb35a3590ab56db677bdb
common/sales: 38758eb50094cd8190a71fe67be4d647
@@ -1939,6 +1940,7 @@ checksums:
environments/unify/change_file: c5163ac18bf443370228a8ecbb0b07da
environments/unify/click_load_sample_csv: 0ee0bf93f10f02863fc658b359706316
environments/unify/click_to_upload: 74a7e7d79a88b6bbfd9f22084bffdb9b
environments/unify/collected_at: b41902ddb4586ba4a4611d726b5014aa
environments/unify/configure_import: 71d550661f7e9fe322b60e7e870aa2fd
environments/unify/configure_mapping: c794411c50bc511f8fc332def0e4e2f9
environments/unify/connection: 421e709602c92ffbe04a266f6a092089
@@ -1969,8 +1971,12 @@ checksums:
environments/unify/enter_name_for_source: de6d02a0a8ccc99204ad831ca6dcdbd3
environments/unify/enter_value: 4f068bb59617975c1e546218373122cd
environments/unify/enum: 96fc644f35edd6b1c09d1d503f078acc
environments/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8
environments/unify/feedback_date: ddba5d3270d4a6394d29721025a04400
environments/unify/feedback_record_fields: 88c0f13afeb88fe751f85e79b0f73064
environments/unify/feedback_records: e24cf48bb6985910f4ffe5e00512d388
environments/unify/field_label: 6384505ca0e40010c666b712511132a6
environments/unify/field_type: 2581066dc304c853a4a817c20996fa08
environments/unify/formbricks_surveys: eba2fce04ee68f02626e5509adf7d66a
environments/unify/historical_import_complete: f46f98bf4db63bf2993bfb234dc95f62
environments/unify/import_csv_data: f05e1d1ed88d528256efe5702df46646
@@ -1980,8 +1986,10 @@ checksums:
environments/unify/importing_historical_data: f5be578704ec26dc4ec573309e9fff20
environments/unify/invalid_enum_values: e6ca8740dab72f64e8dc5780b5cffcc6
environments/unify/invalid_values_found: 5011dc9c0294a222033f9910ea919b8a
environments/unify/load_more: 365c2d8dfc53ac7e9188acd5274e2837
environments/unify/load_sample_csv: ad21fa63f4a3df96a5939c753be21f4e
environments/unify/n_supported_questions: d75413d386441b5eb137a1ea191e4bd9
environments/unify/no_feedback_records: 16a905c40f6d47a5e8f93b3d8c6f6693
environments/unify/no_source_fields_loaded: a597b1d16262cbe897001046eb3ff640
environments/unify/no_sources_connected: 0e8a5612530bfc82091091f40f95012f
environments/unify/no_surveys_found: 649a2f29b4c34525778d9177605fb326
@@ -2003,12 +2011,14 @@ checksums:
environments/unify/select_survey_questions_description: 3386ed56085eabebefa3cc453269fc5b
environments/unify/set_value: b8a86f8da957ebd599ece4b1b1936a78
environments/unify/setup_connection: cce7d9c488d737d04e70bed929a46f8a
environments/unify/showing_count: 20675071b78443b250ab13b11138f30d
environments/unify/showing_rows: 83d3440314d1e6f2721e034369a3a131
environments/unify/source: 45309626f464f4bda161ee783a4c8c80
environments/unify/source_connect_csv_description: 2f9d1dd31668ac52578f16323157b746
environments/unify/source_connect_formbricks_description: 77bda4e1d485d76770ba2221f1faf9ff
environments/unify/source_fields: 1bae074990e64cbfd820a0b6462397be
environments/unify/source_name: 157675beca12efcd8ec512c5256b1a61
environments/unify/source_type: d1ff69af76c687eb189db72030717570
environments/unify/source_type_cannot_be_changed: bb5232c6e92df7f88731310fabbb1eb1
environments/unify/sources: ecbbe6e49baa335c5afd7b04b609d006
environments/unify/status_active: 3de9afebcb9d4ce8ac42e14995f79ffd
@@ -2024,6 +2034,8 @@ checksums:
environments/unify/updated_at: 8fdb85248e591254973403755dcc3724
environments/unify/upload_csv_data_description: 7fab46222ab05a4424db90a7cc96cdf5
environments/unify/upload_csv_file: b77797b68cb46a614b3adaa4db24d4c2
environments/unify/user_identifier: 61073457a5c3901084b557d065f876be
environments/unify/value: 34b0eaa85808b15cbc4be94c64d0146b
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

View File

@@ -25,6 +25,8 @@ import {
getProjectIdFromConnectorId,
getProjectIdFromEnvironmentId,
} from "@/lib/utils/helper";
import { listFeedbackRecords } from "@/modules/hub/service";
import type { FeedbackRecordListParams, FeedbackRecordListResponse } from "@/modules/hub/types";
import { importCsvData } from "./csv-import";
import { importHistoricalResponses } from "./import";
import {
@@ -462,3 +464,59 @@ export const importCsvDataAction = authenticatedActionClient
return result;
}
);
const ZListFeedbackRecordsAction = z.object({
environmentId: ZId,
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
sourceType: z.string().optional(),
fieldType: z.string().optional(),
since: z.string().optional(),
until: z.string().optional(),
});
export const listFeedbackRecordsAction = authenticatedActionClient
.schema(ZListFeedbackRecordsAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZListFeedbackRecordsAction>;
}): Promise<FeedbackRecordListResponse> => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
const params: FeedbackRecordListParams = {
tenant_id: parsedInput.environmentId,
limit: parsedInput.limit ?? 50,
offset: parsedInput.offset ?? 0,
};
if (parsedInput.sourceType) params.source_type = parsedInput.sourceType;
if (parsedInput.fieldType) params.field_type = parsedInput.fieldType;
if (parsedInput.since) params.since = parsedInput.since;
if (parsedInput.until) params.until = parsedInput.until;
const result = await listFeedbackRecords(params);
if (result.error) {
logger.warn({ error: result.error }, "Failed to list feedback records from Hub");
throw new Error(result.error.message);
}
return result.data!;
}
);

View File

@@ -19,7 +19,11 @@ export const importCsvData = async (
throw new InvalidInputError("Connector has no field mappings configured");
}
const { records, skipped } = transformCsvRowsToFeedbackRecords(csvRows, connector.fieldMappings);
const { records, skipped } = transformCsvRowsToFeedbackRecords(
csvRows,
connector.fieldMappings,
connector.environmentId
);
let successes = 0;
let failures = 0;

View File

@@ -55,7 +55,8 @@ const resolveValue = (
*/
export const transformCsvRowToFeedbackRecord = (
row: Record<string, string>,
mappings: TConnectorFieldMapping[]
mappings: TConnectorFieldMapping[],
tenantId?: string
): FeedbackRecordCreateParams | null => {
const record: Record<string, string | number | boolean | Record<string, unknown> | undefined> = {};
@@ -78,6 +79,10 @@ export const transformCsvRowToFeedbackRecord = (
return null;
}
if (tenantId && !record.tenant_id) {
record.tenant_id = tenantId;
}
return record as unknown as FeedbackRecordCreateParams;
};
@@ -87,13 +92,14 @@ export const transformCsvRowToFeedbackRecord = (
*/
export const transformCsvRowsToFeedbackRecords = (
rows: Record<string, string>[],
mappings: TConnectorFieldMapping[]
mappings: TConnectorFieldMapping[],
tenantId?: string
): { records: FeedbackRecordCreateParams[]; skipped: number } => {
const records: FeedbackRecordCreateParams[] = [];
let skipped = 0;
for (const row of rows) {
const record = transformCsvRowToFeedbackRecord(row, mappings);
const record = transformCsvRowToFeedbackRecord(row, mappings, tenantId);
if (record) {
records.push(record);
} else {

View File

@@ -13,14 +13,15 @@ export type TImportResult = { successes: number; failures: number; skipped: numb
const processBatch = async (
responses: Awaited<ReturnType<typeof getResponses>>,
survey: TSurvey,
mappings: TConnectorFormbricksMapping[]
mappings: TConnectorFormbricksMapping[],
tenantId: string
): Promise<TImportResult> => {
let successes = 0;
let failures = 0;
const expectedRecords = responses.length * mappings.length;
const allRecords = responses.flatMap((response) =>
transformResponseToFeedbackRecords(response, survey, mappings)
transformResponseToFeedbackRecords(response, survey, mappings, tenantId)
);
if (allRecords.length > 0) {
@@ -49,7 +50,12 @@ export const importHistoricalResponses = async (
const responses = await getResponses(survey.id, IMPORT_BATCH_SIZE, offset);
if (responses.length === 0) break;
const batch = await processBatch(responses, survey, connector.formbricksMappings);
const batch = await processBatch(
responses,
survey,
connector.formbricksMappings,
connector.environmentId
);
successes += batch.successes;
failures += batch.failures;
skipped += batch.skipped;

View File

@@ -361,6 +361,7 @@
"response_id": "Antwort-ID",
"responses": "Antworten",
"restart": "Neustart",
"retry": "Erneut versuchen",
"role": "Rolle",
"saas": "SaaS",
"sales": "Vertrieb",
@@ -2047,6 +2048,7 @@
"change_file": "Datei ändern",
"click_load_sample_csv": "Klicke auf 'Beispiel-CSV laden', um Spalten zu sehen",
"click_to_upload": "Klicke zum Hochladen",
"collected_at": "Erfasst am",
"configure_import": "Import konfigurieren",
"configure_mapping": "Zuordnung konfigurieren",
"connection": "Verbindung",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Gib einen Namen für diese Quelle ein",
"enter_value": "Wert eingeben...",
"enum": "Enum",
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
"feedback_date": "Aktuelles Datum",
"feedback_record_fields": "Feedback-Datensatzfelder",
"feedback_record_fields": "Feedback-Eintragsfelder",
"feedback_records": "Feedback-Einträge",
"field_label": "Feldbezeichnung",
"field_type": "Feldtyp",
"formbricks_surveys": "Formbricks Umfragen",
"historical_import_complete": "Import abgeschlossen: {successes} erfolgreich, {failures} fehlgeschlagen, {skipped} übersprungen (keine Daten)",
"import_csv_data": "Feedback importieren",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "Historische 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_more": "Mehr laden",
"load_sample_csv": "Beispiel-CSV laden",
"n_supported_questions": "{count} unterstützte Fragen",
"no_feedback_records": "Noch keine Feedback-Einträge vorhanden. Einträge werden hier angezeigt, sobald deine Konnektoren Daten senden.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Wähle aus, welche Umfragefragen FeedbackRecords erstellen sollen.",
"set_value": "Wert festlegen",
"setup_connection": "Verbindung einrichten",
"showing_count": "Zeige {count} von {total} Einträgen",
"showing_rows": "Zeige 3 von {count} Zeilen",
"source": "Quelle",
"source_connect_csv_description": "Feedback aus CSV-Dateien importieren",
"source_connect_formbricks_description": "Feedback aus deinen Formbricks-Umfragen verbinden",
"source_fields": "Quellenfelder",
"source_name": "Quellenname",
"source_type": "Quellentyp",
"source_type_cannot_be_changed": "Quellentyp kann nicht geändert werden",
"sources": "Quellen",
"status_active": "Im Gange",
@@ -2131,7 +2141,9 @@
"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 zu importieren.",
"upload_csv_file": "CSV-Datei hochladen"
"upload_csv_file": "CSV-Datei hochladen",
"user_identifier": "Benutzer",
"value": "Wert"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "Response ID",
"responses": "Responses",
"restart": "Restart",
"retry": "Retry",
"role": "Role",
"saas": "SaaS",
"sales": "Sales",
@@ -2047,6 +2048,7 @@
"change_file": "Change file",
"click_load_sample_csv": "Click 'Load sample CSV' to see columns",
"click_to_upload": "Click to upload",
"collected_at": "Collected At",
"configure_import": "Configure import",
"configure_mapping": "Configure Mapping",
"connection": "Connection",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Enter a name for this source",
"enter_value": "Enter value...",
"enum": "enum",
"failed_to_load_feedback_records": "Failed to load feedback records",
"feedback_date": "Current date",
"feedback_record_fields": "Feedback Record Fields",
"feedback_records": "Feedback Records",
"field_label": "Field Label",
"field_type": "Field Type",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
"import_csv_data": "Import feedback",
@@ -2088,8 +2094,10 @@
"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_more": "Load more",
"load_sample_csv": "Load sample CSV",
"n_supported_questions": "{count} supported questions",
"no_feedback_records": "No feedback records yet. Records will appear here once your connectors start sending data.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Choose which survey questions should create FeedbackRecords.",
"set_value": "set value",
"setup_connection": "Setup connection",
"showing_count": "Showing {count} of {total} records",
"showing_rows": "Showing 3 of {count} rows",
"source": "source",
"source_connect_csv_description": "Import feedback from CSV files",
"source_connect_formbricks_description": "Connect feedback from your Formbricks surveys",
"source_fields": "Source Fields",
"source_name": "Source Name",
"source_type": "Source Type",
"source_type_cannot_be_changed": "Source type cannot be changed",
"sources": "Sources",
"status_active": "In Progress",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "Upload CSV File"
"upload_csv_file": "Upload CSV File",
"user_identifier": "User",
"value": "Value"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID de respuesta",
"responses": "Respuestas",
"restart": "Reiniciar",
"retry": "Reintentar",
"role": "Rol",
"saas": "SaaS",
"sales": "Ventas",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Recopilado el",
"configure_import": "Configurar importación",
"configure_mapping": "Configurar asignación",
"connection": "Conexión",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Introduce un nombre para este origen",
"enter_value": "Introduce un valor...",
"enum": "enum",
"failed_to_load_feedback_records": "Error al cargar los registros de comentarios",
"feedback_date": "Fecha actual",
"feedback_record_fields": "Campos de registro de comentarios",
"feedback_records": "Registros de comentarios",
"field_label": "Etiqueta de campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Importación completada: {successes} correctas, {failures} fallidas, {skipped} omitidas (sin datos)",
"import_csv_data": "Importar comentarios",
@@ -2088,8 +2094,10 @@
"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_more": "Cargar más",
"load_sample_csv": "Cargar CSV de muestra",
"n_supported_questions": "{count} preguntas compatibles",
"no_feedback_records": "Aún no hay registros de comentarios. Los registros aparecerán aquí una vez que tus conectores empiecen a enviar datos.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Elige qué preguntas de la encuesta deben crear FeedbackRecords.",
"set_value": "establecer valor",
"setup_connection": "Configurar conexión",
"showing_count": "Mostrando {count} de {total} registros",
"showing_rows": "Mostrando 3 de {count} filas",
"source": "origen",
"source_connect_csv_description": "Importar feedback desde archivos CSV",
"source_connect_formbricks_description": "Conectar feedback de tus encuestas de Formbricks",
"source_fields": "Campos de origen",
"source_name": "Nombre de origen",
"source_type": "Tipo de fuente",
"source_type_cannot_be_changed": "El tipo de origen no se puede cambiar",
"sources": "Orígenes",
"status_active": "En progreso",
@@ -2131,7 +2141,9 @@
"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 comentarios.",
"upload_csv_file": "Subir archivo CSV"
"upload_csv_file": "Subir archivo CSV",
"user_identifier": "Usuario",
"value": "Valor"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID de réponse",
"responses": "Réponses",
"restart": "Recommencer",
"retry": "Réessayer",
"role": "Rôle",
"saas": "SaaS",
"sales": "Ventes",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Collecté le",
"configure_import": "Configurer l'importation",
"configure_mapping": "Configurer le mappage",
"connection": "Connexion",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Entrez un nom pour cette source",
"enter_value": "Saisir une valeur...",
"enum": "enum",
"failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback",
"feedback_date": "Date actuelle",
"feedback_record_fields": "Champs d'enregistrement de feedback",
"feedback_records": "Enregistrements de feedback",
"field_label": "Libellé du champ",
"field_type": "Type de champ",
"formbricks_surveys": "Sondages Formbricks",
"historical_import_complete": "Importation terminée: {successes} réussies, {failures} échouées, {skipped} ignorées (aucune donnée)",
"import_csv_data": "Importer les retours",
@@ -2088,8 +2094,10 @@
"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_more": "Charger plus",
"load_sample_csv": "Charger un exemple de CSV",
"n_supported_questions": "{count} questions prises en charge",
"no_feedback_records": "Aucun enregistrement de feedback pour le moment. Les enregistrements apparaîtront ici une fois que vos connecteurs commenceront à envoyer des données.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Choisissez quelles questions d'enquête doivent créer des FeedbackRecords.",
"set_value": "définir la valeur",
"setup_connection": "Configurer la connexion",
"showing_count": "Affichage de {count} sur {total} enregistrements",
"showing_rows": "Affichage de 3 sur {count} lignes",
"source": "source",
"source_connect_csv_description": "Importer des feedbacks depuis des fichiers CSV",
"source_connect_formbricks_description": "Connecter les feedbacks de vos enquêtes Formbricks",
"source_fields": "Champs source",
"source_name": "Nom de la source",
"source_type": "Type de source",
"source_type_cannot_be_changed": "Le type de source ne peut pas être modifié",
"sources": "Sources",
"status_active": "En cours",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "Télécharger un fichier CSV"
"upload_csv_file": "Télécharger un fichier CSV",
"user_identifier": "Utilisateur",
"value": "Valeur"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "Válaszazonosító",
"responses": "Válaszok",
"restart": "Újraindítás",
"retry": "Újra",
"role": "Szerep",
"saas": "SaaS",
"sales": "Értékesítés",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Gyűjtve",
"configure_import": "Importálás konfigurálása",
"configure_mapping": "Leképezés konfigurálása",
"connection": "Kapcsolat",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Adj nevet ennek a forrásnak",
"enter_value": "Érték megadása...",
"enum": "felsorolás",
"failed_to_load_feedback_records": "Nem sikerült betölteni a visszajelzési rekordokat",
"feedback_date": "Aktuális dátum",
"feedback_record_fields": "Visszajelzési rekord mezői",
"feedback_record_fields": "Visszajelzési rekord mezők",
"feedback_records": "Visszajelzési rekordok",
"field_label": "Mező címke",
"field_type": "Mező típus",
"formbricks_surveys": "Formbricks kérdőívek",
"historical_import_complete": "Importálás befejezve: {successes} sikeres, {failures} sikertelen, {skipped} kihagyva (nincs adat)",
"import_csv_data": "Visszajelzés importálása",
@@ -2088,8 +2094,10 @@
"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_more": "Továbbiak betöltése",
"load_sample_csv": "Minta CSV betöltése",
"n_supported_questions": "{count} támogatott kérdés",
"no_feedback_records": "Még nincsenek visszajelzési rekordok. A rekordok itt fognak megjelenni, amint a csatlakozók elkezdik küldeni az adatokat.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Válassza ki, mely kérdőívkérdések hozzanak létre visszajelzési rekordokat.",
"set_value": "érték beállítása",
"setup_connection": "Kapcsolat beállítása",
"showing_count": "{count} / {total} rekord megjelenítése",
"showing_rows": "3 megjelenítve {count} sorból",
"source": "forrás",
"source_connect_csv_description": "Visszajelzések importálása CSV fájlokból",
"source_connect_formbricks_description": "Visszajelzések csatlakoztatása a Formbricks kérdőívekből",
"source_fields": "Forrásmezők",
"source_name": "Forrásnév",
"source_type": "Forrás típus",
"source_type_cannot_be_changed": "A forrástípus nem módosítható",
"sources": "Források",
"status_active": "Folyamatban",
@@ -2131,7 +2141,9 @@
"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 a visszajelzési adatok importálásához.",
"upload_csv_file": "CSV fájl feltöltése"
"upload_csv_file": "CSV fájl feltöltése",
"user_identifier": "Felhasználó",
"value": "Érték"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "回答ID",
"responses": "回答",
"restart": "再開",
"retry": "再試行",
"role": "役割",
"saas": "SaaS",
"sales": "セールス",
@@ -2047,6 +2048,7 @@
"change_file": "ファイルを変更",
"click_load_sample_csv": "「サンプルCSVを読み込む」をクリックして列を表示",
"click_to_upload": "クリックしてアップロード",
"collected_at": "収集日時",
"configure_import": "インポートを設定",
"configure_mapping": "マッピングを設定",
"connection": "接続",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "このソースの名前を入力",
"enter_value": "値を入力...",
"enum": "列挙型",
"failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました",
"feedback_date": "現在の日付",
"feedback_record_fields": "フィードバックレコードフィールド",
"feedback_records": "フィードバックレコード",
"field_label": "フィールドラベル",
"field_type": "フィールドタイプ",
"formbricks_surveys": "Formbricks フォーム",
"historical_import_complete": "インポート完了: {successes}件成功、{failures}件失敗、{skipped}件スキップ(データなし)",
"import_csv_data": "フィードバックをインポート",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "過去のデータをインポート中...",
"invalid_enum_values": "{field}にマッピングされた列に無効な値があります",
"invalid_values_found": "検出された値: {values}(行: {rows}{extra}",
"load_more": "さらに読み込む",
"load_sample_csv": "サンプルCSVを読み込む",
"n_supported_questions": "{count} 件のサポートされている質問",
"no_feedback_records": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。",
"no_source_fields_loaded": "ソースフィールドがまだ読み込まれていません",
"no_sources_connected": "ソースがまだ接続されていません。開始するにはソースを追加してください。",
"no_surveys_found": "この環境にフォームが見つかりません",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "フィードバックレコードを作成するフォームの質問を選択してください。",
"set_value": "値を設定",
"setup_connection": "接続を設定",
"showing_count": "{total}件中{count}件を表示",
"showing_rows": "{count}行中3行を表示",
"source": "ソース",
"source_connect_csv_description": "CSVファイルからフィードバックをインポート",
"source_connect_formbricks_description": "Formbricksフォームからフィードバックを接続",
"source_fields": "ソースフィールド",
"source_name": "ソース名",
"source_type": "ソースタイプ",
"source_type_cannot_be_changed": "ソースタイプは変更できません",
"sources": "ソース",
"status_active": "進行中",
@@ -2131,7 +2141,9 @@
"update_mapping_description": "このソースのマッピング設定を更新します。",
"updated_at": "更新日時",
"upload_csv_data_description": "CSVファイルをアップロードして、フィードバックデータをインポートします。",
"upload_csv_file": "CSVファイルをアップロード"
"upload_csv_file": "CSVファイルをアップロード",
"user_identifier": "ユーザー",
"value": "値"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "Antwoord-ID",
"responses": "Reacties",
"restart": "Opnieuw opstarten",
"retry": "Opnieuw proberen",
"role": "Rol",
"saas": "SaaS",
"sales": "Verkoop",
@@ -2047,6 +2048,7 @@
"change_file": "Bestand wijzigen",
"click_load_sample_csv": "Klik op 'Voorbeeld CSV laden' om kolommen te zien",
"click_to_upload": "Klik om te uploaden",
"collected_at": "Verzameld op",
"configure_import": "Import configureren",
"configure_mapping": "Koppeling configureren",
"connection": "Verbinding",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Voer een naam in voor deze bron",
"enter_value": "Voer waarde in...",
"enum": "enum",
"failed_to_load_feedback_records": "Kan feedbackrecords niet laden",
"feedback_date": "Huidige datum",
"feedback_record_fields": "Feedbackrecordvelden",
"feedback_records": "Feedbackrecords",
"field_label": "Veldlabel",
"field_type": "Veldtype",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Import voltooid: {successes} geslaagd, {failures} mislukt, {skipped} overgeslagen (geen data)",
"import_csv_data": "Feedback importeren",
@@ -2088,8 +2094,10 @@
"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_more": "Laad meer",
"load_sample_csv": "Voorbeeld-CSV laden",
"n_supported_questions": "{count} ondersteunde vragen",
"no_feedback_records": "Nog geen feedbackrecords. Records verschijnen hier zodra je connectoren gegevens beginnen te verzenden.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Kies welke enquêtevragen FeedbackRecords moeten aanmaken.",
"set_value": "waarde instellen",
"setup_connection": "Verbinding instellen",
"showing_count": "{count} van {total} records weergegeven",
"showing_rows": "3 van {count} rijen weergegeven",
"source": "bron",
"source_connect_csv_description": "Importeer feedback uit CSV-bestanden",
"source_connect_formbricks_description": "Verbind feedback van je Formbricks-enquêtes",
"source_fields": "Bronvelden",
"source_name": "Bronnaam",
"source_type": "Brontype",
"source_type_cannot_be_changed": "Brontype kan niet worden gewijzigd",
"sources": "Bronnen",
"status_active": "In uitvoering",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "CSV-bestand uploaden"
"upload_csv_file": "CSV-bestand uploaden",
"user_identifier": "Gebruiker",
"value": "Waarde"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID da resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"retry": "Tentar novamente",
"role": "Rolê",
"saas": "SaaS",
"sales": "vendas",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Coletado em",
"configure_import": "Configurar importação",
"configure_mapping": "Configurar mapeamento",
"connection": "Conexão",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Digite um nome para esta origem",
"enter_value": "Digite o valor...",
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registros de feedback",
"feedback_date": "Data atual",
"feedback_record_fields": "Campos de registro de feedback",
"feedback_record_fields": "Campos do registro de feedback",
"feedback_records": "Registros de feedback",
"field_label": "Rótulo do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"historical_import_complete": "Importação concluída: {successes} bem-sucedidas, {failures} falharam, {skipped} ignoradas (sem dados)",
"import_csv_data": "Importar feedback",
@@ -2088,8 +2094,10 @@
"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_more": "Carregar mais",
"load_sample_csv": "Carregar CSV de exemplo",
"n_supported_questions": "{count} perguntas suportadas",
"no_feedback_records": "Nenhum registro de feedback ainda. Os registros aparecerão aqui assim que seus conectores começarem a enviar dados.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Escolha quais perguntas da pesquisa devem criar FeedbackRecords.",
"set_value": "definir valor",
"setup_connection": "Configurar conexão",
"showing_count": "Mostrando {count} de {total} registros",
"showing_rows": "Mostrando 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de arquivos CSV",
"source_connect_formbricks_description": "Conectar feedback das suas pesquisas Formbricks",
"source_fields": "Campos de origem",
"source_name": "Nome da origem",
"source_type": "Tipo de fonte",
"source_type_cannot_be_changed": "O tipo de origem não pode ser alterado",
"sources": "Origens",
"status_active": "Em andamento",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "Fazer upload de arquivo CSV"
"upload_csv_file": "Fazer upload de arquivo CSV",
"user_identifier": "Usuário",
"value": "Valor"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID de resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"retry": "Tentar novamente",
"role": "Função",
"saas": "SaaS",
"sales": "Vendas",
@@ -2047,6 +2048,7 @@
"change_file": "Alterar ficheiro",
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
"click_to_upload": "Clique para carregar",
"collected_at": "Recolhido em",
"configure_import": "Configurar importação",
"configure_mapping": "Configurar mapeamento",
"connection": "Conexão",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Introduz um nome para esta origem",
"enter_value": "Introduzir valor...",
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registos de feedback",
"feedback_date": "Data atual",
"feedback_record_fields": "Campos de registo de feedback",
"feedback_records": "Registos de feedback",
"field_label": "Etiqueta do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"historical_import_complete": "Importação concluída: {successes} com sucesso, {failures} falharam, {skipped} ignorados (sem dados)",
"import_csv_data": "Importar feedback",
@@ -2088,8 +2094,10 @@
"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_more": "Carregar mais",
"load_sample_csv": "Carregar CSV de exemplo",
"n_supported_questions": "{count} perguntas suportadas",
"no_feedback_records": "Ainda não há registos de feedback. Os registos aparecerão aqui assim que os teus conectores começarem a enviar dados.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Escolha quais perguntas do inquérito devem criar FeedbackRecords.",
"set_value": "definir valor",
"setup_connection": "Configurar ligação",
"showing_count": "A mostrar {count} de {total} registos",
"showing_rows": "A mostrar 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de ficheiros CSV",
"source_connect_formbricks_description": "Conectar feedback dos seus inquéritos Formbricks",
"source_fields": "Campos da fonte",
"source_name": "Nome da fonte",
"source_type": "Tipo de fonte",
"source_type_cannot_be_changed": "O tipo de fonte não pode ser alterado",
"sources": "Fontes",
"status_active": "Em progresso",
@@ -2130,8 +2140,10 @@
"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.",
"upload_csv_file": "Carregar ficheiro CSV"
"upload_csv_data_description": "Carrega um ficheiro CSV para importar dados de feedback.",
"upload_csv_file": "Carregar ficheiro CSV",
"user_identifier": "Utilizador",
"value": "Valor"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID răspuns",
"responses": "Răspunsuri",
"restart": "Repornește",
"retry": "Reîncearcă",
"role": "Rolul",
"saas": "SaaS",
"sales": "Vânzări",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Colectat la",
"configure_import": "Configurează importul",
"configure_mapping": "Configurează maparea",
"connection": "Conexiune",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Introdu un nume pentru această sursă",
"enter_value": "Introdu valoarea...",
"enum": "enum",
"failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback",
"feedback_date": "Data curentă",
"feedback_record_fields": "Câmpuri ale înregistrării de feedback",
"feedback_record_fields": "Câmpuri înregistrare feedback",
"feedback_records": "Înregistrări de feedback",
"field_label": "Etichetă câmp",
"field_type": "Tip câmp",
"formbricks_surveys": "Chestionare Formbricks",
"historical_import_complete": "Import finalizat: {successes} reușite, {failures} eșuate, {skipped} omise (fără date)",
"import_csv_data": "Importă feedback",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "Se importă datele istorice...",
"invalid_enum_values": "Valori invalide în coloana mapată la {field}",
"invalid_values_found": "Găsite: {values} (rânduri: {rows}) {extra}",
"load_more": "Încarcă mai multe",
"load_sample_csv": "Încarcă un CSV de exemplu",
"n_supported_questions": "{count} întrebări acceptate",
"no_feedback_records": "Nu există încă înregistrări de feedback. Înregistrările vor apărea aici după ce conectorii tăi vor începe să trimită date.",
"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",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Alege ce întrebări din chestionar vor crea FeedbackRecords.",
"set_value": "setează valoare",
"setup_connection": "Configurează conexiunea",
"showing_count": "Se afișează {count} din {total} înregistrări",
"showing_rows": "Se afișează 3 din {count} rânduri",
"source": "sursă",
"source_connect_csv_description": "Importă feedback din fișiere CSV",
"source_connect_formbricks_description": "Conectează feedback din sondajele Formbricks",
"source_fields": "Câmpuri sursă",
"source_name": "Nume sursă",
"source_type": "Tip sursă",
"source_type_cannot_be_changed": "Tipul sursei nu poate fi schimbat",
"sources": "Surse",
"status_active": "În progres",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "Încarcă fișier CSV"
"upload_csv_file": "Încarcă fișier CSV",
"user_identifier": "Utilizator",
"value": "Valoare"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "ID ответа",
"responses": "Ответы",
"restart": "Перезапустить",
"retry": "Повторить",
"role": "Роль",
"saas": "SaaS",
"sales": "Продажи",
@@ -2047,6 +2048,7 @@
"change_file": "Изменить файл",
"click_load_sample_csv": "Нажмите «Загрузить пример CSV», чтобы увидеть столбцы",
"click_to_upload": "Кликните для загрузки",
"collected_at": "Собрано",
"configure_import": "Настроить импорт",
"configure_mapping": "Настроить сопоставление",
"connection": "Подключение",
@@ -2064,7 +2066,7 @@
"csv_files_only": "Только файлы CSV",
"csv_import": "Импорт CSV",
"csv_import_complete": "Импорт CSV завершён: {successes} успешно, {failures} с ошибками, {skipped} пропущено",
"csv_import_duplicate_warning": "Если импортировать данные дважды, будут созданы дубликаты записей.",
"csv_import_duplicate_warning": "Импорт уже загруженных данных может создать дубликаты записей.",
"csv_inconsistent_columns": "В строке {row} несоответствие столбцов. Во всех строках должны быть одинаковые заголовки.",
"csv_max_records": "Допустимо не более {max} записей.",
"default_connector_name_csv": "Импорт CSV",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Введи имя для этого источника",
"enter_value": "Введите значение...",
"enum": "enum",
"failed_to_load_feedback_records": "Не удалось загрузить отзывы",
"feedback_date": "Текущая дата",
"feedback_record_fields": "Поля записи обратной связи",
"feedback_record_fields": "Поля записи отзыва",
"feedback_records": "Записи отзывов",
"field_label": "Метка поля",
"field_type": "Тип поля",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Импорт завершён: {successes} успешно, {failures} с ошибками, {skipped} пропущено (нет данных)",
"import_csv_data": "Импортировать отзывы",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "Импорт исторических данных...",
"invalid_enum_values": "Недопустимые значения в столбце, сопоставленном с {field}",
"invalid_values_found": "Найдено: {values} (строки: {rows}) {extra}",
"load_more": "Загрузить ещё",
"load_sample_csv": "Загрузить пример CSV",
"n_supported_questions": "Поддерживается {count} вопрос(ов)",
"no_feedback_records": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.",
"no_source_fields_loaded": "Поля источника ещё не загружены",
"no_sources_connected": "Нет подключённых источников. Добавьте источник, чтобы начать.",
"no_surveys_found": "В этой среде не найдено опросов",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Выберите, какие вопросы опроса должны создавать FeedbackRecords.",
"set_value": "установить значение",
"setup_connection": "Настроить подключение",
"showing_count": "Показано {count} из {total} записей",
"showing_rows": "Показано 3 из {count} строк",
"source": "источник",
"source_connect_csv_description": "Импортировать отзывы из CSV-файлов",
"source_connect_formbricks_description": "Подключить отзывы из ваших опросов Formbricks",
"source_fields": "Поля источника",
"source_name": "Имя источника",
"source_type": "Тип источника",
"source_type_cannot_be_changed": "Тип источника нельзя изменить",
"sources": "Источники",
"status_active": "В процессе",
@@ -2130,8 +2140,10 @@
"unify_feedback": "Обратная связь Unify",
"update_mapping_description": "Обнови настройки сопоставления для этого источника.",
"updated_at": "Обновлено",
"upload_csv_data_description": "Загрузите CSV-файл, чтобы импортировать данные обратной связи.",
"upload_csv_file": "Загрузить CSV-файл"
"upload_csv_data_description": "Загрузи CSV-файл, чтобы импортировать данные отзывов.",
"upload_csv_file": "Загрузить CSV-файл",
"user_identifier": "Пользователь",
"value": "Значение"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "Svar-ID",
"responses": "Svar",
"restart": "Starta om",
"retry": "Försök igen",
"role": "Roll",
"saas": "SaaS",
"sales": "Försäljning",
@@ -2047,6 +2048,7 @@
"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",
"collected_at": "Insamlad",
"configure_import": "Konfigurera import",
"configure_mapping": "Konfigurera mappning",
"connection": "Anslutning",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "Ange ett namn för denna källa",
"enter_value": "Ange värde...",
"enum": "enum",
"failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter",
"feedback_date": "Aktuellt datum",
"feedback_record_fields": "Fält för feedbackpost",
"feedback_records": "Feedbackposter",
"field_label": "Fältetikett",
"field_type": "Fälttyp",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Importen klar: {successes} lyckades, {failures} misslyckades, {skipped} hoppades över (ingen data)",
"import_csv_data": "Importera feedback",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "Importerar historisk data...",
"invalid_enum_values": "Ogiltiga värden i kolumnen som är kopplad till {field}",
"invalid_values_found": "Hittade: {values} (rader: {rows}) {extra}",
"load_more": "Ladda mer",
"load_sample_csv": "Ladda exempel-CSV",
"n_supported_questions": "{count} stödda frågor",
"no_feedback_records": "Inga feedbackposter ännu. Poster visas här när dina connectors börjar skicka data.",
"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ö",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "Välj vilka enkätfrågor som ska skapa FeedbackRecords.",
"set_value": "ange värde",
"setup_connection": "Ställ in anslutning",
"showing_count": "Visar {count} av {total} poster",
"showing_rows": "Visar 3 av {count} rader",
"source": "källa",
"source_connect_csv_description": "Importera feedback från CSV-filer",
"source_connect_formbricks_description": "Anslut feedback från dina Formbricks-enkäter",
"source_fields": "Källfält",
"source_name": "Källnamn",
"source_type": "Källtyp",
"source_type_cannot_be_changed": "Källtyp kan inte ändras",
"sources": "Källor",
"status_active": "Pågående",
@@ -2131,7 +2141,9 @@
"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.",
"upload_csv_file": "Ladda upp CSV-fil"
"upload_csv_file": "Ladda upp CSV-fil",
"user_identifier": "Användare",
"value": "Värde"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "响应 ID",
"responses": "反馈",
"restart": "重新启动",
"retry": "重试",
"role": "角色",
"saas": "SaaS",
"sales": "销售",
@@ -2047,6 +2048,7 @@
"change_file": "更换文件",
"click_load_sample_csv": "点击“加载示例 CSV”查看列",
"click_to_upload": "点击上传",
"collected_at": "收集时间",
"configure_import": "配置导入",
"configure_mapping": "配置映射",
"connection": "连接",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "为此来源输入名称",
"enter_value": "请输入值...",
"enum": "枚举",
"failed_to_load_feedback_records": "加载反馈记录失败",
"feedback_date": "当前日期",
"feedback_record_fields": "反馈记录字段",
"feedback_records": "反馈记录",
"field_label": "字段标签",
"field_type": "字段类型",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "导入完成:{successes} 个成功,{failures} 个失败,{skipped} 个跳过(无数据)",
"import_csv_data": "导入反馈",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "正在导入历史数据…",
"invalid_enum_values": "映射到 {field} 的列中存在无效值",
"invalid_values_found": "发现:{values}(行:{rows}{extra}",
"load_more": "加载更多",
"load_sample_csv": "加载示例 CSV",
"n_supported_questions": "{count} 个支持的问题",
"no_feedback_records": "暂无反馈记录。当你的连接器开始发送数据后,记录会显示在这里。",
"no_source_fields_loaded": "尚未加载源字段",
"no_sources_connected": "还没有连接数据源。添加一个数据源开始吧。",
"no_surveys_found": "此环境下未找到调查",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "选择哪些调查问题会创建反馈记录。",
"set_value": "设置值",
"setup_connection": "设置连接",
"showing_count": "正在显示 {count} / {total} 条记录",
"showing_rows": "显示 {count} 行中的 3 行",
"source": "source",
"source_connect_csv_description": "从 CSV 文件导入反馈",
"source_connect_formbricks_description": "连接来自你 Formbricks 调查的反馈",
"source_fields": "来源字段",
"source_name": "来源名称",
"source_type": "来源类型",
"source_type_cannot_be_changed": "来源类型无法更改",
"sources": "来源",
"status_active": "进行中",
@@ -2131,7 +2141,9 @@
"update_mapping_description": "更新此来源的映射配置。",
"updated_at": "更新于",
"upload_csv_data_description": "上传 CSV 文件以导入反馈数据。",
"upload_csv_file": "上传 CSV 文件"
"upload_csv_file": "上传 CSV 文件",
"user_identifier": "用户",
"value": "值"
},
"workspace": {
"api_keys": {

View File

@@ -361,6 +361,7 @@
"response_id": "回應 ID",
"responses": "回應",
"restart": "重新開始",
"retry": "重試",
"role": "角色",
"saas": "SaaS",
"sales": "銷售",
@@ -2047,6 +2048,7 @@
"change_file": "更換檔案",
"click_load_sample_csv": "點擊「載入範例 CSV」以查看欄位",
"click_to_upload": "點擊以上傳",
"collected_at": "收集時間",
"configure_import": "設定匯入",
"configure_mapping": "設定對應關係",
"connection": "連線",
@@ -2077,8 +2079,12 @@
"enter_name_for_source": "請輸入此來源的名稱",
"enter_value": "請輸入值……",
"enum": "enum",
"failed_to_load_feedback_records": "載入回饋紀錄失敗",
"feedback_date": "目前日期",
"feedback_record_fields": "回饋紀錄欄位",
"feedback_records": "回饋紀錄",
"field_label": "欄位標籤",
"field_type": "欄位類型",
"formbricks_surveys": "Formbricks 問卷",
"historical_import_complete": "匯入完成:{successes} 筆成功,{failures} 筆失敗,{skipped} 筆略過(無資料)",
"import_csv_data": "匯入 CSV 資料",
@@ -2088,8 +2094,10 @@
"importing_historical_data": "正在匯入歷史資料…",
"invalid_enum_values": "對應到 {field} 欄位的值無效",
"invalid_values_found": "發現:{values}(列:{rows}{extra}",
"load_more": "載入更多",
"load_sample_csv": "載入範例 CSV",
"n_supported_questions": "{count} 個支援的問題",
"no_feedback_records": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。",
"no_source_fields_loaded": "尚未載入來源欄位",
"no_sources_connected": "尚未連接任何來源。請新增來源以開始使用。",
"no_surveys_found": "此環境中找不到問卷",
@@ -2111,12 +2119,14 @@
"select_survey_questions_description": "請選擇哪些問卷問題要建立 FeedbackRecords。",
"set_value": "設定值",
"setup_connection": "設定連線",
"showing_count": "顯示 {count} 筆,共 {total} 筆紀錄",
"showing_rows": "顯示 {count} 筆資料中的 3 筆",
"source": "來源",
"source_connect_csv_description": "從 CSV 檔案匯入回饋",
"source_connect_formbricks_description": "連接來自你 Formbricks 問卷的回饋",
"source_fields": "來源欄位",
"source_name": "來源名稱",
"source_type": "來源類型",
"source_type_cannot_be_changed": "來源類型無法變更",
"sources": "來源",
"status_active": "進行中",
@@ -2131,7 +2141,9 @@
"update_mapping_description": "更新此來源的對應設定。",
"updated_at": "更新時間",
"upload_csv_data_description": "上傳 CSV 檔案以匯入回饋資料。",
"upload_csv_file": "上傳 CSV 檔案"
"upload_csv_file": "上傳 CSV 檔案",
"user_identifier": "使用者",
"value": "值"
},
"workspace": {
"api_keys": {

View File

@@ -1,3 +1,14 @@
export { getHubClient } from "./hub-client";
export { createFeedbackRecord, createFeedbackRecordsBatch, type CreateFeedbackRecordResult } from "./service";
export type { FeedbackRecordCreateParams, FeedbackRecordData } from "./types";
export {
createFeedbackRecord,
createFeedbackRecordsBatch,
listFeedbackRecords,
type CreateFeedbackRecordResult,
type ListFeedbackRecordsResult,
} from "./service";
export type {
FeedbackRecordCreateParams,
FeedbackRecordData,
FeedbackRecordListParams,
FeedbackRecordListResponse,
} from "./types";

View File

@@ -2,7 +2,12 @@ import "server-only";
import FormbricksHub from "@formbricks/hub";
import { logger } from "@formbricks/logger";
import { getHubClient } from "./hub-client";
import type { FeedbackRecordCreateParams, FeedbackRecordData } from "./types";
import type {
FeedbackRecordCreateParams,
FeedbackRecordData,
FeedbackRecordListParams,
FeedbackRecordListResponse,
} from "./types";
export type CreateFeedbackRecordResult = {
data: FeedbackRecordData | null;
@@ -41,6 +46,32 @@ export const createFeedbackRecord = async (
}
};
export type ListFeedbackRecordsResult = {
data: FeedbackRecordListResponse | null;
error: { status: number; message: string; detail: string } | null;
};
/**
* List feedback records from the Hub with optional filters and pagination.
*/
export const listFeedbackRecords = async (
params?: FeedbackRecordListParams
): Promise<ListFeedbackRecordsResult> => {
const client = getHubClient();
if (!client) {
return { data: null, error: { ...NO_CONFIG_ERROR } };
}
try {
const data = await client.feedbackRecords.list(params);
return { data, error: null };
} catch (err) {
logger.warn({ err }, "Hub: listFeedbackRecords failed");
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 multiple feedback records in the Hub in parallel.
* Returns an array of results (data or error) per input; logs failures.

View File

@@ -2,3 +2,5 @@ import type FormbricksHub from "@formbricks/hub";
export type FeedbackRecordCreateParams = FormbricksHub.FeedbackRecordCreateParams;
export type FeedbackRecordData = FormbricksHub.FeedbackRecordData;
export type FeedbackRecordListParams = FormbricksHub.FeedbackRecordListParams;
export type FeedbackRecordListResponse = FormbricksHub.FeedbackRecordListResponse;

View File

@@ -51,7 +51,10 @@ services:
image: ghcr.io/formbricks/hub:latest
restart: "no"
entrypoint: ["sh", "-c"]
command: ["if [ -x /usr/local/bin/goose ] && [ -x /usr/local/bin/river ]; then /usr/local/bin/goose -dir /app/migrations postgres \"$$DATABASE_URL\" up && /usr/local/bin/river migrate-up --database-url \"$$DATABASE_URL\"; else echo 'Migration tools (goose/river) not in image, skipping migrations.'; fi"]
command:
[
'if [ -x /usr/local/bin/goose ] && [ -x /usr/local/bin/river ]; then /usr/local/bin/goose -dir /app/migrations postgres "$$DATABASE_URL" up && /usr/local/bin/river migrate-up --database-url "$$DATABASE_URL"; else echo ''Migration tools (goose/river) not in image, skipping migrations.''; fi',
]
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
depends_on: