Compare commits

...

1 Commits

Author SHA1 Message Date
Anshuman Pandey 4d44e18a61 feat: connects feedback record directories to connectors (#7749) 2026-04-17 16:07:37 +04:00
48 changed files with 1010 additions and 463 deletions
@@ -298,6 +298,12 @@ export const MainNavigation = ({
href: `/workspaces/${workspace.id}/settings/billing`,
hidden: !isFormbricksCloud,
},
{
id: "feedback-record-directories",
label: t("workspace.settings.feedback_record_directories.title"),
href: `/workspaces/${workspace.id}/settings/feedback-record-directories`,
hidden: !isOwnerOrManager,
},
{
id: "enterprise",
label: t("common.enterprise_license"),
@@ -146,7 +146,7 @@ export const OrganizationBreadcrumb = ({
},
{
id: "feedback-record-directories",
label: t("workspace.settings.feedback_record_directories.nav_label"),
label: t("workspace.settings.feedback_record_directories.title"),
href: `${workspaceBasePath}/settings/feedback-record-directories`,
hidden: isMember,
},
@@ -9,12 +9,16 @@ import { FeedbackRecordsTable } from "./feedback-records-table";
interface FeedbackRecordsPageClientProps {
workspaceId: string;
directories: { id: string; name: string }[];
initialFrdId: string | null;
initialRecords: FeedbackRecordData[];
initialNextCursor?: string;
}
export function FeedbackRecordsPageClient({
workspaceId,
directories,
initialFrdId,
initialRecords,
initialNextCursor,
}: FeedbackRecordsPageClientProps) {
@@ -28,6 +32,8 @@ export function FeedbackRecordsPageClient({
<FeedbackRecordsTable
workspaceId={workspaceId}
directories={directories}
initialFrdId={initialFrdId}
initialRecords={initialRecords}
initialNextCursor={initialNextCursor}
/>
@@ -13,10 +13,19 @@ import { useCallback, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { listFeedbackRecordsAction } from "@/lib/connector/actions";
import { formatDateForDisplay, formatDateTimeForDisplay } from "@/lib/utils/datetime";
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 { Label } from "@/modules/ui/components/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
const RECORDS_PER_PAGE = 50;
@@ -33,24 +42,14 @@ const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
date: <CalendarIcon className="h-3.5 w-3.5" />,
};
const formatValue = (record: FeedbackRecordData, t: TFunction, locale?: string): string => {
const formatValue = (record: FeedbackRecordData, t: TFunction, locale: string): 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 ? t("common.yes") : t("common.no");
if (record.value_date != null) return new Date(record.value_date).toLocaleDateString(locale);
if (record.value_date != null) return formatDateForDisplay(new Date(record.value_date), locale);
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) + "…";
@@ -58,16 +57,22 @@ function truncate(str: string, maxLen: number): string {
interface FeedbackRecordsTableProps {
workspaceId: string;
directories: { id: string; name: string }[];
initialFrdId: string | null;
initialRecords: FeedbackRecordData[];
initialNextCursor?: string;
}
export const FeedbackRecordsTable = ({
workspaceId,
directories,
initialFrdId,
initialRecords,
initialNextCursor,
}: FeedbackRecordsTableProps) => {
const { t, i18n } = useTranslation();
const locale = i18n.resolvedLanguage ?? i18n.language ?? "en-US";
const [selectedFrdId, setSelectedFrdId] = useState<string | null>(initialFrdId);
const [records, setRecords] = useState<FeedbackRecordData[]>(initialRecords);
const [nextCursor, setNextCursor] = useState<string | undefined>(initialNextCursor);
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -75,13 +80,14 @@ export const FeedbackRecordsTable = ({
const [error, setError] = useState<string | null>(null);
const fetchRecords = useCallback(
async (cursor: string | undefined, append: boolean) => {
async (frdId: string, cursor: string | undefined, append: boolean) => {
const setLoading = append ? setIsLoadingMore : setIsRefreshing;
setLoading(true);
setError(null);
const result = await listFeedbackRecordsAction({
workspaceId,
frdId,
limit: RECORDS_PER_PAGE,
cursor,
});
@@ -100,37 +106,37 @@ export const FeedbackRecordsTable = ({
[workspaceId, t]
);
const handleFrdChange = (frdId: string) => {
setSelectedFrdId(frdId);
fetchRecords(frdId, undefined, false);
};
const handleLoadMore = () => {
fetchRecords(nextCursor, true);
if (!selectedFrdId) return;
fetchRecords(selectedFrdId, nextCursor, true);
};
const handleRefresh = async () => {
if (isRefreshing) return;
setIsRefreshing(true);
setError(null);
if (!selectedFrdId || isRefreshing) return;
const toastId = toast.loading(t("workspace.unify.refreshing_feedback_records"));
const result = await listFeedbackRecordsAction({
workspaceId,
limit: RECORDS_PER_PAGE,
});
if (!result?.data) {
toast.error(getFormattedErrorMessage(result) ?? t("workspace.unify.failed_to_load_feedback_records"), {
id: toastId,
});
setIsRefreshing(false);
return;
}
setRecords(result.data.data);
setNextCursor(result.data.next_cursor);
setIsRefreshing(false);
await fetchRecords(selectedFrdId, undefined, false);
toast.success(t("workspace.unify.feedback_records_refreshed"), { id: toastId });
};
const hasMore = !!nextCursor;
const isEmpty = records.length === 0 && !isRefreshing;
const currentFrdName = directories.find((d) => d.id === selectedFrdId)?.name ?? "—";
if (directories.length === 0) {
return (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50 p-8 text-center">
<MessageSquareTextIcon className="mx-auto h-8 w-8 text-slate-400" />
<p className="mt-2 text-sm text-slate-500">
{t("workspace.unify.no_feedback_record_directory_available")}
</p>
</div>
);
}
if (error) {
return (
@@ -146,15 +152,35 @@ export const FeedbackRecordsTable = ({
);
}
const isEmpty = records.length === 0 && !isRefreshing;
return (
<div className="space-y-3">
{!isEmpty && (
<div className="flex items-center justify-between">
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count", { count: records.length })}
</p>
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-col gap-1">
<Label>{t("workspace.unify.feedback_record_directory")}</Label>
{directories.length === 1 ? (
<p className="text-sm font-medium text-slate-900">{currentFrdName}</p>
) : (
<Select value={selectedFrdId ?? ""} onValueChange={handleFrdChange}>
<SelectTrigger className="min-w-[220px]">
<SelectValue placeholder={t("workspace.unify.select_feedback_record_directory")} />
</SelectTrigger>
<SelectContent>
{directories.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="flex items-center gap-3">
{!isEmpty && (
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count_loaded", { count: records.length })}
</p>
)}
<Button
variant="secondary"
size="sm"
@@ -164,7 +190,7 @@ export const FeedbackRecordsTable = ({
<RefreshCwIcon className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
</div>
)}
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="overflow-x-auto">
@@ -193,7 +219,7 @@ export const FeedbackRecordsTable = ({
) : (
<tbody className="divide-y divide-slate-100">
{records.map((record) => (
<FeedbackRecordRow key={record.id} record={record} locale={i18n.language} t={t} />
<FeedbackRecordRow key={record.id} record={record} locale={locale} t={t} />
))}
</tbody>
)}
@@ -204,7 +230,7 @@ export const FeedbackRecordsTable = ({
{hasMore && (
<div className="flex justify-center">
<Button variant="secondary" size="sm" onClick={handleLoadMore} loading={isLoadingMore}>
{t("workspace.unify.load_more")}
{t("common.load_more")}
</Button>
</div>
)}
@@ -227,7 +253,7 @@ const FeedbackRecordRow = ({
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)}
{formatDateTimeForDisplay(new Date(record.collected_at), locale)}
</td>
<td className="whitespace-nowrap px-4 py-3">
<Badge text={record.source_type} type="gray" size="tiny" />
@@ -1,12 +1,16 @@
import { notFound } from "next/navigation";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { FeedbackRecordListResponse } from "@/modules/hub";
import { listFeedbackRecords } from "@/modules/hub/service";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { FeedbackRecordsPageClient } from "./feedback-records-page-client";
const INITIAL_PAGE_SIZE = 50;
const INITIAL_PAGE_SIZE = 10;
export default async function UnifyFeedbackRecordsPage(props: { params: Promise<{ workspaceId: string }> }) {
export default async function UnifyFeedbackRecordsPage(props: {
readonly params: Promise<{ workspaceId: string }>;
}) {
const t = await getTranslate();
const params = await props.params;
@@ -22,22 +26,27 @@ export default async function UnifyFeedbackRecordsPage(props: { params: Promise<
return notFound();
}
const result = await listFeedbackRecords({
tenant_id: params.workspaceId,
limit: INITIAL_PAGE_SIZE,
});
const frds = await getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId);
if (result.error) {
throw new Error(t("workspace.unify.failed_to_load_feedback_records"));
// Preload first FRD's records server-side for fast initial render
const initialFrdId = frds[0]?.id;
let initialRecords: FeedbackRecordListResponse | null = null;
if (initialFrdId) {
const result = await listFeedbackRecords({ tenant_id: initialFrdId, limit: INITIAL_PAGE_SIZE });
// Don't crash if Hub is down — show empty state
if (!result.error) {
initialRecords = result.data;
}
}
const initialData = result.data ?? { data: [], limit: INITIAL_PAGE_SIZE };
return (
<FeedbackRecordsPageClient
workspaceId={params.workspaceId}
initialRecords={initialData.data}
initialNextCursor={initialData.next_cursor}
directories={frds}
initialFrdId={initialFrdId ?? null}
initialRecords={initialRecords?.data ?? []}
initialNextCursor={initialRecords?.next_cursor}
/>
);
}
@@ -25,12 +25,14 @@ interface ConnectorsSectionProps {
workspaceId: string;
initialConnectors: TConnectorWithMappings[];
initialSurveys: TUnifySurvey[];
directories: { id: string; name: string }[];
}
export function ConnectorsSection({
workspaceId,
initialConnectors,
initialSurveys,
directories,
}: ConnectorsSectionProps) {
const { t } = useTranslation();
const router = useRouter();
@@ -41,6 +43,7 @@ export function ConnectorsSection({
const handleCreateConnector = async (data: {
name: string;
type: TConnectorType;
feedbackRecordDirectoryId: string;
surveyMappings?: { surveyId: string; elementIds: string[] }[];
fieldMappings?: TFieldMapping[];
}): Promise<string | undefined> => {
@@ -49,6 +52,7 @@ export function ConnectorsSection({
connectorInput: {
name: data.name,
type: data.type,
feedbackRecordDirectoryId: data.feedbackRecordDirectoryId,
},
formbricksMappings:
data.type === "formbricks" && data.surveyMappings?.length ? data.surveyMappings : undefined,
@@ -159,6 +163,7 @@ export function ConnectorsSection({
onCreateConnector={handleCreateConnector}
surveys={initialSurveys}
workspaceId={workspaceId}
directories={directories}
/>
}>
<UnifyConfigNavigation workspaceId={workspaceId} activeId="sources" />
@@ -182,6 +187,7 @@ export function ConnectorsSection({
onOpenChange={(open) => !open && setEditingConnector(null)}
onUpdateConnector={handleUpdateConnector}
surveys={initialSurveys}
directories={directories}
onOpenCsvImport={() => {
if (editingConnector) {
setCsvImportConnector(editingConnector);
@@ -23,6 +23,13 @@ import {
} from "@/modules/ui/components/dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import {
FEEDBACK_RECORD_FIELDS,
TCreateConnectorStep,
@@ -41,11 +48,13 @@ interface CreateConnectorModalProps {
onCreateConnector: (data: {
name: string;
type: TConnectorType;
feedbackRecordDirectoryId: string;
surveyMappings?: { surveyId: string; elementIds: string[] }[];
fieldMappings?: TFieldMapping[];
}) => Promise<string | undefined>;
surveys: TUnifySurvey[];
workspaceId: string;
directories: { id: string; name: string }[];
}
const getDialogTitle = (
@@ -152,6 +161,7 @@ export const CreateConnectorModal = ({
onCreateConnector,
surveys,
workspaceId,
directories,
}: CreateConnectorModalProps) => {
const { t } = useTranslation();
@@ -178,6 +188,9 @@ export const CreateConnectorModal = ({
const [importHistoricalBySurvey, setImportHistoricalBySurvey] = useState<Record<string, boolean>>({});
const [isImporting, setIsImporting] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<string | null>(
directories.length === 1 ? directories[0].id : null
);
const fetchResponseCount = useCallback(
async (surveyId: string) => {
@@ -214,6 +227,7 @@ export const CreateConnectorModal = ({
setImportHistoricalBySurvey({});
setIsImporting(false);
setIsCreating(false);
setSelectedDirectoryId(directories.length === 1 ? directories[0].id : null);
};
const handleOpenChange = (newOpen: boolean) => {
@@ -348,7 +362,7 @@ export const CreateConnectorModal = ({
};
const handleCreate = async () => {
if (!selectedType || !connectorName.trim()) return;
if (!selectedType || !connectorName.trim() || !selectedDirectoryId) return;
if (selectedType === "csv" && csvParsedData.length > 0) {
const errors = validateEnumMappings(mappings, csvParsedData);
@@ -366,6 +380,7 @@ export const CreateConnectorModal = ({
const connectorId = await onCreateConnector({
name: connectorName.trim(),
type: selectedType,
feedbackRecordDirectoryId: selectedDirectoryId,
surveyMappings: selectedType === "formbricks" && surveyMappings.length > 0 ? surveyMappings : undefined,
fieldMappings: selectedType !== "formbricks" && mappings.length > 0 ? mappings : undefined,
});
@@ -441,6 +456,14 @@ export const CreateConnectorModal = ({
/>
</div>
<FrdPicker
directories={directories}
selectedDirectoryId={selectedDirectoryId}
onChange={setSelectedDirectoryId}
workspaceId={workspaceId}
t={t}
/>
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<FormbricksSurveySelector
surveys={surveys}
@@ -492,6 +515,14 @@ export const CreateConnectorModal = ({
/>
</div>
<FrdPicker
directories={directories}
selectedDirectoryId={selectedDirectoryId}
onChange={setSelectedDirectoryId}
workspaceId={workspaceId}
t={t}
/>
<div className="max-h-[55vh] overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
<CsvConnectorUI
sourceFields={sourceFields}
@@ -556,6 +587,7 @@ export const CreateConnectorModal = ({
isCreating ||
isImporting ||
!connectorName.trim() ||
!selectedDirectoryId ||
getCreateDisabled(selectedType, !!isFormbricksValid, isCsvValid, allRequiredMapped)
}>
{isCreating && <Loader2Icon className="mr-2 h-4 w-4 animate-spin" />}
@@ -568,3 +600,53 @@ export const CreateConnectorModal = ({
</>
);
};
interface FrdPickerProps {
directories: { id: string; name: string }[];
selectedDirectoryId: string | null;
onChange: (id: string) => void;
workspaceId: string;
t: (key: string) => string;
}
const FrdPicker = ({ directories, selectedDirectoryId, onChange, workspaceId, t }: FrdPickerProps) => {
if (directories.length === 0) {
return (
<Alert variant="error" size="small">
<div>
<p>{t("workspace.unify.no_feedback_record_directory_available")}</p>
<a
className="mt-1 inline-block font-medium underline"
href={`/workspaces/${workspaceId}/settings/feedback-record-directories`}>
{t("workspace.unify.go_to_feedback_record_directories")}
</a>
</div>
</Alert>
);
}
if (directories.length === 1) {
return (
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
{t("workspace.unify.records_will_go_to")}{" "}
<span className="font-medium text-slate-900">{directories[0].name}</span>
</div>
);
}
return (
<div className="space-y-2">
<Label htmlFor="feedbackRecordDirectory">{t("workspace.unify.feedback_record_directory")}</Label>
<Select value={selectedDirectoryId ?? ""} onValueChange={onChange}>
<SelectTrigger id="feedbackRecordDirectory">
<SelectValue placeholder={t("workspace.unify.select_feedback_record_directory")} />
</SelectTrigger>
<SelectContent>
{directories.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -38,6 +38,7 @@ interface EditConnectorModalProps {
fieldMappings?: TFieldMapping[];
}) => Promise<void>;
surveys: TUnifySurvey[];
directories: { id: string; name: string }[];
onOpenCsvImport?: () => void;
}
@@ -80,6 +81,7 @@ export const EditConnectorModal = ({
onOpenChange,
onUpdateConnector,
surveys,
directories,
onOpenCsvImport,
}: EditConnectorModalProps) => {
const { t } = useTranslation();
@@ -202,6 +204,11 @@ export const EditConnectorModal = ({
handleOpenChange(false);
};
const assignedDirectoryName =
directories.find((d) => d.id === connector?.feedbackRecordDirectoryId)?.name ??
connector?.feedbackRecordDirectoryId ??
"—";
const saveChangesDisbaled = useMemo(() => {
if (!connector) return true;
if (!connectorName.trim()) return true;
@@ -246,6 +253,12 @@ export const EditConnectorModal = ({
/>
</div>
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
{t("workspace.unify.records_will_go_to")}{" "}
<span className="font-medium text-slate-900">{assignedDirectoryName}</span>
<p className="mt-1 text-xs text-slate-400">{t("workspace.unify.frd_cannot_be_changed")}</p>
</div>
{connector.type === "formbricks" ? (
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<FormbricksSurveySelector
@@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { getSurveys } from "@/lib/survey/service";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { ConnectorsSection } from "./components/connectors-page-client";
import { transformToUnifySurvey } from "./lib";
@@ -22,9 +23,10 @@ export default async function UnifySourcesPage(props: { params: Promise<{ worksp
return notFound();
}
const [connectors, surveys] = await Promise.all([
const [connectors, surveys, directories] = await Promise.all([
getConnectorsWithMappings(params.workspaceId),
getSurveys(params.workspaceId),
getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId),
]);
const unifySurveys = surveys.map(transformToUnifySurvey);
@@ -34,6 +36,7 @@ export default async function UnifySourcesPage(props: { params: Promise<{ worksp
workspaceId={params.workspaceId}
initialConnectors={connectors}
initialSurveys={unifySurveys}
directories={directories}
/>
);
}
+13 -3
View File
@@ -159,6 +159,7 @@ checksums:
common/count_questions: a7a34376a01eda781381fe7544541293
common/count_responses: 437e022825c7a08481d8f7e56926742d
common/count_selections: a1ec41682b9a7d8601c3905dfba34e16
common/create: 757ccd28dd533ff3a933355273c1e32a
common/create_new_organization: 51dae7b33143686ee218abf5bea764a5
common/create_segment: 9d8291cd4d778b53b73bbc84fd91c181
common/create_survey: 1cfbba08d34876566d84b2960054a987
@@ -442,6 +443,7 @@ checksums:
common/variables: ffd3eec5497af36d7b4e4185bad1313a
common/verified_email: d4a9e5e47d622c6ef2fede44233076c7
common/video: 8050c90e4289b105a0780f0fdda6ff66
common/view: 36a9b5e3dc153c036d320460d72a03c3
common/warning: 6618da2c7e5e93bb4ea0e16d29ab8c4c
common/we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable: f29f2e0286195dab170b9806bcd74fc9
common/webhook: 70f95b2c27f2c3840b500fcaf79ee83c
@@ -2213,6 +2215,7 @@ checksums:
workspace/settings/feedback_record_directories/archive_not_allowed: 3ffe3336572a633406858887de60a470
workspace/settings/feedback_record_directories/are_you_sure_you_want_to_archive: d249e6e8bc0345835a13f70856eb1c30
workspace/settings/feedback_record_directories/assign_workspaces_description: 6c3f0bbf3bd7744bb313f4cd7886e184
workspace/settings/feedback_record_directories/connectors_description: 6efec0b94291db18124e8bfb1ced7e89
workspace/settings/feedback_record_directories/create_feedback_directory: c178dd6dbd702398df3ac08a9fa43324
workspace/settings/feedback_record_directories/description: 8f56b169cb38d8c7b2697bf3a3ed7a61
workspace/settings/feedback_record_directories/directory_archived_successfully: fba5b99ced59d0546c8f2241c092a5dd
@@ -2224,12 +2227,13 @@ checksums:
workspace/settings/feedback_record_directories/directory_unarchived_successfully: 08d56e260decc62fe664b50ab774b728
workspace/settings/feedback_record_directories/directory_updated_successfully: 638cb6c92f535328d809274cf2be4d7d
workspace/settings/feedback_record_directories/empty_state: 665593dcb7cfa081a3e719677d0f6b0d
workspace/settings/feedback_record_directories/enter_directory_name: a1c950988199bb4c4e014dcf430cce41
workspace/settings/feedback_record_directories/error_directory_has_connectors: 792ca3a69d639f4fb602dd72daf5a806
workspace/settings/feedback_record_directories/error_directory_name_duplicate: 349d650f562cff96b084787126323ca2
workspace/settings/feedback_record_directories/error_directory_name_required: 0f42d7292979006a1069063ab213b8e3
workspace/settings/feedback_record_directories/error_directory_workspaces_invalid_org: 477b5c1a466c4194668544ffd42ec9bf
workspace/settings/feedback_record_directories/nav_label: cf9a57b3cbac0f04b98e06fb693e986e
workspace/settings/feedback_record_directories/no_access: cc3385cd01a11e3949003a2cc6fb5b31
workspace/settings/feedback_record_directories/no_connectors: b1becb4fe4e2ba7c5d277db149f092ff
workspace/settings/feedback_record_directories/select_workspaces_placeholder: 7d8c8f5910b264525f73bd32107765db
workspace/settings/feedback_record_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de
workspace/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f
@@ -3236,6 +3240,7 @@ checksums:
workspace/unify/connector_duplicated_successfully: eb21ce42cdbef5fa38244206bf65fe4e
workspace/unify/connector_status_updated_successfully: 443fd63b27f15a81ff146375adac739f
workspace/unify/connector_updated_successfully: 11308c4a2881345209cefa06a3d90eab
workspace/unify/connectors: 4d6f256254573013a8714c2afe98dcc2
workspace/unify/create_mapping: cbe8c951e7819f574ca7d793920b2b60
workspace/unify/created_by: 6775c2fa7d495fea48f1ad816daea93b
workspace/unify/csv_at_least_one_row: 165bbc1853dde85c44eb5a587c52ce28
@@ -3260,12 +3265,15 @@ checksums:
workspace/unify/enum: 96fc644f35edd6b1c09d1d503f078acc
workspace/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8
workspace/unify/feedback_date: ddba5d3270d4a6394d29721025a04400
workspace/unify/feedback_record_directory: 89a08a540d1c6eb9f0b1a4b8f56e8aca
workspace/unify/feedback_record_fields: 88c0f13afeb88fe751f85e79b0f73064
workspace/unify/feedback_records: e24cf48bb6985910f4ffe5e00512d388
workspace/unify/feedback_records_refreshed: 4b27a8e2a8dbe8afa945d9f874aa7ef1
workspace/unify/field_label: 6384505ca0e40010c666b712511132a6
workspace/unify/field_type: 2581066dc304c853a4a817c20996fa08
workspace/unify/formbricks_surveys: eba2fce04ee68f02626e5509adf7d66a
workspace/unify/frd_cannot_be_changed: 265c12529f540d8309811f4e0090272f
workspace/unify/go_to_feedback_record_directories: 16b66b62f85e7be311778f39315d118a
workspace/unify/historical_import_complete: f46f98bf4db63bf2993bfb234dc95f62
workspace/unify/import_csv_data: f05e1d1ed88d528256efe5702df46646
workspace/unify/import_feedback: f05e1d1ed88d528256efe5702df46646
@@ -3274,9 +3282,9 @@ checksums:
workspace/unify/importing_historical_data: f5be578704ec26dc4ec573309e9fff20
workspace/unify/invalid_enum_values: e6ca8740dab72f64e8dc5780b5cffcc6
workspace/unify/invalid_values_found: 5011dc9c0294a222033f9910ea919b8a
workspace/unify/load_more: 365c2d8dfc53ac7e9188acd5274e2837
workspace/unify/load_sample_csv: ad21fa63f4a3df96a5939c753be21f4e
workspace/unify/n_supported_questions: d75413d386441b5eb137a1ea191e4bd9
workspace/unify/no_feedback_record_directory_available: b8126ef5d6276d9655a9b27ffcaca824
workspace/unify/no_feedback_records: 16a905c40f6d47a5e8f93b3d8c6f6693
workspace/unify/no_source_fields_loaded: a597b1d16262cbe897001046eb3ff640
workspace/unify/no_sources_connected: 0e8a5612530bfc82091091f40f95012f
@@ -3286,6 +3294,7 @@ checksums:
workspace/unify/question_selected: b9ff13b6212874258da911867932dc7d
workspace/unify/question_type_not_supported: 8d9f7554e3b509dfd5307d8d1fef08d7
workspace/unify/questions_selected: 1f13d6fecafa2ce5ea9e6d07078a1d38
workspace/unify/records_will_go_to: 6a3f5a6580857a931bab389ad354831c
workspace/unify/refresh_feedback_records: c111751e02a7dee57390ed7fb79cfcc6
workspace/unify/refreshing_feedback_records: 2a03b44510ebe19eea6473639e9a7222
workspace/unify/required: 04d7fb6f37ffe0a6ca97d49e2a8b6eb5
@@ -3293,6 +3302,7 @@ checksums:
workspace/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20
workspace/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862
workspace/unify/select_all: eedc7cdb02de467c15dc418a066a77f2
workspace/unify/select_feedback_record_directory: 88afbf2c2a322249908ee5d00ec5f65d
workspace/unify/select_questions: 13c79b8c284423eb6140534bf2137e56
workspace/unify/select_source_type_description: fd7e3c49b81f8e89f294c8fd94efcdfc
workspace/unify/select_source_type_prompt: c3fce7d908ee62b9e1b7fab1b17606d7
@@ -3301,7 +3311,7 @@ checksums:
workspace/unify/select_survey_questions_description: 3386ed56085eabebefa3cc453269fc5b
workspace/unify/set_value: b8a86f8da957ebd599ece4b1b1936a78
workspace/unify/setup_connection: cce7d9c488d737d04e70bed929a46f8a
workspace/unify/showing_count: 20675071b78443b250ab13b11138f30d
workspace/unify/showing_count_loaded: f443aae08223b65fbd5521d6e69534a4
workspace/unify/showing_rows: 83d3440314d1e6f2721e034369a3a131
workspace/unify/source: 45309626f464f4bda161ee783a4c8c80
workspace/unify/source_connect_csv_description: 2f9d1dd31668ac52578f16323157b746
+76 -63
View File
@@ -1,6 +1,7 @@
"use server";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import {
@@ -23,7 +24,7 @@ import {
getOrganizationIdFromSurveyId,
getOrganizationIdFromWorkspaceId,
} from "@/lib/utils/helper";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { listFeedbackRecords } from "@/modules/hub/service";
import type { FeedbackRecordListParams, FeedbackRecordListResponse } from "@/modules/hub/types";
import { importCsvData } from "./csv-import";
@@ -43,7 +44,7 @@ const ZDeleteConnectorAction = z.object({
});
export const deleteConnectorAction = authenticatedActionClient
.schema(ZDeleteConnectorAction)
.inputSchema(ZDeleteConnectorAction)
.action(
async ({
ctx,
@@ -126,7 +127,7 @@ const ZCreateConnectorWithMappingsAction = z
if (data.connectorInput.type === "formbricks") {
if (!data.formbricksMappings?.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
code: "custom",
path: ["formbricksMappings"],
message: "At least one survey mapping is required for Formbricks connectors",
});
@@ -134,7 +135,7 @@ const ZCreateConnectorWithMappingsAction = z
} else if (data.connectorInput.type === "csv") {
if (!data.fieldMappings?.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
code: "custom",
path: ["fieldMappings"],
message: "At least one field mapping is required for CSV connectors",
});
@@ -143,58 +144,59 @@ const ZCreateConnectorWithMappingsAction = z
});
export const createConnectorWithMappingsAction = authenticatedActionClient
.schema(ZCreateConnectorWithMappingsAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZCreateConnectorWithMappingsAction>;
}): Promise<TConnectorWithMappings> => {
const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
minPermission: "readWrite",
workspaceId: parsedInput.workspaceId,
},
],
});
.inputSchema(ZCreateConnectorWithMappingsAction)
.action(async ({ ctx, parsedInput }): Promise<TConnectorWithMappings> => {
const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
minPermission: "readWrite",
workspaceId: parsedInput.workspaceId,
},
],
});
let mappingsInput: TMappingsInput | undefined;
const { formbricksMappings, fieldMappings } = parsedInput;
if (formbricksMappings?.length) {
await Promise.all(
formbricksMappings.map(async ({ surveyId }) => {
const orgId = await getOrganizationIdFromSurveyId(surveyId);
if (orgId !== organizationId) {
throw new AuthorizationError("You are not authorized to access this survey");
}
})
);
mappingsInput = await resolveFormbricksMappingsInput(formbricksMappings);
} else if (fieldMappings?.length) {
mappingsInput = { type: "field", mappings: fieldMappings };
}
return createConnectorWithMappings(
parsedInput.workspaceId,
{ ...parsedInput.connectorInput, createdBy: ctx.user.id },
mappingsInput
);
// Verify FRD belongs to same org
const frd = await prisma.feedbackRecordDirectory.findUnique({
where: { id: parsedInput.connectorInput.feedbackRecordDirectoryId },
select: { organizationId: true },
});
if (frd?.organizationId !== organizationId) {
throw new AuthorizationError("Invalid feedback record directory");
}
);
let mappingsInput: TMappingsInput | undefined;
const { formbricksMappings, fieldMappings } = parsedInput;
if (formbricksMappings?.length) {
await Promise.all(
formbricksMappings.map(async ({ surveyId }) => {
const orgId = await getOrganizationIdFromSurveyId(surveyId);
if (orgId !== organizationId) {
throw new AuthorizationError("You are not authorized to access this survey");
}
})
);
mappingsInput = await resolveFormbricksMappingsInput(formbricksMappings);
} else if (fieldMappings?.length) {
mappingsInput = { type: "field", mappings: fieldMappings };
}
return createConnectorWithMappings(
parsedInput.workspaceId,
{ ...parsedInput.connectorInput, createdBy: ctx.user.id },
mappingsInput
);
});
const ZUpdateConnectorWithMappingsAction = z.object({
connectorId: ZId,
@@ -205,7 +207,7 @@ const ZUpdateConnectorWithMappingsAction = z.object({
});
export const updateConnectorWithMappingsAction = authenticatedActionClient
.schema(ZUpdateConnectorWithMappingsAction)
.inputSchema(ZUpdateConnectorWithMappingsAction)
.action(
async ({
ctx,
@@ -263,7 +265,7 @@ const ZDuplicateConnectorAction = z.object({
});
export const duplicateConnectorAction = authenticatedActionClient
.schema(ZDuplicateConnectorAction)
.inputSchema(ZDuplicateConnectorAction)
.action(
async ({
ctx,
@@ -319,7 +321,12 @@ export const duplicateConnectorAction = authenticatedActionClient
return createConnectorWithMappings(
parsedInput.workspaceId,
{ name: `${source.name} (copy)`, type: source.type, createdBy: ctx.user.id },
{
name: `${source.name} (copy)`,
type: source.type,
feedbackRecordDirectoryId: source.feedbackRecordDirectoryId,
createdBy: ctx.user.id,
},
mappingsInput
);
}
@@ -331,7 +338,7 @@ const ZGetResponseCountAction = z.object({
});
export const getResponseCountAction = authenticatedActionClient
.schema(ZGetResponseCountAction)
.inputSchema(ZGetResponseCountAction)
.action(
async ({
ctx,
@@ -368,7 +375,7 @@ const ZImportHistoricalResponsesAction = z.object({
});
export const importHistoricalResponsesAction = authenticatedActionClient
.schema(ZImportHistoricalResponsesAction)
.inputSchema(ZImportHistoricalResponsesAction)
.action(
async ({
ctx,
@@ -415,7 +422,7 @@ const ZImportCsvDataAction = z.object({
});
export const importCsvDataAction = authenticatedActionClient
.schema(ZImportCsvDataAction)
.inputSchema(ZImportCsvDataAction)
.action(
async ({
ctx,
@@ -460,6 +467,7 @@ export const importCsvDataAction = authenticatedActionClient
const ZListFeedbackRecordsAction = z.object({
workspaceId: ZId,
frdId: ZId,
limit: z.number().min(1).max(1000).optional(),
cursor: z.string().optional(),
sourceType: z.string().optional(),
@@ -497,8 +505,14 @@ export const listFeedbackRecordsAction = authenticatedActionClient
],
});
// Verify FRD belongs to workspace's accessible FRDs
const frds = await getFeedbackRecordDirectoriesByWorkspaceId(parsedInput.workspaceId);
if (!frds.some((f) => f.id === parsedInput.frdId)) {
throw new Error("Feedback record directory not accessible");
}
const params: FeedbackRecordListParams = {
tenant_id: parsedInput.workspaceId,
tenant_id: parsedInput.frdId,
limit: parsedInput.limit ?? 50,
};
if (parsedInput.cursor) params.cursor = parsedInput.cursor;
@@ -510,8 +524,7 @@ export const listFeedbackRecordsAction = authenticatedActionClient
const result = await listFeedbackRecords(params);
if (result.error || !result.data) {
logger.warn({ error: result.error }, "Failed to list feedback records");
const t = await getTranslate();
throw new Error(result.error?.message ?? t("workspace.unify.failed_to_load_feedback_records"));
throw new Error(result.error?.message ?? "Failed to load feedback records");
}
return result.data;
+1 -1
View File
@@ -22,7 +22,7 @@ export const importCsvData = async (
const { records, skipped } = transformCsvRowsToFeedbackRecords(
csvRows,
connector.fieldMappings,
connector.workspaceId
connector.feedbackRecordDirectoryId
);
let successes = 0;
+6 -1
View File
@@ -50,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, connector.workspaceId);
const batch = await processBatch(
responses,
survey,
connector.formbricksMappings,
connector.feedbackRecordDirectoryId
);
successes += batch.successes;
failures += batch.failures;
skipped += batch.skipped;
@@ -3,6 +3,8 @@ import { TConnectorWithMappings } from "@formbricks/types/connector";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
vi.mock("server-only", () => ({}));
const mockCreateFeedbackRecordsBatch = vi.fn();
vi.mock("@/modules/hub", () => ({
@@ -54,6 +56,7 @@ function createConnector(
type: "formbricks",
status: "active",
workspaceId: "env-1",
feedbackRecordDirectoryId: "frd-1",
lastSyncAt: null,
formbricksMappings: [
{
@@ -117,7 +120,7 @@ describe("handleConnectorPipeline", () => {
mockResponse,
mockSurvey,
connector.formbricksMappings,
"env-1"
"frd-1"
);
expect(mockCreateFeedbackRecordsBatch).not.toHaveBeenCalled();
expect(updateConnector).not.toHaveBeenCalled();
+1 -1
View File
@@ -41,7 +41,7 @@ const processConnector = async (
response,
survey,
connector.formbricksMappings,
workspaceId
connector.feedbackRecordDirectoryId
);
if (feedbackRecords.length === 0) {
+19 -10
View File
@@ -39,6 +39,7 @@ vi.mock("@/lib/utils/validate", () => ({
const ENV_ID = "clxxxxxxxxxxxxxxxx001";
const CONNECTOR_ID = "clxxxxxxxxxxxxxxxx002";
const SURVEY_ID = "clxxxxxxxxxxxxxxxx003";
const FRD_ID = "clxxxxxxxxxxxxxxxx004";
const NOW = new Date("2026-02-24T10:00:00.000Z");
const mockConnector = {
@@ -300,11 +301,15 @@ describe("createConnectorWithMappings", () => {
tx.connector.create.mockResolvedValue({ id: CONNECTOR_ID, workspaceId: ENV_ID });
tx.connector.findUniqueOrThrow.mockResolvedValue(mockConnectorWithMappingsFromDb);
const result = await createConnectorWithMappings(ENV_ID, { name: "New", type: "formbricks" });
const result = await createConnectorWithMappings(ENV_ID, {
name: "New",
type: "formbricks",
feedbackRecordDirectoryId: FRD_ID,
});
expect(tx.connector.create).toHaveBeenCalledWith(
expect.objectContaining({
data: { name: "New", type: "formbricks", workspaceId: ENV_ID },
data: { name: "New", type: "formbricks", workspaceId: ENV_ID, feedbackRecordDirectoryId: FRD_ID },
})
);
expect(tx.connectorFormbricksMapping.create).not.toHaveBeenCalled();
@@ -320,7 +325,7 @@ describe("createConnectorWithMappings", () => {
await createConnectorWithMappings(
ENV_ID,
{ name: "FB", type: "formbricks" },
{ name: "FB", type: "formbricks", feedbackRecordDirectoryId: FRD_ID },
{
type: "formbricks",
mappings: [
@@ -356,7 +361,7 @@ describe("createConnectorWithMappings", () => {
await createConnectorWithMappings(
ENV_ID,
{ name: "CSV", type: "csv" },
{ name: "CSV", type: "csv", feedbackRecordDirectoryId: FRD_ID },
{
type: "field",
mappings: [{ sourceFieldId: "col-1", targetFieldId: "value_text" }],
@@ -384,9 +389,13 @@ describe("createConnectorWithMappings", () => {
})
);
await expect(createConnectorWithMappings(ENV_ID, { name: "Dup", type: "formbricks" })).rejects.toThrow(
InvalidInputError
);
await expect(
createConnectorWithMappings(ENV_ID, {
name: "Dup",
type: "formbricks",
feedbackRecordDirectoryId: FRD_ID,
})
).rejects.toThrow(InvalidInputError);
});
test("throws DatabaseError on generic Prisma error", async () => {
@@ -397,9 +406,9 @@ describe("createConnectorWithMappings", () => {
})
);
await expect(createConnectorWithMappings(ENV_ID, { name: "Fail", type: "csv" })).rejects.toThrow(
DatabaseError
);
await expect(
createConnectorWithMappings(ENV_ID, { name: "Fail", type: "csv", feedbackRecordDirectoryId: FRD_ID })
).rejects.toThrow(DatabaseError);
});
});
+3
View File
@@ -26,6 +26,7 @@ const selectConnectorWithMappings = {
type: true,
status: true,
workspaceId: true,
feedbackRecordDirectoryId: true,
lastSyncAt: true,
createdBy: true,
creator: { select: { name: true } },
@@ -62,6 +63,7 @@ const selectConnector = {
type: true,
status: true,
workspaceId: true,
feedbackRecordDirectoryId: true,
lastSyncAt: true,
createdBy: true,
} satisfies Prisma.ConnectorSelect;
@@ -236,6 +238,7 @@ export const createConnectorWithMappings = async (
name: data.name,
type: data.type,
workspaceId,
feedbackRecordDirectoryId: data.feedbackRecordDirectoryId,
createdBy: data.createdBy,
},
});
+7 -92
View File
@@ -22,6 +22,7 @@ import {
getWorkspaceIdFromContactId,
getWorkspaceIdFromIntegrationId,
getWorkspaceIdFromLanguageId,
getWorkspaceIdFromQuotaId,
getWorkspaceIdFromResponseId,
getWorkspaceIdFromSegmentId,
getWorkspaceIdFromSurveyId,
@@ -328,22 +329,18 @@ describe("Helper Utilities", () => {
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromConnectorId returns organization ID through environment and project", async () => {
test("getOrganizationIdFromConnectorId returns organization ID through workspace", async () => {
vi.mocked(services.getConnector).mockResolvedValueOnce({
environmentId: "env1",
workspaceId: "workspace1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
vi.mocked(services.getWorkspace).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromConnectorId("connector1");
expect(orgId).toBe("org1");
expect(services.getConnector).toHaveBeenCalledWith("connector1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
expect(services.getProject).toHaveBeenCalledWith("project1");
expect(services.getWorkspace).toHaveBeenCalledWith("workspace1");
});
test("getOrganizationIdFromConnectorId throws error when connector not found", async () => {
@@ -493,90 +490,8 @@ describe("Helper Utilities", () => {
workspaceId: "workspace1",
});
const projectId = await getProjectIdFromQuotaId("quota1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromConnectorId returns project ID through environment", async () => {
vi.mocked(services.getConnector).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromConnectorId("connector1");
expect(projectId).toBe("project1");
expect(services.getConnector).toHaveBeenCalledWith("connector1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
});
test("getProjectIdFromConnectorId throws error when connector not found", async () => {
vi.mocked(services.getConnector).mockResolvedValueOnce(null);
await expect(getProjectIdFromConnectorId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
expect(services.getConnector).toHaveBeenCalledWith("nonexistent");
});
});
describe("Environment ID retrieval functions", () => {
test("getEnvironmentIdFromSurveyId returns environment ID directly", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromSurveyId("survey1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromSurveyId throws error when survey not found", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromSurveyId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromResponseId returns environment ID correctly", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromResponseId("response1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromResponseId throws error when response not found", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromSegmentId returns environment ID directly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromSegmentId("segment1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromSegmentId throws error when segment not found", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromSegmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromTagId returns environment ID directly", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromTagId("tag1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromTagId throws error when tag not found", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromTagId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
const workspaceId = await getWorkspaceIdFromQuotaId("quota1");
expect(workspaceId).toBe("workspace1");
});
});
+2 -2
View File
@@ -565,14 +565,14 @@ describe("Service Functions", () => {
const connectorId = "connector123";
test("returns the connector when found", async () => {
const mockConnector = { environmentId: "env123" };
const mockConnector = { workspaceId: "ws123" };
vi.mocked(prisma.connector.findUnique).mockResolvedValue(mockConnector);
const result = await getConnector(connectorId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.connector.findUnique).toHaveBeenCalledWith({
where: { id: connectorId },
select: { environmentId: true },
select: { workspaceId: true },
});
expect(result).toEqual(mockConnector);
});
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} Frage} other {{count} Fragen}}",
"count_responses": "{count, plural, one {{count} Antwort} other {{count} Antworten}}",
"count_selections": "{count, plural, one {{count} Auswahl} other {{count} Auswahlmöglichkeiten}}",
"create": "Erstellen",
"create_new_organization": "Neue Organisation erstellen",
"create_segment": "Segment erstellen",
"create_survey": "Umfrage erstellen",
@@ -469,6 +470,7 @@
"variables": "Variablen",
"verified_email": "Verifizierte E-Mail",
"video": "Video",
"view": "Ansehen",
"warning": "Warnung",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Wir konnten deine Lizenz nicht verifizieren, da der Lizenzserver nicht erreichbar ist.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Du darfst dieses Verzeichnis nicht archivieren.",
"are_you_sure_you_want_to_archive": "Bist du sicher, dass du dieses Verzeichnis archivieren möchtest? Workspaces haben dann keinen Zugriff mehr darauf.",
"assign_workspaces_description": "Lege fest, welche Workspaces auf dieses Feedback-Datensatz-Verzeichnis zugreifen können.",
"connectors_description": "Connectoren, die Feedback-Datensätze an dieses Verzeichnis senden.",
"create_feedback_directory": "Feedback-Verzeichnis erstellen",
"description": "Verwalte Feedback-Datensatz-Verzeichnisse und ihre Workspace-Zuordnungen.",
"directory_archived_successfully": "Verzeichnis erfolgreich archiviert",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Verzeichnis erfolgreich wiederhergestellt",
"directory_updated_successfully": "Verzeichnis erfolgreich aktualisiert",
"empty_state": "Keine Feedback-Datensatz-Verzeichnisse gefunden. Erstelle eins, um loszulegen.",
"enter_directory_name": "Verzeichnisnamen eingeben",
"error_directory_has_connectors": "Ein Verzeichnis mit verknüpften Connectoren kann nicht archiviert werden. Entferne zuerst alle Connectoren.",
"error_directory_name_duplicate": "Ein Feedback-Datensatz-Verzeichnis mit diesem Namen existiert bereits.",
"error_directory_name_required": "Verzeichnisname ist erforderlich.",
"error_directory_workspaces_invalid_org": "Einige der angegebenen Workspaces gehören nicht zu dieser Organisation.",
"nav_label": "Feedback-Verzeichnisse",
"no_access": "Du hast keine Berechtigung, Feedback-Datensatz-Verzeichnisse zu verwalten.",
"no_connectors": "Noch keine Connectoren mit diesem Verzeichnis verknüpft.",
"select_workspaces_placeholder": "Workspaces auswählen...",
"show_archived": "Archivierte anzeigen",
"title": "Feedback-Datensatz-Verzeichnisse",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Connector erfolgreich dupliziert",
"connector_status_updated_successfully": "Connector-Status erfolgreich aktualisiert",
"connector_updated_successfully": "Connector erfolgreich aktualisiert",
"connectors": "Connectoren",
"create_mapping": "Zuordnung erstellen",
"created_by": "Erstellt von",
"csv_at_least_one_row": "Die CSV-Datei muss mindestens eine Datenzeile enthalten.",
@@ -3414,12 +3419,15 @@
"enum": "Aufzählung",
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
"feedback_date": "Aktuelles Datum",
"feedback_record_directory": "Feedback-Datensatz-Verzeichnis",
"feedback_record_fields": "Feedback-Eintragsfelder",
"feedback_records": "Feedback-Einträge",
"feedback_records_refreshed": "Feedback-Einträge aktualisiert",
"field_label": "Feldbezeichnung",
"field_type": "Feldtyp",
"formbricks_surveys": "Formbricks-Umfragen",
"frd_cannot_be_changed": "Das Feedback-Verzeichnis kann nach der Erstellung nicht mehr geändert werden.",
"go_to_feedback_record_directories": "Zu den Verzeichnis-Einstellungen",
"historical_import_complete": "Import abgeschlossen: {successes} erfolgreich, {failures} fehlgeschlagen, {skipped} übersprungen (keine Daten)",
"import_csv_data": "Feedback importieren",
"import_feedback": "Feedback importieren",
@@ -3428,9 +3436,9 @@
"importing_historical_data": "Historische Daten werden importiert...",
"invalid_enum_values": "Ungültige Werte in der Spalte, die {field} zugeordnet 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_record_directory_available": "Diesem Workspace ist kein Feedback-Datensatz-Verzeichnis zugewiesen. Erstelle oder weise zuerst eines zu.",
"no_feedback_records": "Noch keine Feedback-Einträge vorhanden. Einträge erscheinen hier, 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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> Frage ausgewählt. Jede Antwort auf diese Frage wird einen neuen Feedback-Eintrag erstellen.",
"question_type_not_supported": "Dieser Fragetyp wird nicht unterstützt",
"questions_selected": "<strong>{count}</strong> Fragen ausgewählt. Jede Antwort auf diese Fragen wird einen neuen Feedback-Eintrag erstellen.",
"records_will_go_to": "Datensätze gehen an",
"refresh_feedback_records": "Feedback-Einträge aktualisieren",
"refreshing_feedback_records": "Feedback-Einträge werden aktualisiert...",
"required": "Erforderlich",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Wähle eine Umfrage aus, um ihre Fragen zu sehen",
"select_a_value": "Wähle einen Wert aus...",
"select_all": "Alle auswählen",
"select_feedback_record_directory": "Verzeichnis auswählen",
"select_questions": "Fragen auswählen",
"select_source_type_description": "Wähle die Art der Feedback-Quelle aus, die Du verbinden möchtest.",
"select_source_type_prompt": "Wähle die Art der Feedback-Quelle aus, die Du verbinden möchtest:",
@@ -3455,7 +3465,7 @@
"select_survey_questions_description": "Wähle aus, welche Umfragefragen FeedbackRecords erstellen sollen.",
"set_value": "Wert festlegen",
"setup_connection": "Verbindung einrichten",
"showing_count": "{count} von {total} Einträgen werden angezeigt",
"showing_count_loaded": "{count} Datensätze werden angezeigt",
"showing_rows": "3 von {count} Zeilen werden angezeigt",
"source": "Quelle",
"source_connect_csv_description": "Feedback aus CSV-Dateien importieren",
+14 -4
View File
@@ -167,7 +167,7 @@
"code": "Code",
"collapse_rows": "Collapse rows",
"completed": "Completed",
"configuration": "Configuration",
"configuration": "Configure",
"confirm": "Confirm",
"connect": "Connect",
"connect_formbricks": "Connect Formbricks",
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
"count_responses": "{count, plural, one {{count} response} other {{count} responses}}",
"count_selections": "{count, plural, one {{count} selection} other {{count} selections}}",
"create": "Create",
"create_new_organization": "Create new organization",
"create_segment": "Create segment",
"create_survey": "Create survey",
@@ -469,6 +470,7 @@
"variables": "Variables",
"verified_email": "Verified Email",
"video": "Video",
"view": "View",
"warning": "Warning",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "We were unable to verify your license because the license server is unreachable.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "You are not allowed to archive this directory.",
"are_you_sure_you_want_to_archive": "Are you sure you want to archive this directory? Workspaces will no longer have access to it.",
"assign_workspaces_description": "Control which workspaces can access this feedback record directory.",
"connectors_description": "Connectors that send feedback records to this directory.",
"create_feedback_directory": "Create feedback directory",
"description": "Manage feedback record directories and their workspace assignments.",
"directory_archived_successfully": "Directory archived successfully",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Directory unarchived successfully",
"directory_updated_successfully": "Directory updated successfully",
"empty_state": "No feedback record directories found. Create one to get started.",
"enter_directory_name": "Enter directory name",
"error_directory_has_connectors": "Cannot archive a directory that has connectors linked to it. Remove all connectors first.",
"error_directory_name_duplicate": "A feedback record directory with this name already exists.",
"error_directory_name_required": "Directory name is required.",
"error_directory_workspaces_invalid_org": "Some specified workspaces do not belong to this organization.",
"nav_label": "Feedback Directories",
"no_access": "You do not have permission to manage feedback record directories.",
"no_connectors": "No connectors linked to this directory yet.",
"select_workspaces_placeholder": "Select workspaces...",
"show_archived": "Show archived",
"title": "Feedback Record Directories",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Connector duplicated successfully",
"connector_status_updated_successfully": "Connector status updated successfully",
"connector_updated_successfully": "Connector updated successfully",
"connectors": "Connectors",
"create_mapping": "Create mapping",
"created_by": "Created by",
"csv_at_least_one_row": "CSV must contain at least one data row.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Failed to load feedback records",
"feedback_date": "Current date",
"feedback_record_directory": "Feedback Record Directory",
"feedback_record_fields": "Feedback Record Fields",
"feedback_records": "Feedback Records",
"feedback_records_refreshed": "Feedback records refreshed",
"field_label": "Field Label",
"field_type": "Field Type",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedback directory cannot be changed after creation.",
"go_to_feedback_record_directories": "Go to directories settings",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
"import_csv_data": "Import feedback",
"import_feedback": "Import feedback",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "No feedback record directory assigned to this workspace. Create or assign one first.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> question selected. Each response to these questions will create a new Feedback Record.",
"question_type_not_supported": "This question type is not supported",
"questions_selected": "<strong>{count}</strong> questions selected. Each response to these questions will create a new Feedback Record.",
"records_will_go_to": "Records will go to",
"refresh_feedback_records": "Refresh feedback records",
"refreshing_feedback_records": "Refreshing feedback records...",
"required": "Required",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Select a survey to see its questions",
"select_a_value": "Select a value...",
"select_all": "Select all",
"select_feedback_record_directory": "Select a directory",
"select_questions": "Select questions",
"select_source_type_description": "Select the type of feedback source you want to connect.",
"select_source_type_prompt": "Select the type of feedback source you want to connect:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Showing {count} records",
"showing_rows": "Showing 3 of {count} rows",
"source": "source",
"source_connect_csv_description": "Import feedback from CSV files",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} pregunta} other {{count} preguntas}}",
"count_responses": "{count, plural, one {{count} respuesta} other {{count} respuestas}}",
"count_selections": "{count, plural, one {{count} selección} other {{count} selecciones}}",
"create": "Crear",
"create_new_organization": "Crear organización nueva",
"create_segment": "Crear segmento",
"create_survey": "Crear encuesta",
@@ -469,6 +470,7 @@
"variables": "Variables",
"verified_email": "Correo electrónico verificado",
"video": "Vídeo",
"view": "Ver",
"warning": "Advertencia",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "No pudimos verificar tu licencia porque el servidor de licencias no está accesible.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "No tienes permiso para archivar este directorio.",
"are_you_sure_you_want_to_archive": "¿Estás seguro de que quieres archivar este directorio? Los espacios de trabajo ya no tendrán acceso a él.",
"assign_workspaces_description": "Controla qué espacios de trabajo pueden acceder a este directorio de registros de feedback.",
"connectors_description": "Conectores que envían registros de comentarios a este directorio.",
"create_feedback_directory": "Crear directorio de comentarios",
"description": "Gestiona los directorios de registros de feedback y sus asignaciones de espacios de trabajo.",
"directory_archived_successfully": "Directorio archivado correctamente",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Directorio desarchivado correctamente",
"directory_updated_successfully": "Directorio actualizado correctamente",
"empty_state": "No se encontraron directorios de registros de feedback. Crea uno para empezar.",
"enter_directory_name": "Introduce el nombre del directorio",
"error_directory_has_connectors": "No se puede archivar un directorio que tiene conectores vinculados. Elimina primero todos los conectores.",
"error_directory_name_duplicate": "Ya existe un directorio de registros de comentarios con este nombre.",
"error_directory_name_required": "El nombre del directorio es obligatorio.",
"error_directory_workspaces_invalid_org": "Algunos de los espacios de trabajo especificados no pertenecen a esta organización.",
"nav_label": "Directorios de Feedback",
"no_access": "No tienes permiso para gestionar los directorios de registros de feedback.",
"no_connectors": "Aún no hay conectores vinculados a este directorio.",
"select_workspaces_placeholder": "Selecciona espacios de trabajo...",
"show_archived": "Mostrar archivados",
"title": "Directorios de Registros de Feedback",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Conector duplicado correctamente",
"connector_status_updated_successfully": "Estado del conector actualizado correctamente",
"connector_updated_successfully": "Conector actualizado correctamente",
"connectors": "Conectores",
"create_mapping": "Crear asignación",
"created_by": "Creado por",
"csv_at_least_one_row": "El CSV debe contener al menos una fila de datos.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Error al cargar los registros de comentarios",
"feedback_date": "Fecha actual",
"feedback_record_directory": "Directorio de Registros de Comentarios",
"feedback_record_fields": "Campos de registro de comentarios",
"feedback_records": "Registros de comentarios",
"feedback_records_refreshed": "Registros de comentarios actualizados",
"field_label": "Etiqueta de campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "El directorio de comentarios no se puede cambiar después de su creación.",
"go_to_feedback_record_directories": "Ir a la configuración de directorios",
"historical_import_complete": "Importación completada: {successes} correctas, {failures} fallidas, {skipped} omitidas (sin datos)",
"import_csv_data": "Importar comentarios",
"import_feedback": "Importar comentarios",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "No hay ningún directorio de registros de comentarios asignado a este espacio de trabajo. Crea o asigna uno primero.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> pregunta seleccionada. Cada respuesta a esta pregunta creará un registro de feedback nuevo.",
"question_type_not_supported": "Este tipo de pregunta no es compatible",
"questions_selected": "<strong>{count}</strong> preguntas seleccionadas. Cada respuesta a estas preguntas creará un registro de feedback nuevo.",
"records_will_go_to": "Los registros se enviarán a",
"refresh_feedback_records": "Actualizar los registros de comentarios",
"refreshing_feedback_records": "Actualizando registros de comentarios...",
"required": "Obligatorio",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas",
"select_a_value": "Selecciona un valor...",
"select_all": "Seleccionar todo",
"select_feedback_record_directory": "Selecciona un directorio",
"select_questions": "Seleccionar preguntas",
"select_source_type_description": "Selecciona el tipo de fuente de feedback que quieres conectar.",
"select_source_type_prompt": "Selecciona el tipo de fuente de feedback que quieres conectar:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Mostrando {count} registros",
"showing_rows": "Mostrando 3 de {count} filas",
"source": "origen",
"source_connect_csv_description": "Importar feedback desde archivos CSV",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
"count_responses": "{count, plural, one {{count} réponse} other {{count} réponses}}",
"count_selections": "{count, plural, one {{count} sélection} other {{count} sélections}}",
"create": "Créer",
"create_new_organization": "Créer une nouvelle organisation",
"create_segment": "Créer un segment",
"create_survey": "Créer un sondage",
@@ -469,6 +470,7 @@
"variables": "Variables",
"verified_email": "Email vérifié",
"video": "Vidéo",
"view": "Afficher",
"warning": "Avertissement",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Nous n'avons pas pu vérifier votre licence car le serveur de licence est inaccessible.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Vous n'êtes pas autorisé à archiver ce répertoire.",
"are_you_sure_you_want_to_archive": "Es-tu sûr de vouloir archiver ce répertoire ? Les espaces de travail n'y auront plus accès.",
"assign_workspaces_description": "Contrôle quels espaces de travail peuvent accéder à ce répertoire de feedback.",
"connectors_description": "Connecteurs qui envoient des enregistrements de retour d'expérience vers ce répertoire.",
"create_feedback_directory": "Créer un répertoire de commentaires",
"description": "Gère les répertoires de feedback et leurs affectations aux espaces de travail.",
"directory_archived_successfully": "Répertoire archivé avec succès",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Répertoire désarchivé avec succès",
"directory_updated_successfully": "Répertoire mis à jour avec succès",
"empty_state": "Aucun répertoire de feedback trouvé. Crée-en un pour commencer.",
"enter_directory_name": "Saisir le nom du répertoire",
"error_directory_has_connectors": "Impossible d'archiver un répertoire auquel des connecteurs sont liés. Supprimez d'abord tous les connecteurs.",
"error_directory_name_duplicate": "Un répertoire d'enregistrement de feedback avec ce nom existe déjà.",
"error_directory_name_required": "Le nom du répertoire est requis.",
"error_directory_workspaces_invalid_org": "Certains espaces de travail spécifiés n'appartiennent pas à cette organisation.",
"nav_label": "Répertoires de feedback",
"no_access": "Tu n'as pas la permission de gérer les répertoires de feedback.",
"no_connectors": "Aucun connecteur lié à ce répertoire pour le moment.",
"select_workspaces_placeholder": "Sélectionner des espaces de travail...",
"show_archived": "Afficher les éléments archivés",
"title": "Répertoires d'enregistrement des retours",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Connecteur dupliqué avec succès",
"connector_status_updated_successfully": "Statut du connecteur mis à jour avec succès",
"connector_updated_successfully": "Connecteur mis à jour avec succès",
"connectors": "Connecteurs",
"create_mapping": "Créer un mappage",
"created_by": "Créé par",
"csv_at_least_one_row": "Le CSV doit contenir au moins une ligne de données.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback",
"feedback_date": "Date actuelle",
"feedback_record_directory": "Répertoire d'enregistrements de retour d'expérience",
"feedback_record_fields": "Champs d'enregistrement de feedback",
"feedback_records": "Enregistrements de feedback",
"feedback_records_refreshed": "Enregistrements de feedback actualisés",
"field_label": "Libellé du champ",
"field_type": "Type de champ",
"formbricks_surveys": "Sondages Formbricks",
"frd_cannot_be_changed": "Le répertoire de retours d'expérience ne peut pas être modifié après sa création.",
"go_to_feedback_record_directories": "Accéder aux paramètres des répertoires",
"historical_import_complete": "Importation terminée: {successes} réussies, {failures} échouées, {skipped} ignorées (aucune donnée)",
"import_csv_data": "Importer les retours",
"import_feedback": "Importer les retours",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Aucun répertoire d'enregistrements de retour d'expérience n'est assigné à cet espace de travail. Créez-en un ou assignez-en un d'abord.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> question sélectionnée. Chaque réponse à cette question créera un nouvel enregistrement de feedback.",
"question_type_not_supported": "Ce type de question n'est pas pris en charge",
"questions_selected": "<strong>{count}</strong> questions sélectionnées. Chaque réponse à ces questions créera un nouvel enregistrement de feedback.",
"records_will_go_to": "Les enregistrements seront envoyés vers",
"refresh_feedback_records": "Actualiser les enregistrements de retours",
"refreshing_feedback_records": "Actualisation des enregistrements de feedback...",
"required": "Requis",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions",
"select_a_value": "Sélectionnez une valeur...",
"select_all": "Sélectionner tout",
"select_feedback_record_directory": "Sélectionner un répertoire",
"select_questions": "Sélectionner les questions",
"select_source_type_description": "Sélectionnez le type de source de feedback que vous souhaitez connecter.",
"select_source_type_prompt": "Sélectionnez le type de source de feedback que vous souhaitez connecter:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Affichage de {count} enregistrements",
"showing_rows": "Affichage de 3 sur {count} lignes",
"source": "source",
"source_connect_csv_description": "Importer des feedbacks depuis des fichiers CSV",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
"count_selections": "{count, plural, one {{count} kiválasztás} other {{count} kiválasztás}}",
"create": "Létrehozás",
"create_new_organization": "Új szervezet létrehozása",
"create_segment": "Szakasz létrehozása",
"create_survey": "Kérdőív létrehozása",
@@ -469,6 +470,7 @@
"variables": "Változók",
"verified_email": "Ellenőrzött e-mail-cím",
"video": "Videó",
"view": "Megtekintés",
"warning": "Figyelmeztetés",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Nem tudtuk ellenőrizni a licencét, mert a licenckiszolgáló nem érhető el.",
"webhook": "Webhorog",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Nem rendelkezik jogosultsággal ezen könyvtár archiválásához.",
"are_you_sure_you_want_to_archive": "Biztosan archiválni kívánja ezt a könyvtárat? A munkaterületek többé nem férhetnek hozzá.",
"assign_workspaces_description": "Szabályozza, mely munkaterületek férhetnek hozzá ehhez a visszajelzési nyilvántartási könyvtárhoz.",
"connectors_description": "Csatlakozók, amelyek visszajelzési rekordokat küldenek ebbe a könyvtárba.",
"create_feedback_directory": "Visszajelzési könyvtár létrehozása",
"description": "Visszajelzési nyilvántartási könyvtárak és munkaterület-hozzárendeléseik kezelése.",
"directory_archived_successfully": "A könyvtár sikeresen archiválva",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "A könyvtár archiválása sikeresen visszavonva",
"directory_updated_successfully": "A könyvtár sikeresen frissítve",
"empty_state": "Nem található visszajelzési nyilvántartási könyvtár. Hozzon létre egyet a kezdéshez.",
"enter_directory_name": "Adja meg a könyvtár nevét",
"error_directory_has_connectors": "Nem archiválható olyan könyvtár, amelyhez csatlakozók vannak társítva. Először távolítson el minden csatlakozót.",
"error_directory_name_duplicate": "Ezzel a névvel már létezik visszajelzési rekord könyvtár.",
"error_directory_name_required": "A könyvtár neve kötelező megadni.",
"error_directory_workspaces_invalid_org": "Egyes megadott munkaterületek nem ehhez a szervezethez tartoznak.",
"nav_label": "Visszajelzési könyvtárak",
"no_access": "Nem rendelkezik jogosultsággal a visszajelzési nyilvántartási könyvtárak kezeléséhez.",
"no_connectors": "Még nincsenek csatlakozók társítva ehhez a könyvtárhoz.",
"select_workspaces_placeholder": "Munkaterületek kiválasztása...",
"show_archived": "Archivált elemek megjelenítése",
"title": "Visszajelzési Nyilvántartási Könyvtárak",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Csatlakozó sikeresen duplikálva",
"connector_status_updated_successfully": "Csatlakozó állapota sikeresen frissítve",
"connector_updated_successfully": "Csatlakozó sikeresen frissítve",
"connectors": "Csatlakozók",
"create_mapping": "Leképezés létrehozása",
"created_by": "Létrehozta",
"csv_at_least_one_row": "A CSV-nek legalább egy adatsort kell tartalmaznia.",
@@ -3414,12 +3419,15 @@
"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_directory": "Visszajelzési Rekord Könyvtár",
"feedback_record_fields": "Visszajelzési rekord mezők",
"feedback_records": "Visszajelzési rekordok",
"feedback_records_refreshed": "Visszajelzési rekordok frissítve",
"field_label": "Mező címke",
"field_type": "Mező típus",
"formbricks_surveys": "Formbricks kérdőívek",
"frd_cannot_be_changed": "A visszajelzési könyvtár a létrehozás után nem módosítható.",
"go_to_feedback_record_directories": "Ugrás a könyvtárbeállításokhoz",
"historical_import_complete": "Importálás befejezve: {successes} sikeres, {failures} sikertelen, {skipped} kihagyva (nincs adat)",
"import_csv_data": "Visszajelzés importálása",
"import_feedback": "Visszajelzés importálása",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Ehhez a munkaterülethez nem tartozik visszajelzési rekord könyvtár. Először hozzon létre vagy rendeljen hozzá egyet.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
"question_type_not_supported": "Ez a kérdéstípus nem támogatott",
"questions_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
"records_will_go_to": "A rekordok ide kerülnek",
"refresh_feedback_records": "Visszajelzési rekordok frissítése",
"refreshing_feedback_records": "Visszajelzési rekordok frissítése...",
"required": "Kötelező",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Válassz egy kérdőívet a kérdések megtekintéséhez",
"select_a_value": "Válassz egy értéket...",
"select_all": "Összes kiválasztása",
"select_feedback_record_directory": "Válasszon egy könyvtárat",
"select_questions": "Kérdések kiválasztása",
"select_source_type_description": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát.",
"select_source_type_prompt": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "{count} 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",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, other {# 件の質問}}",
"count_responses": "{count, plural, other {{count} 件の回答}}",
"count_selections": "{count, plural, other {{count} 件の選択}}",
"create": "作成",
"create_new_organization": "新しい組織を作成",
"create_segment": "セグメントを作成",
"create_survey": "フォームを作成",
@@ -469,6 +470,7 @@
"variables": "変数",
"verified_email": "認証済みメールアドレス",
"video": "動画",
"view": "表示",
"warning": "警告",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "ライセンスサーバーにアクセスできないため、ライセンスを認証できませんでした。",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "このディレクトリをアーカイブする権限がありません。",
"are_you_sure_you_want_to_archive": "このディレクトリをアーカイブしてもよろしいですか?ワークスペースはアクセスできなくなります。",
"assign_workspaces_description": "このフィードバック記録ディレクトリにアクセスできるワークスペースを管理します。",
"connectors_description": "このディレクトリにフィードバックレコードを送信するコネクタ。",
"create_feedback_directory": "フィードバックディレクトリを作成",
"description": "フィードバック記録ディレクトリとワークスペースの割り当てを管理します。",
"directory_archived_successfully": "ディレクトリをアーカイブしました",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "ディレクトリのアーカイブを解除しました",
"directory_updated_successfully": "ディレクトリを更新しました",
"empty_state": "フィードバック記録ディレクトリが見つかりません。最初のディレクトリを作成してください。",
"enter_directory_name": "ディレクトリ名を入力してください",
"error_directory_has_connectors": "コネクタがリンクされているディレクトリはアーカイブできません。まずすべてのコネクタを削除してください",
"error_directory_name_duplicate": "この名前のフィードバック記録ディレクトリは既に存在します。",
"error_directory_name_required": "ディレクトリ名は必須です。",
"error_directory_workspaces_invalid_org": "指定されたワークスペースの一部がこの組織に属していません。",
"nav_label": "フィードバックディレクトリ",
"no_access": "フィードバック記録ディレクトリを管理する権限がありません。",
"no_connectors": "このディレクトリにリンクされているコネクタはまだありません。",
"select_workspaces_placeholder": "ワークスペースを選択...",
"show_archived": "アーカイブ済みを表示",
"title": "フィードバック記録ディレクトリ",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "コネクタが正常に複製されました",
"connector_status_updated_successfully": "コネクタのステータスが正常に更新されました",
"connector_updated_successfully": "コネクタが正常に更新されました",
"connectors": "コネクタ",
"create_mapping": "マッピングを作成",
"created_by": "作成者",
"csv_at_least_one_row": "CSVには少なくとも1行のデータが必要です。",
@@ -3414,12 +3419,15 @@
"enum": "列挙型",
"failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました",
"feedback_date": "現在の日付",
"feedback_record_directory": "フィードバックレコードディレクトリ",
"feedback_record_fields": "フィードバックレコードフィールド",
"feedback_records": "フィードバックレコード",
"feedback_records_refreshed": "フィードバックレコードを更新しました",
"field_label": "フィールドラベル",
"field_type": "フィールドタイプ",
"formbricks_surveys": "Formbricks フォーム",
"frd_cannot_be_changed": "フィードバックディレクトリは作成後に変更できません。",
"go_to_feedback_record_directories": "ディレクトリ設定へ移動",
"historical_import_complete": "インポート完了: {successes}件成功、{failures}件失敗、{skipped}件スキップ(データなし)",
"import_csv_data": "フィードバックをインポート",
"import_feedback": "フィードバックをインポート",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "このワークスペースにフィードバックレコードディレクトリが割り当てられていません。まず作成または割り当てを行ってください。",
"no_feedback_records": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。",
"no_source_fields_loaded": "ソースフィールドがまだ読み込まれていません",
"no_sources_connected": "ソースがまだ接続されていません。開始するにはソースを追加してください。",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
"question_type_not_supported": "この質問タイプはサポートされていません",
"questions_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
"records_will_go_to": "レコードの保存先",
"refresh_feedback_records": "フィードバック記録を更新",
"refreshing_feedback_records": "フィードバックレコードを更新中...",
"required": "必須",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "フォームを選択して質問を表示",
"select_a_value": "値を選択...",
"select_all": "すべて選択",
"select_feedback_record_directory": "ディレクトリを選択",
"select_questions": "質問を選択",
"select_source_type_description": "接続するフィードバックソースの種類を選択してください。",
"select_source_type_prompt": "接続するフィードバックソースの種類を選択してください:",
@@ -3455,7 +3465,7 @@
"select_survey_questions_description": "フィードバックレコードを作成するフォームの質問を選択してください。",
"set_value": "値を設定",
"setup_connection": "接続を設定",
"showing_count": "{total}件中{count}件を表示",
"showing_count_loaded": "{count}件のレコードを表示",
"showing_rows": "{count}行中3行を表示",
"source": "ソース",
"source_connect_csv_description": "CSVファイルからフィードバックをインポート",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} vraag} other {{count} vragen}}",
"count_responses": "{count, plural, one {{count} reactie} other {{count} reacties}}",
"count_selections": "{count, plural, one {{count} selectie} other {{count} selecties}}",
"create": "Aanmaken",
"create_new_organization": "Creëer een nieuwe organisatie",
"create_segment": "Segment maken",
"create_survey": "Enquête maken",
@@ -469,6 +470,7 @@
"variables": "Variabelen",
"verified_email": "Geverifieerde e-mail",
"video": "Video",
"view": "Weergeven",
"warning": "Waarschuwing",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "We kunnen uw licentie niet verifiëren omdat de licentieserver niet bereikbaar is.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Je hebt geen toestemming om deze map te archiveren.",
"are_you_sure_you_want_to_archive": "Weet je zeker dat je deze map wilt archiveren? Workspaces hebben er dan geen toegang meer toe.",
"assign_workspaces_description": "Bepaal welke workspaces toegang hebben tot deze feedbackregistratiemap.",
"connectors_description": "Connectoren die feedbackrecords naar deze map sturen.",
"create_feedback_directory": "Feedbackmap maken",
"description": "Beheer feedbackregistratiemappen en hun workspace-toewijzingen.",
"directory_archived_successfully": "Map succesvol gearchiveerd",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Map succesvol gedearchiveerd",
"directory_updated_successfully": "Map succesvol bijgewerkt",
"empty_state": "Geen feedbackregistratiemappen gevonden. Maak er een aan om te beginnen.",
"enter_directory_name": "Voer mapnaam in",
"error_directory_has_connectors": "Kan een map met gekoppelde connectoren niet archiveren. Verwijder eerst alle connectoren.",
"error_directory_name_duplicate": "Er bestaat al een feedback-recordmap met deze naam.",
"error_directory_name_required": "Mapnaam is verplicht.",
"error_directory_workspaces_invalid_org": "Sommige opgegeven werkruimtes behoren niet tot deze organisatie.",
"nav_label": "Feedbackmappen",
"no_access": "Je hebt geen toestemming om feedbackregistratiemappen te beheren.",
"no_connectors": "Nog geen connectoren gekoppeld aan deze map.",
"select_workspaces_placeholder": "Selecteer werkruimtes...",
"show_archived": "Gearchiveerde weergeven",
"title": "Feedbackregistratiemappen",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Connector succesvol gedupliceerd",
"connector_status_updated_successfully": "Connectorstatus succesvol bijgewerkt",
"connector_updated_successfully": "Connector succesvol bijgewerkt",
"connectors": "Connectoren",
"create_mapping": "Koppeling aanmaken",
"created_by": "Gemaakt door",
"csv_at_least_one_row": "CSV moet minimaal één datarij bevatten.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Kan feedbackrecords niet laden",
"feedback_date": "Huidige datum",
"feedback_record_directory": "Feedbackrecordmap",
"feedback_record_fields": "Feedbackrecordvelden",
"feedback_records": "Feedbackrecords",
"feedback_records_refreshed": "Feedbackrecords vernieuwd",
"field_label": "Veldlabel",
"field_type": "Veldtype",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedbackmap kan niet worden gewijzigd na aanmaak.",
"go_to_feedback_record_directories": "Ga naar map-instellingen",
"historical_import_complete": "Import voltooid: {successes} geslaagd, {failures} mislukt, {skipped} overgeslagen (geen data)",
"import_csv_data": "Feedback importeren",
"import_feedback": "Feedback importeren",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Geen feedbackrecordmap toegewezen aan deze workspace. Maak er eerst een aan of wijs er een toe.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> vraag geselecteerd. Elk antwoord op deze vraag zal een nieuw feedbackrecord aanmaken.",
"question_type_not_supported": "Dit vraagtype wordt niet ondersteund",
"questions_selected": "<strong>{count}</strong> vragen geselecteerd. Elk antwoord op deze vragen zal een nieuw feedbackrecord aanmaken.",
"records_will_go_to": "Records gaan naar",
"refresh_feedback_records": "Feedbackrecords verversen",
"refreshing_feedback_records": "Feedbackrecords vernieuwen...",
"required": "Vereist",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien",
"select_a_value": "Selecteer een waarde...",
"select_all": "Selecteer alles",
"select_feedback_record_directory": "Selecteer een map",
"select_questions": "Selecteer vragen",
"select_source_type_description": "Selecteer het type feedbackbron dat je wilt verbinden.",
"select_source_type_prompt": "Selecteer het type feedbackbron dat je wilt verbinden:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Er worden {count} records weergegeven",
"showing_rows": "3 van {count} rijen weergegeven",
"source": "bron",
"source_connect_csv_description": "Importeer feedback uit CSV-bestanden",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
"count_responses": "{count, plural, one {{count} resposta} other {{count} respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"create": "Criar",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar pesquisa",
@@ -469,6 +470,7 @@
"variables": "Variáveis",
"verified_email": "Email Verificado",
"video": "vídeo",
"view": "Visualizar",
"warning": "Aviso",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Não conseguimos verificar sua licença porque o servidor de licenças está inacessível.",
"webhook": "webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Você não tem permissão para arquivar este diretório.",
"are_you_sure_you_want_to_archive": "Tem certeza de que deseja arquivar este diretório? Os espaços de trabalho não terão mais acesso a ele.",
"assign_workspaces_description": "Controle quais espaços de trabalho podem acessar este diretório de registros de feedback.",
"connectors_description": "Conectores que enviam registros de feedback para este diretório.",
"create_feedback_directory": "Criar diretório de feedback",
"description": "Gerencie diretórios de registros de feedback e suas atribuições de espaços de trabalho.",
"directory_archived_successfully": "Diretório arquivado com sucesso",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Diretório desarquivado com sucesso",
"directory_updated_successfully": "Diretório atualizado com sucesso",
"empty_state": "Nenhum diretório de registros de feedback encontrado. Crie um para começar.",
"enter_directory_name": "Digite o nome do diretório",
"error_directory_has_connectors": "Não é possível arquivar um diretório que tem conectores vinculados a ele. Remova todos os conectores primeiro.",
"error_directory_name_duplicate": "Já existe um diretório de registros de feedback com este nome.",
"error_directory_name_required": "O nome do diretório é obrigatório.",
"error_directory_workspaces_invalid_org": "Alguns espaços de trabalho especificados não pertencem a esta organização.",
"nav_label": "Diretórios de Feedback",
"no_access": "Você não tem permissão para gerenciar diretórios de registros de feedback.",
"no_connectors": "Nenhum conector vinculado a este diretório ainda.",
"select_workspaces_placeholder": "Selecionar espaços de trabalho...",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registros de Feedback",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Conector duplicado com sucesso",
"connector_status_updated_successfully": "Status do conector atualizado com sucesso",
"connector_updated_successfully": "Conector atualizado com sucesso",
"connectors": "Conectores",
"create_mapping": "Criar mapeamento",
"created_by": "Criado por",
"csv_at_least_one_row": "O CSV deve conter pelo menos uma linha de dados.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registros de feedback",
"feedback_date": "Data atual",
"feedback_record_directory": "Diretório de Registros de Feedback",
"feedback_record_fields": "Campos do registro de feedback",
"feedback_records": "Registros de feedback",
"feedback_records_refreshed": "Registros de feedback atualizados",
"field_label": "Rótulo do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"frd_cannot_be_changed": "O diretório de feedback não pode ser alterado após a criação.",
"go_to_feedback_record_directories": "Ir para configurações de diretórios",
"historical_import_complete": "Importação concluída: {successes} bem-sucedidas, {failures} falharam, {skipped} ignoradas (sem dados)",
"import_csv_data": "Importar feedback",
"import_feedback": "Importar feedback",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Nenhum diretório de registros de feedback atribuído a este workspace. Crie ou atribua um primeiro.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registro de feedback.",
"question_type_not_supported": "Este tipo de pergunta não é suportado",
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registro de feedback.",
"records_will_go_to": "Os registros serão enviados para",
"refresh_feedback_records": "Atualizar registros de feedback",
"refreshing_feedback_records": "Atualizando registros de feedback...",
"required": "Obrigatório",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas",
"select_a_value": "Selecione um valor...",
"select_all": "Selecionar tudo",
"select_feedback_record_directory": "Selecione um diretório",
"select_questions": "Selecionar perguntas",
"select_source_type_description": "Selecione o tipo de fonte de feedback que você deseja conectar.",
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que você deseja conectar:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Mostrando {count} registros",
"showing_rows": "Mostrando 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de arquivos CSV",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
"count_responses": "{count, plural, one {{count} resposta} other {{count} respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"create": "Criar",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar inquérito",
@@ -469,6 +470,7 @@
"variables": "Variáveis",
"verified_email": "Email verificado",
"video": "Vídeo",
"view": "Ver",
"warning": "Aviso",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Não foi possível verificar a sua licença porque o servidor de licenças está inacessível.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Não tens permissão para arquivar este diretório.",
"are_you_sure_you_want_to_archive": "Tens a certeza de que queres arquivar este diretório? Os espaços de trabalho deixarão de ter acesso ao mesmo.",
"assign_workspaces_description": "Controla quais os espaços de trabalho que podem aceder a este diretório de registos de feedback.",
"connectors_description": "Conectores que enviam registos de feedback para este diretório.",
"create_feedback_directory": "Criar diretório de feedback",
"description": "Gere diretórios de registos de feedback e as suas atribuições de espaços de trabalho.",
"directory_archived_successfully": "Diretório arquivado com sucesso",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Diretório desarquivado com sucesso",
"directory_updated_successfully": "Diretório atualizado com sucesso",
"empty_state": "Não foram encontrados diretórios de registos de feedback. Cria um para começar.",
"enter_directory_name": "Insere o nome do diretório",
"error_directory_has_connectors": "Não é possível arquivar um diretório que tem conectores associados. Remove todos os conectores primeiro.",
"error_directory_name_duplicate": "Já existe um diretório de registos de feedback com este nome.",
"error_directory_name_required": "O nome do diretório é obrigatório.",
"error_directory_workspaces_invalid_org": "Algumas áreas de trabalho especificadas não pertencem a esta organização.",
"nav_label": "Diretórios de Feedback",
"no_access": "Não tens permissão para gerir diretórios de registos de feedback.",
"no_connectors": "Ainda não há conectores associados a este diretório.",
"select_workspaces_placeholder": "Selecionar espaços de trabalho...",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registos de Feedback",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Conector duplicado com sucesso",
"connector_status_updated_successfully": "Estado do conector atualizado com sucesso",
"connector_updated_successfully": "Conector atualizado com sucesso",
"connectors": "Conectores",
"create_mapping": "Criar mapeamento",
"created_by": "Criado por",
"csv_at_least_one_row": "O CSV deve conter pelo menos uma linha de dados.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registos de feedback",
"feedback_date": "Data atual",
"feedback_record_directory": "Diretório de Registos de Feedback",
"feedback_record_fields": "Campos de registo de feedback",
"feedback_records": "Registos de feedback",
"feedback_records_refreshed": "Registos de feedback atualizados",
"field_label": "Etiqueta do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"frd_cannot_be_changed": "O diretório de feedback não pode ser alterado após a criação.",
"go_to_feedback_record_directories": "Ir para definições de diretórios",
"historical_import_complete": "Importação concluída: {successes} com sucesso, {failures} falharam, {skipped} ignorados (sem dados)",
"import_csv_data": "Importar feedback",
"import_feedback": "Importar feedback",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Não há nenhum diretório de registos de feedback atribuído a este espaço de trabalho. Cria ou atribui um primeiro.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registo de feedback.",
"question_type_not_supported": "Este tipo de pergunta não é suportado",
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registo de feedback.",
"records_will_go_to": "Os registos irão para",
"refresh_feedback_records": "Atualizar registos de feedback",
"refreshing_feedback_records": "A atualizar registos de feedback...",
"required": "Obrigatório",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas",
"select_a_value": "Selecione um valor...",
"select_all": "Selecionar tudo",
"select_feedback_record_directory": "Selecionar um diretório",
"select_questions": "Selecionar perguntas",
"select_source_type_description": "Selecione o tipo de fonte de feedback que pretende conectar.",
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que pretende conectar:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "A mostrar {count} registos",
"showing_rows": "A mostrar 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de ficheiros CSV",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {# întrebare} few {# întrebări} other {# de întrebări}}",
"count_responses": "{count, plural, one {{count} răspuns} few {{count} răspunsuri} other {{count} de răspunsuri}}",
"count_selections": "{count, plural, one {{count} selecție} few {{count} selecții} other {{count} de selecții}}",
"create": "Creează",
"create_new_organization": "Creează organizație nouă",
"create_segment": "Creați segment",
"create_survey": "Creează sondaj",
@@ -469,6 +470,7 @@
"variables": "Variante",
"verified_email": "Email verificat",
"video": "Video",
"view": "Vizualizare",
"warning": "Avertisment",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Nu am putut verifica licența dvs. deoarece serverul de licențe este inaccesibil.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Nu ai permisiunea să arhivezi acest director.",
"are_you_sure_you_want_to_archive": "Ești sigur că vrei să arhivezi acest director? Spațiile de lucru nu vor mai avea acces la el.",
"assign_workspaces_description": "Controlează care spații de lucru pot accesa acest director de înregistrări de feedback.",
"connectors_description": "Conectori care trimit înregistrări de feedback către acest director.",
"create_feedback_directory": "Creează director de feedback",
"description": "Gestionează directoarele de înregistrări de feedback și atribuirile lor la spații de lucru.",
"directory_archived_successfully": "Directorul a fost arhivat cu succes",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Directorul a fost dezarhivat cu succes",
"directory_updated_successfully": "Directorul a fost actualizat cu succes",
"empty_state": "Nu au fost găsite directoare de înregistrări de feedback. Creează unul pentru a începe.",
"enter_directory_name": "Introdu numele directorului",
"error_directory_has_connectors": "Nu poți arhiva un director care are conectori asociați. Elimină mai întâi toți conectorii.",
"error_directory_name_duplicate": "Există deja un director de înregistrări feedback cu acest nume.",
"error_directory_name_required": "Numele directorului este obligatoriu.",
"error_directory_workspaces_invalid_org": "Unele spații de lucru specificate nu aparțin acestei organizații.",
"nav_label": "Directoare de feedback",
"no_access": "Nu ai permisiunea de a gestiona directoarele de înregistrări de feedback.",
"no_connectors": "Niciun conector asociat acestui director încă.",
"select_workspaces_placeholder": "Selectează spații de lucru...",
"show_archived": "Afișează arhivate",
"title": "Directoare de Înregistrări Feedback",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Conector duplicat cu succes",
"connector_status_updated_successfully": "Statusul conectorului a fost actualizat cu succes",
"connector_updated_successfully": "Conector actualizat cu succes",
"connectors": "Conectori",
"create_mapping": "Creează mapare",
"created_by": "Creat de",
"csv_at_least_one_row": "CSV-ul trebuie să conțină cel puțin un rând de date.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback",
"feedback_date": "Data curentă",
"feedback_record_directory": "Director de înregistrări feedback",
"feedback_record_fields": "Câmpuri înregistrare feedback",
"feedback_records": "Înregistrări de feedback",
"feedback_records_refreshed": "Înregistrările de feedback au fost actualizate",
"field_label": "Etichetă câmp",
"field_type": "Tip câmp",
"formbricks_surveys": "Chestionare Formbricks",
"frd_cannot_be_changed": "Directorul de feedback nu poate fi modificat după creare.",
"go_to_feedback_record_directories": "Mergi la setările directoarelor",
"historical_import_complete": "Import finalizat: {successes} reușite, {failures} eșuate, {skipped} omise (fără date)",
"import_csv_data": "Importă feedback",
"import_feedback": "Importă feedback",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Niciun director de înregistrări feedback atribuit acestui spațiu de lucru. Creează sau atribuie unul mai întâi.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> întrebare selectată. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
"question_type_not_supported": "Acest tip de întrebare nu este suportat",
"questions_selected": "<strong>{count}</strong> întrebări selectate. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
"records_will_go_to": "Înregistrările vor ajunge în",
"refresh_feedback_records": "Reîmprospătează înregistrările de feedback",
"refreshing_feedback_records": "Se actualizează înregistrările de feedback...",
"required": "Obligatoriu",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările",
"select_a_value": "Selectează o valoare...",
"select_all": "Selectează tot",
"select_feedback_record_directory": "Selectează un director",
"select_questions": "Selectează întrebări",
"select_source_type_description": "Selectează tipul sursei de feedback pe care vrei să o conectezi.",
"select_source_type_prompt": "Selectează tipul sursei de feedback pe care vrei să o conectezi:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Se afișează {count} înregistrări",
"showing_rows": "Se afișează 3 din {count} rânduri",
"source": "sursă",
"source_connect_csv_description": "Importă feedback din fișiere CSV",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} вопрос} few {{count} вопроса} many {{count} вопросов} other {{count} вопросов}}",
"count_responses": "{count, plural, one {{count} ответ} few {{count} ответа} many {{count} ответов} other {{count} ответа}}",
"count_selections": "{count, plural, one {{count} выбор} few {{count} выбора} many {{count} выборов} other {{count} выбора}}",
"create": "Создать",
"create_new_organization": "Создать новую организацию",
"create_segment": "Создать сегмент",
"create_survey": "Создать опрос",
@@ -469,6 +470,7 @@
"variables": "Переменные",
"verified_email": "Подтверждённый email",
"video": "Видео",
"view": "Просмотр",
"warning": "Предупреждение",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Не удалось проверить вашу лицензию, так как сервер лицензий недоступен.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "У тебя нет прав для архивирования этого каталога.",
"are_you_sure_you_want_to_archive": "Ты уверен, что хочешь архивировать этот каталог? Рабочие пространства больше не будут иметь к нему доступа.",
"assign_workspaces_description": "Управляй тем, какие рабочие пространства могут получить доступ к этому каталогу записей отзывов.",
"connectors_description": "Коннекторы, которые отправляют записи обратной связи в этот каталог.",
"create_feedback_directory": "Создать директорию для отзывов",
"description": "Управляй каталогами записей отзывов и их назначением рабочим пространствам.",
"directory_archived_successfully": "Каталог успешно архивирован",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Каталог успешно разархивирован",
"directory_updated_successfully": "Каталог успешно обновлён",
"empty_state": "Каталоги записей отзывов не найдены. Создай один, чтобы начать.",
"enter_directory_name": "Введи название каталога",
"error_directory_has_connectors": "Невозможно архивировать каталог, к которому привязаны коннекторы. Сначала удалите все коннекторы.",
"error_directory_name_duplicate": "Директория с записями обратной связи с таким именем уже существует.",
"error_directory_name_required": "Необходимо указать имя директории.",
"error_directory_workspaces_invalid_org": "Некоторые указанные рабочие пространства не принадлежат этой организации.",
"nav_label": "Каталоги отзывов",
"no_access": "У тебя нет прав для управления каталогами записей отзывов.",
"no_connectors": "К этому каталогу пока не привязано ни одного коннектора.",
"select_workspaces_placeholder": "Выберите рабочие области...",
"show_archived": "Показать архивные",
"title": "Директории записей обратной связи",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Коннектор успешно дублирован",
"connector_status_updated_successfully": "Статус коннектора успешно обновлён",
"connector_updated_successfully": "Коннектор успешно обновлён",
"connectors": "Коннекторы",
"create_mapping": "Создать сопоставление",
"created_by": "Создано пользователем",
"csv_at_least_one_row": "CSV должен содержать хотя бы одну строку с данными.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Не удалось загрузить отзывы",
"feedback_date": "Текущая дата",
"feedback_record_directory": "Каталог записей обратной связи",
"feedback_record_fields": "Поля записи отзыва",
"feedback_records": "Записи отзывов",
"feedback_records_refreshed": "Записи отзывов обновлены",
"field_label": "Метка поля",
"field_type": "Тип поля",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Каталог обратной связи нельзя изменить после создания.",
"go_to_feedback_record_directories": "Перейти к настройкам каталогов",
"historical_import_complete": "Импорт завершён: {successes} успешно, {failures} с ошибками, {skipped} пропущено (нет данных)",
"import_csv_data": "Импортировать отзывы",
"import_feedback": "Импортировать отзывы",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "К этому рабочему пространству не назначен каталог записей обратной связи. Сначала создайте или назначьте каталог.",
"no_feedback_records": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.",
"no_source_fields_loaded": "Поля источника ещё не загружены",
"no_sources_connected": "Нет подключённых источников. Добавьте источник, чтобы начать.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> выбранный вопрос. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
"question_type_not_supported": "Этот тип вопроса не поддерживается",
"questions_selected": "<strong>{count}</strong> выбранных вопроса. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
"records_will_go_to": "Записи будут отправлены в",
"refresh_feedback_records": "Обновить записи отзывов",
"refreshing_feedback_records": "Обновляем записи отзывов...",
"required": "Обязательно",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы",
"select_a_value": "Выберите значение...",
"select_all": "Выбрать все",
"select_feedback_record_directory": "Выберите каталог",
"select_questions": "Выберите вопросы",
"select_source_type_description": "Выберите тип источника отзывов, который хотите подключить.",
"select_source_type_prompt": "Выберите тип источника отзывов, который хотите подключить:",
@@ -3455,7 +3465,7 @@
"select_survey_questions_description": "Выберите, какие вопросы опроса должны создавать FeedbackRecords.",
"set_value": "установить значение",
"setup_connection": "Настроить подключение",
"showing_count": "Показано {count} из {total} записей",
"showing_count_loaded": "Показано записей: {count}",
"showing_rows": "Показано 3 из {count} строк",
"source": "источник",
"source_connect_csv_description": "Импортировать отзывы из CSV-файлов",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, one {{count} fråga} other {{count} frågor}}",
"count_responses": "{count, plural, one {{count} svar} other {{count} svar}}",
"count_selections": "{count, plural, one {{count} val} other {{count} val}}",
"create": "Skapa",
"create_new_organization": "Skapa ny organisation",
"create_segment": "Skapa segment",
"create_survey": "Skapa enkät",
@@ -469,6 +470,7 @@
"variables": "Variabler",
"verified_email": "Verifierad e-post",
"video": "Video",
"view": "Visa",
"warning": "Varning",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Vi kunde inte verifiera din licens eftersom licensservern inte kan nås.",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "Du har inte behörighet att arkivera den här katalogen.",
"are_you_sure_you_want_to_archive": "Är du säker på att du vill arkivera den här katalogen? Arbetsytor kommer inte längre ha tillgång till den.",
"assign_workspaces_description": "Styr vilka arbetsytor som kan komma åt den här katalogen för feedbackposter.",
"connectors_description": "Kopplingar som skickar feedbackposter till den här katalogen.",
"create_feedback_directory": "Skapa feedbackkatalog",
"description": "Hantera kataloger för feedbackposter och deras arbetsytstilldelningar.",
"directory_archived_successfully": "Katalogen arkiverades",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "Katalogen återställdes från arkivet",
"directory_updated_successfully": "Katalogen uppdaterades",
"empty_state": "Inga kataloger för feedbackposter hittades. Skapa en för att komma igång.",
"enter_directory_name": "Ange katalognamn",
"error_directory_has_connectors": "Kan inte arkivera en katalog som har kopplingar kopplade till den. Ta bort alla kopplingar först.",
"error_directory_name_duplicate": "En katalog för återkopplingsregister med detta namn finns redan.",
"error_directory_name_required": "Katalognamn krävs.",
"error_directory_workspaces_invalid_org": "Vissa angivna arbetsytor tillhör inte denna organisation.",
"nav_label": "Feedbackkataloger",
"no_access": "Du har inte behörighet att hantera kataloger för feedbackposter.",
"no_connectors": "Inga kopplingar länkade till den här katalogen ännu.",
"select_workspaces_placeholder": "Välj arbetsytor...",
"show_archived": "Visa arkiverade",
"title": "Feedbackkataloger",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "Kopplingen har duplicerats",
"connector_status_updated_successfully": "Kopplingens status har uppdaterats",
"connector_updated_successfully": "Kopplingen uppdaterades",
"connectors": "Kopplingar",
"create_mapping": "Skapa mappning",
"created_by": "Skapad av",
"csv_at_least_one_row": "CSV-filen måste innehålla minst en datarad.",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter",
"feedback_date": "Aktuellt datum",
"feedback_record_directory": "Katalog för feedbackposter",
"feedback_record_fields": "Fält för feedbackpost",
"feedback_records": "Feedbackposter",
"feedback_records_refreshed": "Feedbackposter har uppdaterats",
"field_label": "Fältetikett",
"field_type": "Fälttyp",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedbackkatalog kan inte ändras efter att den skapats.",
"go_to_feedback_record_directories": "Gå till kataloginställningar",
"historical_import_complete": "Importen klar: {successes} lyckades, {failures} misslyckades, {skipped} hoppades över (ingen data)",
"import_csv_data": "Importera feedback",
"import_feedback": "Importera feedback",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "Ingen katalog för feedbackposter tilldelad till den här arbetsytan. Skapa eller tilldela en först.",
"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.",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> fråga vald. Varje svar på dessa frågor skapar en ny feedbackpost.",
"question_type_not_supported": "Den här frågetypen stöds inte",
"questions_selected": "<strong>{count}</strong> frågor valda. Varje svar på dessa frågor skapar en ny feedbackpost.",
"records_will_go_to": "Poster kommer att hamna i",
"refresh_feedback_records": "Uppdatera feedbackposter",
"refreshing_feedback_records": "Uppdaterar feedbackposter...",
"required": "Obligatoriskt",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "Välj en enkät för att se dess frågor",
"select_a_value": "Välj ett värde...",
"select_all": "Välj alla",
"select_feedback_record_directory": "Välj en katalog",
"select_questions": "Välj frågor",
"select_source_type_description": "Välj vilken typ av feedbackkälla du vill ansluta.",
"select_source_type_prompt": "Välj vilken typ av feedbackkälla du vill ansluta:",
@@ -3455,7 +3465,7 @@
"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_count_loaded": "Visar {count} poster",
"showing_rows": "Visar 3 av {count} rader",
"source": "källa",
"source_connect_csv_description": "Importera feedback från CSV-filer",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "共{count}个问题",
"count_responses": "{count, plural, other {{count} 回复} }",
"count_selections": "{count, plural, other {已选择{count}项}}",
"create": "创建",
"create_new_organization": "创建 新的 组织",
"create_segment": "创建 细分",
"create_survey": "创建 调查",
@@ -469,6 +470,7 @@
"variables": "变量",
"verified_email": "已验证 电子邮件",
"video": "视频",
"view": "查看",
"warning": "警告",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "我们无法验证您的许可证,因为许可证服务器无法访问。",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "你无权归档此目录。",
"are_you_sure_you_want_to_archive": "确定要归档此目录吗?工作区将无法再访问它。",
"assign_workspaces_description": "控制哪些工作区可以访问此反馈记录目录。",
"connectors_description": "将反馈记录发送到此目录的连接器。",
"create_feedback_directory": "创建反馈目录",
"description": "管理反馈记录目录及其工作区分配。",
"directory_archived_successfully": "目录已成功归档",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "目录已成功取消归档",
"directory_updated_successfully": "目录已成功更新",
"empty_state": "未找到反馈记录目录。创建一个开始使用吧。",
"enter_directory_name": "输入目录名称",
"error_directory_has_connectors": "无法归档已链接连接器的目录。请先移除所有连接器。",
"error_directory_name_duplicate": "已存在同名的反馈记录目录。",
"error_directory_name_required": "目录名称为必填项。",
"error_directory_workspaces_invalid_org": "某些指定的工作区不属于此组织。",
"nav_label": "反馈目录",
"no_access": "你没有管理反馈记录目录的权限。",
"no_connectors": "此目录尚未链接任何连接器。",
"select_workspaces_placeholder": "选择工作区...",
"show_archived": "显示已归档",
"title": "反馈记录目录",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "连接器复制成功",
"connector_status_updated_successfully": "连接器状态更新成功",
"connector_updated_successfully": "连接器更新成功",
"connectors": "连接器",
"create_mapping": "创建映射",
"created_by": "由 创建",
"csv_at_least_one_row": "CSV 文件中至少要有一行数据。",
@@ -3414,12 +3419,15 @@
"enum": "枚举",
"failed_to_load_feedback_records": "加载反馈记录失败",
"feedback_date": "当前日期",
"feedback_record_directory": "反馈记录目录",
"feedback_record_fields": "反馈记录字段",
"feedback_records": "反馈记录",
"feedback_records_refreshed": "反馈记录已刷新",
"field_label": "字段标签",
"field_type": "字段类型",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "反馈目录创建后无法更改。",
"go_to_feedback_record_directories": "前往目录设置",
"historical_import_complete": "导入完成:{successes} 个成功,{failures} 个失败,{skipped} 个跳过(无数据)",
"import_csv_data": "导入反馈",
"import_feedback": "导入反馈",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "此工作区未分配反馈记录目录。请先创建或分配一个。",
"no_feedback_records": "暂无反馈记录。当你的连接器开始发送数据后,记录会显示在这里。",
"no_source_fields_loaded": "尚未加载源字段",
"no_sources_connected": "还没有连接数据源。添加一个数据源开始吧。",
@@ -3440,6 +3448,7 @@
"question_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
"question_type_not_supported": "不支持此问题类型",
"questions_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
"records_will_go_to": "记录将发送至",
"refresh_feedback_records": "刷新反馈记录",
"refreshing_feedback_records": "正在刷新反馈记录…",
"required": "必填",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "请选择一个调查以查看其问题",
"select_a_value": "选择一个值...",
"select_all": "全选",
"select_feedback_record_directory": "选择目录",
"select_questions": "选择问题",
"select_source_type_description": "请选择你想要连接的反馈来源类型。",
"select_source_type_prompt": "请选择你想要连接的反馈来源类型:",
@@ -3455,7 +3465,7 @@
"select_survey_questions_description": "选择哪些调查问题会创建反馈记录。",
"set_value": "设置值",
"setup_connection": "设置连接",
"showing_count": "正在显示 {count} / {total} 条记录",
"showing_count_loaded": "显示 {count} 条记录",
"showing_rows": "显示 {count} 行中的 3 行",
"source": "source",
"source_connect_csv_description": "从 CSV 文件导入反馈",
+13 -3
View File
@@ -186,6 +186,7 @@
"count_questions": "{count, plural, other {{count} 個問題}}",
"count_responses": "{count, plural, other {{count} 答覆}}",
"count_selections": "{count, plural, other {{count} 個選擇}}",
"create": "建立",
"create_new_organization": "建立新組織",
"create_segment": "建立區隔",
"create_survey": "建立問卷",
@@ -469,6 +470,7 @@
"variables": "變數",
"verified_email": "已驗證的電子郵件",
"video": "影片",
"view": "檢視",
"warning": "警告",
"we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "我們無法驗證您的授權,因為授權伺服器無法連線。",
"webhook": "Webhook",
@@ -2314,6 +2316,7 @@
"archive_not_allowed": "您沒有權限封存此目錄。",
"are_you_sure_you_want_to_archive": "確定要封存此目錄嗎?工作區將無法再存取它。",
"assign_workspaces_description": "控制哪些工作區可以存取此意見回饋記錄目錄。",
"connectors_description": "將意見回饋記錄傳送至此目錄的連接器。",
"create_feedback_directory": "建立意見回饋目錄",
"description": "管理意見回饋記錄目錄及其工作區配置。",
"directory_archived_successfully": "目錄已成功封存",
@@ -2325,12 +2328,13 @@
"directory_unarchived_successfully": "目錄已成功取消封存",
"directory_updated_successfully": "目錄已成功更新",
"empty_state": "找不到任何意見回饋記錄目錄。建立一個開始使用吧。",
"enter_directory_name": "輸入目錄名稱",
"error_directory_has_connectors": "無法封存已連結連接器的目錄。請先移除所有連接器。",
"error_directory_name_duplicate": "已存在同名的意見回饋記錄目錄。",
"error_directory_name_required": "目錄名稱為必填項目。",
"error_directory_workspaces_invalid_org": "部分指定的工作區不屬於此組織。",
"nav_label": "意見回饋目錄",
"no_access": "您沒有權限管理意見回饋記錄目錄。",
"no_connectors": "此目錄尚未連結任何連接器。",
"select_workspaces_placeholder": "選擇工作區...",
"show_archived": "顯示已封存",
"title": "意見回饋記錄目錄",
@@ -3390,6 +3394,7 @@
"connector_duplicated_successfully": "連接器複製成功",
"connector_status_updated_successfully": "連接器狀態更新成功",
"connector_updated_successfully": "連接器更新成功",
"connectors": "連接器",
"create_mapping": "建立對應關係",
"created_by": "建立者",
"csv_at_least_one_row": "CSV 必須至少包含一筆資料列。",
@@ -3414,12 +3419,15 @@
"enum": "enum",
"failed_to_load_feedback_records": "載入回饋紀錄失敗",
"feedback_date": "目前日期",
"feedback_record_directory": "意見回饋記錄目錄",
"feedback_record_fields": "回饋紀錄欄位",
"feedback_records": "回饋紀錄",
"feedback_records_refreshed": "回饋紀錄已更新",
"field_label": "欄位標籤",
"field_type": "欄位類型",
"formbricks_surveys": "Formbricks 問卷",
"frd_cannot_be_changed": "意見回饋目錄在建立後無法變更。",
"go_to_feedback_record_directories": "前往目錄設定",
"historical_import_complete": "匯入完成:{successes} 筆成功,{failures} 筆失敗,{skipped} 筆略過(無資料)",
"import_csv_data": "匯入 CSV 資料",
"import_feedback": "匯入回饋",
@@ -3428,9 +3436,9 @@
"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_record_directory_available": "此工作區尚未指派意見回饋記錄目錄。請先建立或指派一個目錄。",
"no_feedback_records": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。",
"no_source_fields_loaded": "尚未載入來源欄位",
"no_sources_connected": "尚未連接任何來源。請新增來源以開始使用。",
@@ -3440,6 +3448,7 @@
"question_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
"question_type_not_supported": "不支援此題型",
"questions_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
"records_will_go_to": "記錄將傳送至",
"refresh_feedback_records": "重新整理回饋紀錄",
"refreshing_feedback_records": "正在更新回饋紀錄…",
"required": "必填",
@@ -3447,6 +3456,7 @@
"select_a_survey_to_see_questions": "請選擇問卷以查看其問題",
"select_a_value": "請選擇一個值...",
"select_all": "全選",
"select_feedback_record_directory": "選擇目錄",
"select_questions": "選擇問題",
"select_source_type_description": "請選擇你想要連接的回饋來源類型。",
"select_source_type_prompt": "請選擇你想要連接的回饋來源類型:",
@@ -3455,7 +3465,7 @@
"select_survey_questions_description": "請選擇哪些問卷問題要建立 FeedbackRecords。",
"set_value": "設定值",
"setup_connection": "設定連線",
"showing_count": "顯示 {count} 筆,共 {total} 筆紀錄",
"showing_count_loaded": "顯示 {count} 筆錄",
"showing_rows": "顯示 {count} 筆資料中的 3 筆",
"source": "來源",
"source_connect_csv_description": "從 CSV 檔案匯入回饋",
@@ -16,6 +16,7 @@ import { ZFeedbackRecordDirectoryUpdateInput } from "@/modules/ee/feedback-recor
const ZCreateFeedbackRecordDirectoryAction = z.object({
organizationId: ZId,
name: z.string().trim().min(1, "DIRECTORY_NAME_REQUIRED"),
workspaceIds: z.array(ZId).optional(),
});
export const createFeedbackRecordDirectoryAction = authenticatedActionClient
@@ -33,7 +34,11 @@ export const createFeedbackRecordDirectoryAction = authenticatedActionClient
],
});
const result = await createFeedbackRecordDirectory(parsedInput.organizationId, parsedInput.name);
const result = await createFeedbackRecordDirectory(
parsedInput.organizationId,
parsedInput.name,
parsedInput.workspaceIds
);
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
ctx.auditLoggingCtx.feedbackRecordDirectoryId = result;
ctx.auditLoggingCtx.newObject = {
@@ -1,118 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createFeedbackRecordDirectoryAction } from "@/modules/ee/feedback-record-directory/actions";
import {
TFeedbackRecordDirectoryCreateInput,
ZFeedbackRecordDirectoryCreateInput,
getTranslatedFeedbackRecordDirectoryError,
} from "@/modules/ee/feedback-record-directory/types/feedback-record-directory";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
interface CreateFeedbackRecordDirectoryModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
organizationId: string;
}
export const CreateFeedbackRecordDirectoryModal = ({
open,
setOpen,
organizationId,
}: CreateFeedbackRecordDirectoryModalProps) => {
const { t } = useTranslation();
const router = useRouter();
const form = useForm<TFeedbackRecordDirectoryCreateInput>({
defaultValues: { name: "" },
mode: "onChange",
resolver: zodResolver(ZFeedbackRecordDirectoryCreateInput),
});
const {
control,
handleSubmit,
formState: { isSubmitting },
reset,
} = form;
const handleCreation: SubmitHandler<TFeedbackRecordDirectoryCreateInput> = async (data) => {
const response = await createFeedbackRecordDirectoryAction({ name: data.name, organizationId });
if (response?.data) {
toast.success(t("workspace.settings.feedback_record_directories.directory_created_successfully"));
router.refresh();
setOpen(false);
reset();
} else {
const errorCode = getFormattedErrorMessage(response);
toast.error(getTranslatedFeedbackRecordDirectoryError(errorCode, t));
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t("workspace.settings.feedback_record_directories.create_feedback_directory")}
</DialogTitle>
</DialogHeader>
<FormProvider {...form}>
<form onSubmit={handleSubmit(handleCreation)} className="gap-y-4 pt-4">
<DialogBody>
<FormField
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem className="pb-4">
<FormLabel>
{t("workspace.settings.feedback_record_directories.directory_name")}
</FormLabel>
<FormControl>
<Input
placeholder={t("workspace.settings.feedback_record_directories.enter_directory_name")}
{...field}
/>
</FormControl>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
)}
/>
</DialogBody>
<DialogFooter>
<Button
variant="secondary"
type="button"
onClick={() => {
setOpen(false);
reset();
}}>
{t("common.cancel")}
</Button>
<Button disabled={!form.formState.isValid || isSubmitting} loading={isSubmitting} type="submit">
{t("workspace.settings.feedback_record_directories.create_feedback_directory")}
</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);
};
@@ -9,7 +9,10 @@ import { useTranslation } from "react-i18next";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateFeedbackRecordDirectoryAction } from "@/modules/ee/feedback-record-directory/actions";
import {
createFeedbackRecordDirectoryAction,
updateFeedbackRecordDirectoryAction,
} from "@/modules/ee/feedback-record-directory/actions";
import { ArchiveFeedbackRecordDirectory } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-settings/archive-feedback-record-directory";
import {
TFeedbackRecordDirectoryDetails,
@@ -37,7 +40,8 @@ import { Muted } from "@/modules/ui/components/typography";
interface FeedbackRecordDirectorySettingsModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
directory: TFeedbackRecordDirectoryDetails;
directory?: TFeedbackRecordDirectoryDetails;
organizationId: string;
orgWorkspaces: TOrganizationWorkspace[];
membershipRole: TOrganizationRole;
}
@@ -46,6 +50,7 @@ export const FeedbackRecordDirectorySettingsModal = ({
open,
setOpen,
directory,
organizationId,
orgWorkspaces,
membershipRole,
}: FeedbackRecordDirectorySettingsModalProps) => {
@@ -53,6 +58,7 @@ export const FeedbackRecordDirectorySettingsModal = ({
const { isOwner, isManager } = getAccessFlags(membershipRole);
const isOwnerOrManager = isOwner || isManager;
const router = useRouter();
const isEdit = !!directory;
const workspaceOptions = useMemo(
() =>
@@ -63,13 +69,13 @@ export const FeedbackRecordDirectorySettingsModal = ({
);
const initialWorkspaceIds = useMemo(
() => directory.workspaces.map((p) => p.workspaceId),
[directory.workspaces]
() => directory?.workspaces.map((p) => p.workspaceId) ?? [],
[directory?.workspaces]
);
const form = useForm<TFeedbackRecordDirectoryUpdateInput>({
defaultValues: {
name: directory.name,
name: directory?.name ?? "",
workspaceIds: initialWorkspaceIds,
},
mode: "onChange",
@@ -81,24 +87,33 @@ export const FeedbackRecordDirectorySettingsModal = ({
handleSubmit,
formState: { isSubmitting },
setValue,
reset,
} = form;
const closeSettingsModal = () => {
const closeModal = () => {
reset();
setOpen(false);
};
const handleUpdate: SubmitHandler<TFeedbackRecordDirectoryUpdateInput> = async (data) => {
const response = await updateFeedbackRecordDirectoryAction({
directoryId: directory.id,
data: {
name: data.name,
workspaceIds: data.workspaceIds,
},
});
const handleSubmitForm: SubmitHandler<TFeedbackRecordDirectoryUpdateInput> = async (data) => {
const response = isEdit
? await updateFeedbackRecordDirectoryAction({
directoryId: directory.id,
data: { name: data.name, workspaceIds: data.workspaceIds },
})
: await createFeedbackRecordDirectoryAction({
organizationId,
name: data.name ?? "",
workspaceIds: data.workspaceIds,
});
if (response?.data) {
toast.success(t("workspace.settings.feedback_record_directories.directory_updated_successfully"));
closeSettingsModal();
toast.success(
isEdit
? t("workspace.settings.feedback_record_directories.directory_updated_successfully")
: t("workspace.settings.feedback_record_directories.directory_created_successfully")
);
closeModal();
router.refresh();
} else {
const errorCode = getFormattedErrorMessage(response);
@@ -107,20 +122,24 @@ export const FeedbackRecordDirectorySettingsModal = ({
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={(newOpen) => (newOpen ? setOpen(true) : closeModal())}>
<DialogContent>
<DialogHeader className="pb-4">
<DialogTitle>
{t("workspace.settings.feedback_record_directories.directory_settings_title", {
directoryName: directory.name,
})}
{isEdit
? t("workspace.settings.feedback_record_directories.directory_settings_title", {
directoryName: directory.name,
})
: t("workspace.settings.feedback_record_directories.create_feedback_directory")}
</DialogTitle>
<DialogDescription>
{t("workspace.settings.feedback_record_directories.directory_settings_description")}
{isEdit
? t("workspace.settings.feedback_record_directories.directory_settings_description")
: t("workspace.settings.feedback_record_directories.create_feedback_directory")}
</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form className="contents space-y-4" onSubmit={handleSubmit(handleUpdate)}>
<form className="contents space-y-4" onSubmit={handleSubmit(handleSubmitForm)}>
<DialogBody className="flex-grow space-y-6 overflow-y-auto">
<FormField
control={control}
@@ -143,11 +162,13 @@ export const FeedbackRecordDirectorySettingsModal = ({
)}
/>
<IdBadge
id={directory.id}
label={t("workspace.settings.feedback_record_directories.directory_id")}
variant="column"
/>
{isEdit && (
<IdBadge
id={directory.id}
label={t("workspace.settings.feedback_record_directories.directory_id")}
variant="column"
/>
)}
<div className="space-y-2">
<FormLabel>{t("common.workspaces")}</FormLabel>
@@ -156,7 +177,7 @@ export const FeedbackRecordDirectorySettingsModal = ({
</Muted>
<MultiSelect
options={workspaceOptions}
value={form.watch("workspaceIds")}
value={form.watch("workspaceIds") ?? []}
onChange={(selected) => {
setValue("workspaceIds", selected, { shouldDirty: true });
}}
@@ -167,20 +188,56 @@ export const FeedbackRecordDirectorySettingsModal = ({
containerClassName="focus-within:ring-0 focus-within:ring-offset-0"
/>
</div>
{isEdit && (
<div className="space-y-2">
<FormLabel>{t("workspace.unify.connectors")}</FormLabel>
<Muted className="block text-slate-500">
{t("workspace.settings.feedback_record_directories.connectors_description")}
</Muted>
{directory.connectors.length === 0 ? (
<p className="rounded-md border border-dashed border-slate-200 p-3 text-center text-sm text-slate-400">
{t("workspace.settings.feedback_record_directories.no_connectors")}
</p>
) : (
<ul className="space-y-2">
{directory.connectors.map((c) => (
<li
key={c.id}
className="flex items-center justify-between rounded-md border border-slate-200 bg-slate-50 p-3 text-sm">
<div>
<p className="font-medium text-slate-900">{c.name}</p>
<p className="text-xs text-slate-500">
{c.type} · {c.workspaceName}
</p>
</div>
<a
className="text-xs font-medium text-slate-700 hover:text-slate-900 hover:underline"
href={`/workspaces/${c.workspaceId}/unify/sources`}>
{t("common.view")}
</a>
</li>
))}
</ul>
)}
</div>
)}
</DialogBody>
<DialogFooter>
<div className="w-full">
<ArchiveFeedbackRecordDirectory
directoryId={directory.id}
onArchive={closeSettingsModal}
isOwnerOrManager={isOwnerOrManager}
/>
</div>
<Button size="default" type="button" variant="outline" onClick={closeSettingsModal}>
{isEdit && (
<div className="w-full">
<ArchiveFeedbackRecordDirectory
directoryId={directory.id}
onArchive={closeModal}
isOwnerOrManager={isOwnerOrManager}
/>
</div>
)}
<Button size="default" type="button" variant="outline" onClick={closeModal}>
{t("common.cancel")}
</Button>
<Button type="submit" size="default" loading={isSubmitting} disabled={!isOwnerOrManager}>
{t("common.save")}
{isEdit ? t("common.save") : t("common.create")}
</Button>
</DialogFooter>
</form>
@@ -11,7 +11,6 @@ import {
getFeedbackRecordDirectoryDetailsAction,
updateFeedbackRecordDirectoryAction,
} from "@/modules/ee/feedback-record-directory/actions";
import { CreateFeedbackRecordDirectoryModal } from "@/modules/ee/feedback-record-directory/components/create-feedback-record-directory-modal";
import { FeedbackRecordDirectorySettingsModal } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-settings/feedback-record-directory-settings-modal";
import {
TFeedbackRecordDirectory,
@@ -161,17 +160,22 @@ export const FeedbackRecordDirectoryTable = ({
</Table>
</div>
<CreateFeedbackRecordDirectoryModal
open={openCreateModal}
setOpen={setOpenCreateModal}
organizationId={organizationId}
/>
{openCreateModal && (
<FeedbackRecordDirectorySettingsModal
open={openCreateModal}
setOpen={setOpenCreateModal}
organizationId={organizationId}
orgWorkspaces={orgWorkspaces}
membershipRole={membershipRole}
/>
)}
{openSettingsModal && selectedDirectory && (
<FeedbackRecordDirectorySettingsModal
open={openSettingsModal}
setOpen={setOpenSettingsModal}
directory={selectedDirectory}
organizationId={organizationId}
orgWorkspaces={orgWorkspaces}
membershipRole={membershipRole}
/>
@@ -5,11 +5,18 @@ import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbr
import {
createFeedbackRecordDirectory,
getFeedbackRecordDirectories,
getFeedbackRecordDirectoriesByWorkspaceId,
getFeedbackRecordDirectoryDetails,
getOrganizationIdFromDirectoryId,
updateFeedbackRecordDirectory,
} from "./feedback-record-directory";
vi.mock("server-only", () => ({}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
feedbackRecordDirectory: {
@@ -18,9 +25,15 @@ vi.mock("@formbricks/database", () => ({
create: vi.fn(),
update: vi.fn(),
},
feedbackRecordDirectoryWorkspace: {
findMany: vi.fn(),
},
workspace: {
count: vi.fn(),
},
connector: {
count: vi.fn().mockResolvedValue(0),
},
},
}));
@@ -33,7 +46,7 @@ const mockDirectoryDbRow = {
id: mockDirectoryId,
name: "Test Directory",
isArchived: false,
_count: { workspaces: 2 },
_count: { workspaces: 2, connectors: 1 },
};
const mockDirectoryDetailsDbRow = {
@@ -45,6 +58,7 @@ const mockDirectoryDetailsDbRow = {
{ workspaceId: mockWorkspaceId1, workspace: { name: "Workspace A" } },
{ workspaceId: mockWorkspaceId2, workspace: { name: "Workspace B" } },
],
connectors: [],
};
describe("FeedbackRecordDirectory Service", () => {
@@ -64,6 +78,7 @@ describe("FeedbackRecordDirectory Service", () => {
name: "Test Directory",
isArchived: false,
workspaceCount: 2,
connectorCount: 1,
},
]);
expect(prisma.feedbackRecordDirectory.findMany).toHaveBeenCalledWith({
@@ -72,7 +87,7 @@ describe("FeedbackRecordDirectory Service", () => {
id: true,
name: true,
isArchived: true,
_count: { select: { workspaces: true } },
_count: { select: { workspaces: true, connectors: true } },
},
orderBy: { createdAt: "desc" },
});
@@ -121,9 +136,38 @@ describe("FeedbackRecordDirectory Service", () => {
{ workspaceId: mockWorkspaceId1, workspaceName: "Workspace A" },
{ workspaceId: mockWorkspaceId2, workspaceName: "Workspace B" },
],
connectors: [],
});
});
test("returns directory details with connectors", async () => {
const dbRowWithConnectors = {
...mockDirectoryDetailsDbRow,
connectors: [
{
id: "conn-1",
name: "My Connector",
type: "formbricks",
workspaceId: mockWorkspaceId1,
workspace: { name: "Workspace A" },
},
],
};
vi.mocked(prisma.feedbackRecordDirectory.findUnique).mockResolvedValueOnce(dbRowWithConnectors as any);
const result = await getFeedbackRecordDirectoryDetails(mockDirectoryId);
expect(result?.connectors).toEqual([
{
id: "conn-1",
name: "My Connector",
type: "formbricks",
workspaceId: mockWorkspaceId1,
workspaceName: "Workspace A",
},
]);
});
test("returns null when directory not found", async () => {
vi.mocked(prisma.feedbackRecordDirectory.findUnique).mockResolvedValueOnce(null);
@@ -158,6 +202,41 @@ describe("FeedbackRecordDirectory Service", () => {
});
});
test("creates a directory with workspace links", async () => {
vi.mocked(prisma.workspace.count).mockResolvedValueOnce(2);
vi.mocked(prisma.feedbackRecordDirectory.create).mockResolvedValueOnce({
id: mockDirectoryId,
} as any);
const result = await createFeedbackRecordDirectory(mockOrganizationId, "With Workspaces", [
mockWorkspaceId1,
mockWorkspaceId2,
]);
expect(result).toBe(mockDirectoryId);
expect(prisma.workspace.count).toHaveBeenCalledWith({
where: { id: { in: [mockWorkspaceId1, mockWorkspaceId2] }, organizationId: mockOrganizationId },
});
expect(prisma.feedbackRecordDirectory.create).toHaveBeenCalledWith({
data: {
name: "With Workspaces",
organizationId: mockOrganizationId,
workspaces: {
create: [{ workspaceId: mockWorkspaceId1 }, { workspaceId: mockWorkspaceId2 }],
},
},
select: { id: true },
});
});
test("throws InvalidInputError when workspaceIds belong to different org", async () => {
vi.mocked(prisma.workspace.count).mockResolvedValueOnce(0);
await expect(
createFeedbackRecordDirectory(mockOrganizationId, "Bad Workspaces", [mockWorkspaceId1])
).rejects.toThrow(new InvalidInputError("DIRECTORY_PROJECTS_INVALID_ORG"));
});
test("throws InvalidInputError on duplicate name (unique constraint violation)", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint", {
code: "P2002",
@@ -203,7 +282,8 @@ describe("FeedbackRecordDirectory Service", () => {
});
});
test("updates archive status", async () => {
test("archives directory when no connectors linked", async () => {
vi.mocked(prisma.connector.count).mockResolvedValueOnce(0);
vi.mocked(prisma.feedbackRecordDirectory.update).mockResolvedValueOnce({} as any);
const result = await updateFeedbackRecordDirectory(mockDirectoryId, mockOrganizationId, {
@@ -211,12 +291,37 @@ describe("FeedbackRecordDirectory Service", () => {
});
expect(result).toBe(true);
expect(prisma.connector.count).toHaveBeenCalledWith({
where: { feedbackRecordDirectoryId: mockDirectoryId },
});
expect(prisma.feedbackRecordDirectory.update).toHaveBeenCalledWith({
where: { id: mockDirectoryId },
data: { isArchived: true },
});
});
test("throws InvalidInputError when archiving directory with connectors", async () => {
vi.mocked(prisma.connector.count).mockResolvedValueOnce(2);
await expect(
updateFeedbackRecordDirectory(mockDirectoryId, mockOrganizationId, { isArchived: true })
).rejects.toThrow(new InvalidInputError("DIRECTORY_HAS_CONNECTORS"));
});
test("unarchives directory", async () => {
vi.mocked(prisma.feedbackRecordDirectory.update).mockResolvedValueOnce({} as any);
const result = await updateFeedbackRecordDirectory(mockDirectoryId, mockOrganizationId, {
isArchived: false,
});
expect(result).toBe(true);
expect(prisma.feedbackRecordDirectory.update).toHaveBeenCalledWith({
where: { id: mockDirectoryId },
data: { isArchived: false },
});
});
test("updates workspace assignments with diff", async () => {
// getFeedbackRecordDirectoryDetails call
vi.mocked(prisma.feedbackRecordDirectory.findUnique).mockResolvedValueOnce(
@@ -293,6 +398,54 @@ describe("FeedbackRecordDirectory Service", () => {
});
});
describe("getFeedbackRecordDirectoriesByWorkspaceId", () => {
test("returns directories assigned to workspace", async () => {
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.findMany).mockResolvedValueOnce([
{ feedbackRecordDirectory: { id: mockDirectoryId, name: "Test Directory" } },
] as any);
const result = await getFeedbackRecordDirectoriesByWorkspaceId(mockWorkspaceId1);
expect(result).toEqual([{ id: mockDirectoryId, name: "Test Directory" }]);
expect(prisma.feedbackRecordDirectoryWorkspace.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId1,
feedbackRecordDirectory: { isArchived: false },
},
select: {
feedbackRecordDirectory: { select: { id: true, name: true } },
},
});
});
test("returns empty array when no directories assigned", async () => {
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.findMany).mockResolvedValueOnce([]);
const result = await getFeedbackRecordDirectoriesByWorkspaceId(mockWorkspaceId1);
expect(result).toEqual([]);
});
test("throws DatabaseError on Prisma error", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("DB error", {
code: "P2010",
clientVersion: "0.0.1",
});
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.findMany).mockRejectedValueOnce(prismaError);
await expect(getFeedbackRecordDirectoriesByWorkspaceId(mockWorkspaceId1)).rejects.toThrow(
DatabaseError
);
});
test("re-throws unexpected errors", async () => {
const error = new Error("Unexpected");
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.findMany).mockRejectedValueOnce(error);
await expect(getFeedbackRecordDirectoriesByWorkspaceId(mockWorkspaceId1)).rejects.toThrow(error);
});
});
describe("getOrganizationIdFromDirectoryId", () => {
test("returns organization ID for a valid directory", async () => {
vi.mocked(prisma.feedbackRecordDirectory.findUnique).mockResolvedValueOnce({
@@ -38,6 +38,7 @@ export const getFeedbackRecordDirectories = reactCache(
_count: {
select: {
workspaces: true,
connectors: true,
},
},
},
@@ -51,6 +52,7 @@ export const getFeedbackRecordDirectories = reactCache(
name: dir.name,
isArchived: dir.isArchived,
workspaceCount: dir._count.workspaces,
connectorCount: dir._count.connectors,
}));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -70,6 +72,33 @@ export const getFeedbackRecordDirectories = reactCache(
* @throws {DatabaseError} If a Prisma database error occurs.
* @throws Re-throws any other unexpected errors.
*/
/**
* Lists feedback record directories assigned to a workspace.
* Used by connector creation to pick an FRD.
*/
export const getFeedbackRecordDirectoriesByWorkspaceId = reactCache(
async (workspaceId: string): Promise<{ id: string; name: string }[]> => {
validateInputs([workspaceId, ZId]);
try {
const rows = await prisma.feedbackRecordDirectoryWorkspace.findMany({
where: {
workspaceId,
feedbackRecordDirectory: { isArchived: false },
},
select: {
feedbackRecordDirectory: { select: { id: true, name: true } },
},
});
return rows.map((r) => r.feedbackRecordDirectory);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const getFeedbackRecordDirectoryDetails = reactCache(
async (directoryId: string): Promise<TFeedbackRecordDirectoryDetails | null> => {
validateInputs([directoryId, ZId]);
@@ -93,6 +122,16 @@ export const getFeedbackRecordDirectoryDetails = reactCache(
},
},
},
connectors: {
select: {
id: true,
name: true,
type: true,
workspaceId: true,
workspace: { select: { name: true } },
},
orderBy: { createdAt: "desc" },
},
},
});
@@ -109,6 +148,13 @@ export const getFeedbackRecordDirectoryDetails = reactCache(
workspaceId: dp.workspaceId,
workspaceName: dp.workspace.name,
})),
connectors: directory.connectors.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
workspaceId: c.workspaceId,
workspaceName: c.workspace.name,
})),
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -133,14 +179,28 @@ export const getFeedbackRecordDirectoryDetails = reactCache(
*/
export const createFeedbackRecordDirectory = async (
organizationId: string,
name: string
name: string,
workspaceIds?: string[]
): Promise<string> => {
validateInputs([organizationId, ZId], [name, z.string().trim().min(1, "DIRECTORY_NAME_REQUIRED")]);
try {
// Verify workspaces belong to same org
if (workspaceIds?.length) {
const count = await prisma.workspace.count({
where: { id: { in: workspaceIds }, organizationId },
});
if (count !== workspaceIds.length) {
throw new InvalidInputError("DIRECTORY_PROJECTS_INVALID_ORG");
}
}
const directory = await prisma.feedbackRecordDirectory.create({
data: {
name,
organizationId,
workspaces: workspaceIds?.length
? { create: workspaceIds.map((workspaceId) => ({ workspaceId })) }
: undefined,
},
select: {
id: true,
@@ -244,8 +304,16 @@ export const updateFeedbackRecordDirectory = async (
payload.name = name;
}
if (isArchived !== undefined) {
payload.isArchived = isArchived;
if (isArchived === true) {
const connectorCount = await prisma.connector.count({
where: { feedbackRecordDirectoryId: directoryId },
});
if (connectorCount > 0) {
throw new InvalidInputError("DIRECTORY_HAS_CONNECTORS");
}
payload.isArchived = true;
} else if (isArchived === false) {
payload.isArchived = false;
}
if (workspaceIds !== undefined) {
@@ -6,6 +6,7 @@ export const ZFeedbackRecordDirectory = z.object({
name: z.string(),
isArchived: z.boolean(),
workspaceCount: z.number(),
connectorCount: z.number(),
});
export type TFeedbackRecordDirectory = z.infer<typeof ZFeedbackRecordDirectory>;
@@ -21,12 +22,22 @@ export const ZFeedbackRecordDirectoryDetails = z.object({
workspaceName: z.string(),
})
),
connectors: z.array(
z.object({
id: ZId,
name: z.string(),
type: z.string(),
workspaceId: ZId,
workspaceName: z.string(),
})
),
});
export type TFeedbackRecordDirectoryDetails = z.infer<typeof ZFeedbackRecordDirectoryDetails>;
export const ZFeedbackRecordDirectoryCreateInput = z.object({
name: z.string().trim().min(1, "DIRECTORY_NAME_REQUIRED"),
workspaceIds: z.array(ZId).optional(),
});
export type TFeedbackRecordDirectoryCreateInput = z.infer<typeof ZFeedbackRecordDirectoryCreateInput>;
@@ -54,6 +65,8 @@ export const getTranslatedFeedbackRecordDirectoryError = (
return t("workspace.settings.feedback_record_directories.error_directory_name_duplicate");
case "DIRECTORY_PROJECTS_INVALID_ORG":
return t("workspace.settings.feedback_record_directories.error_directory_workspaces_invalid_org");
case "DIRECTORY_HAS_CONNECTORS":
return t("workspace.settings.feedback_record_directories.error_directory_has_connectors");
default:
return errorCode;
}
+10 -3
View File
@@ -1,8 +1,11 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import FormbricksHub from "@formbricks/hub";
vi.mock("server-only", () => ({}));
vi.mock("@formbricks/hub", () => {
const MockFormbricksHub = vi.fn();
// Must use `function` (not arrow) so it's valid as a `new` target.
const MockFormbricksHub = vi.fn(function () {});
return { default: MockFormbricksHub };
});
@@ -40,7 +43,9 @@ describe("getHubClient", () => {
test("creates and caches a new client when HUB_API_KEY is set", async () => {
mutableEnv.HUB_API_KEY = "test-key";
const mockInstance = { feedbackRecords: {} } as unknown as FormbricksHub;
vi.mocked(FormbricksHub).mockReturnValue(mockInstance);
vi.mocked(FormbricksHub).mockImplementation(function () {
return mockInstance as any;
});
const { getHubClient } = await import("./hub-client");
const client = getHubClient();
@@ -71,7 +76,9 @@ describe("getHubClient", () => {
mutableEnv.HUB_API_KEY = "now-set";
const mockInstance = { feedbackRecords: {} } as unknown as FormbricksHub;
vi.mocked(FormbricksHub).mockReturnValue(mockInstance);
vi.mocked(FormbricksHub).mockImplementation(function () {
return mockInstance as any;
});
const second = getHubClient();
expect(second).toBe(mockInstance);
@@ -38,6 +38,13 @@ vi.mock("@formbricks/database", () => ({
workspaceTeam: {
createMany: vi.fn(),
},
feedbackRecordDirectory: {
upsert: vi.fn(),
},
feedbackRecordDirectoryWorkspace: {
count: vi.fn(),
create: vi.fn(),
},
},
}));
@@ -94,10 +101,50 @@ describe("workspace lib", () => {
const createdWorkspace = { ...baseWorkspace, id: "p2" };
vi.mocked(prisma.workspace.create).mockResolvedValueOnce(createdWorkspace as any);
vi.mocked(prisma.workspaceTeam.createMany).mockResolvedValueOnce({} as any);
vi.mocked(prisma.feedbackRecordDirectory.upsert).mockResolvedValueOnce({ id: "frd-1" } as any);
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.count).mockResolvedValueOnce(0);
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.create).mockResolvedValueOnce({} as any);
const result = await createWorkspace("org1", { name: "Workspace 1", teamIds: ["t1"] });
expect(result).toEqual(createdWorkspace);
expect(prisma.workspace.create).toHaveBeenCalled();
expect(prisma.workspaceTeam.createMany).toHaveBeenCalled();
expect(prisma.feedbackRecordDirectory.upsert).toHaveBeenCalled();
});
test("creates workspace and links default FRD when first workspace", async () => {
const createdWorkspace = { ...baseWorkspace, id: "p3" };
vi.mocked(prisma.workspace.create).mockResolvedValueOnce(createdWorkspace as any);
vi.mocked(prisma.feedbackRecordDirectory.upsert).mockResolvedValueOnce({ id: "frd-1" } as any);
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.count).mockResolvedValueOnce(0);
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.create).mockResolvedValueOnce({} as any);
await createWorkspace("org1", { name: "Workspace No Teams" });
expect(prisma.feedbackRecordDirectory.upsert).toHaveBeenCalledWith({
where: {
organizationId_name: { organizationId: "org1", name: "Default Feedback Record Directory" },
},
create: { name: "Default Feedback Record Directory", organizationId: "org1" },
update: {},
select: { id: true },
});
expect(prisma.feedbackRecordDirectoryWorkspace.count).toHaveBeenCalledWith({
where: { feedbackRecordDirectoryId: "frd-1" },
});
expect(prisma.feedbackRecordDirectoryWorkspace.create).toHaveBeenCalledWith({
data: { feedbackRecordDirectoryId: "frd-1", workspaceId: "p3" },
});
});
test("skips FRD link when default FRD already has links", async () => {
const createdWorkspace = { ...baseWorkspace, id: "p4" };
vi.mocked(prisma.workspace.create).mockResolvedValueOnce(createdWorkspace as any);
vi.mocked(prisma.feedbackRecordDirectory.upsert).mockResolvedValueOnce({ id: "frd-1" } as any);
vi.mocked(prisma.feedbackRecordDirectoryWorkspace.count).mockResolvedValueOnce(1);
await createWorkspace("org1", { name: "Second Workspace" });
expect(prisma.feedbackRecordDirectoryWorkspace.create).not.toHaveBeenCalled();
});
test("throws ValidationError if name is missing", async () => {
@@ -89,6 +89,30 @@ export const createWorkspace = async (
});
}
// Ensure default FRD exists + link to first workspace atomically
const defaultFrd = await prisma.feedbackRecordDirectory.upsert({
where: {
organizationId_name: { organizationId, name: "Default Feedback Record Directory" },
},
create: { name: "Default Feedback Record Directory", organizationId },
update: {},
select: { id: true },
});
// Link only if this is the first workspace (no existing links for this FRD)
const existingLinks = await prisma.feedbackRecordDirectoryWorkspace.count({
where: { feedbackRecordDirectoryId: defaultFrd.id },
});
if (existingLinks === 0) {
await prisma.feedbackRecordDirectoryWorkspace.create({
data: {
feedbackRecordDirectoryId: defaultFrd.id,
workspaceId: workspace.id,
},
});
}
return workspace;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
+1 -1
View File
@@ -265,7 +265,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByText("German", { exact: true }).nth(1).click();
await page.getByRole("button", { name: "Save changes" }).click();
await page.waitForTimeout(2000);
await page.getByRole("link", { name: "Ask" }).click();
await page.getByRole("link", { name: "Surveys" }).click();
await page.getByText("Start from scratch").click();
await page.getByRole("button", { name: "Create survey", exact: true }).click();
await page.locator("#multi-lang-toggle").click();
@@ -41,6 +41,11 @@ ALTER TABLE "FeedbackRecordDirectoryWorkspace" ADD CONSTRAINT "FeedbackRecordDir
-- AddForeignKey
ALTER TABLE "FeedbackRecordDirectoryWorkspace" ADD CONSTRAINT "FeedbackRecordDirectoryWorkspace_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Connector → FeedbackRecordDirectory FK
ALTER TABLE "Connector" ADD COLUMN "feedbackRecordDirectoryId" TEXT NOT NULL;
ALTER TABLE "Connector" ADD CONSTRAINT "Connector_feedbackRecordDirectoryId_fkey" FOREIGN KEY ("feedbackRecordDirectoryId") REFERENCES "FeedbackRecordDirectory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX "Connector_feedbackRecordDirectoryId_idx" ON "Connector"("feedbackRecordDirectoryId");
-- RenameIndex
ALTER INDEX "ConnectorFieldMapping_workspaceId_connectorId_source_fiel_key" RENAME TO "ConnectorFieldMapping_workspaceId_connectorId_source_field__key";
+17 -13
View File
@@ -1086,23 +1086,26 @@ enum HubFieldType {
/// @property formbricksMappings - Element mappings for Formbricks connectors
/// @property fieldMappings - Field mappings for other connector types
model Connector {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
type ConnectorType
status ConnectorStatus @default(active)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
formbricksMappings ConnectorFormbricksMapping[]
fieldMappings ConnectorFieldMapping[]
lastSyncAt DateTime? @map(name: "last_sync_at")
createdBy String? @map(name: "created_by")
creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull)
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
type ConnectorType
status ConnectorStatus @default(active)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
feedbackRecordDirectoryId String
feedbackRecordDirectory FeedbackRecordDirectory @relation(fields: [feedbackRecordDirectoryId], references: [id], onDelete: Cascade)
formbricksMappings ConnectorFormbricksMapping[]
fieldMappings ConnectorFieldMapping[]
lastSyncAt DateTime? @map(name: "last_sync_at")
createdBy String? @map(name: "created_by")
creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull)
@@unique([id, workspaceId])
@@unique([workspaceId, name])
@@index([type])
@@index([feedbackRecordDirectoryId])
}
/// Maps survey elements to Hub FeedbackRecords for Formbricks connectors.
@@ -1169,6 +1172,7 @@ model FeedbackRecordDirectory {
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
workspaces FeedbackRecordDirectoryWorkspace[]
connectors Connector[]
@@unique([organizationId, name])
}
+27
View File
@@ -373,6 +373,18 @@ async function main(): Promise<void> {
},
});
const defaultFrd = await prisma.feedbackRecordDirectory.upsert({
where: {
organizationId_name: { organizationId: organization.id, name: "Default Feedback Record Directory" },
},
update: {},
create: {
name: "Default Feedback Record Directory",
organizationId: organization.id,
},
select: { id: true },
});
// Users
const passwordHash = await bcrypt.hash(SEED_CREDENTIALS.ADMIN.password, 10);
@@ -444,6 +456,21 @@ async function main(): Promise<void> {
},
});
// Link default FRD to workspace
await prisma.feedbackRecordDirectoryWorkspace.upsert({
where: {
feedbackRecordDirectoryId_workspaceId: {
feedbackRecordDirectoryId: defaultFrd.id,
workspaceId: workspace.id,
},
},
update: {},
create: {
feedbackRecordDirectoryId: defaultFrd.id,
workspaceId: workspace.id,
},
});
// Contact attribute keys for the workspace
const defaultAttributeKeys = [
{ name: "Email", key: "email", isUnique: true, type: "default" as const },
+2
View File
@@ -54,6 +54,7 @@ export const ZConnector = z.object({
type: ZConnectorType,
status: ZConnectorStatus,
workspaceId: z.cuid2(),
feedbackRecordDirectoryId: z.cuid2(),
lastSyncAt: z.date().nullable(),
createdBy: z.string().nullable(),
});
@@ -94,6 +95,7 @@ export type TConnectorWithMappings = z.infer<typeof ZConnectorWithMappings>;
export const ZConnectorCreateInput = z.object({
name: z.string().min(1),
type: ZConnectorType,
feedbackRecordDirectoryId: z.cuid2(),
createdBy: z.cuid2().optional(),
});
export type TConnectorCreateInput = z.infer<typeof ZConnectorCreateInput>;