mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-13 03:16:58 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d44e18a61 |
@@ -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,
|
||||
},
|
||||
|
||||
+6
@@ -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}
|
||||
/>
|
||||
|
||||
+72
-46
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
@@ -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);
|
||||
|
||||
+83
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+13
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const importCsvData = async (
|
||||
const { records, skipped } = transformCsvRowsToFeedbackRecords(
|
||||
csvRows,
|
||||
connector.fieldMappings,
|
||||
connector.workspaceId
|
||||
connector.feedbackRecordDirectoryId
|
||||
);
|
||||
|
||||
let successes = 0;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -41,7 +41,7 @@ const processConnector = async (
|
||||
response,
|
||||
survey,
|
||||
connector.formbricksMappings,
|
||||
workspaceId
|
||||
connector.feedbackRecordDirectoryId
|
||||
);
|
||||
|
||||
if (feedbackRecords.length === 0) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ファイルからフィードバックをインポート",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-файлов",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 文件导入反馈",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
-118
@@ -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>
|
||||
);
|
||||
};
|
||||
+94
-37
@@ -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>
|
||||
|
||||
+10
-6
@@ -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}
|
||||
/>
|
||||
|
||||
+156
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+5
@@ -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";
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user