mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-06 11:20:56 -05:00
feat: integrate hub feedback records into unify workspace (#7828)
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
|
||||
import { createFeedbackRecord, retrieveFeedbackRecord, updateFeedbackRecord } from "@/modules/hub/service";
|
||||
import type { FeedbackRecordCreateParams, FeedbackRecordUpdateParams } from "@/modules/hub/types";
|
||||
|
||||
const ZFeedbackRecordId = z.uuid();
|
||||
|
||||
const ZFeedbackRecordFieldType = z.enum([
|
||||
"text",
|
||||
"categorical",
|
||||
"nps",
|
||||
"csat",
|
||||
"ces",
|
||||
"rating",
|
||||
"number",
|
||||
"boolean",
|
||||
"date",
|
||||
]);
|
||||
|
||||
const ZFeedbackRecordMetadata = z.record(z.string(), z.unknown());
|
||||
|
||||
const ZFeedbackRecordCreateInput = z.object({
|
||||
submission_id: z.string().min(1),
|
||||
tenant_id: ZId,
|
||||
source_type: z.string().min(1),
|
||||
field_id: z.string().min(1),
|
||||
field_type: ZFeedbackRecordFieldType,
|
||||
collected_at: z.iso.datetime().optional(),
|
||||
source_id: z.string().optional().nullable(),
|
||||
source_name: z.string().optional().nullable(),
|
||||
field_label: z.string().optional().nullable(),
|
||||
field_group_id: z.string().optional(),
|
||||
field_group_label: z.string().optional().nullable(),
|
||||
value_text: z.string().optional().nullable(),
|
||||
value_number: z.number().optional(),
|
||||
value_boolean: z.boolean().optional(),
|
||||
value_date: z.iso.datetime().optional(),
|
||||
metadata: ZFeedbackRecordMetadata.optional(),
|
||||
language: z.string().optional(),
|
||||
user_identifier: z.string().optional(),
|
||||
});
|
||||
|
||||
const ZFeedbackRecordUpdateInput = z
|
||||
.object({
|
||||
value_text: z.string().optional().nullable(),
|
||||
value_number: z.number().optional().nullable(),
|
||||
value_boolean: z.boolean().optional().nullable(),
|
||||
value_date: z.iso.datetime().optional().nullable(),
|
||||
language: z.string().optional().nullable(),
|
||||
metadata: ZFeedbackRecordMetadata.optional(),
|
||||
user_identifier: z.string().optional().nullable(),
|
||||
})
|
||||
.refine(
|
||||
(value) => Object.values(value).some((entry) => entry !== undefined),
|
||||
"At least one field must be provided for update"
|
||||
);
|
||||
|
||||
const ZRetrieveFeedbackRecordAction = z.object({
|
||||
workspaceId: ZId,
|
||||
recordId: ZFeedbackRecordId,
|
||||
});
|
||||
|
||||
const ZCreateFeedbackRecordAction = z.object({
|
||||
workspaceId: ZId,
|
||||
recordInput: ZFeedbackRecordCreateInput,
|
||||
});
|
||||
|
||||
const ZUpdateFeedbackRecordAction = z.object({
|
||||
workspaceId: ZId,
|
||||
recordId: ZFeedbackRecordId,
|
||||
updateInput: ZFeedbackRecordUpdateInput,
|
||||
});
|
||||
|
||||
const ensureAccess = async (
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
minPermission: "read" | "readWrite"
|
||||
): Promise<void> => {
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
minPermission,
|
||||
workspaceId,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkspaceDirectoryIds = async (workspaceId: string): Promise<Set<string>> => {
|
||||
const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId);
|
||||
return new Set(directories.map((directory) => directory.id));
|
||||
};
|
||||
|
||||
const assertRecordBelongsToWorkspace = (directoryIds: Set<string>, tenantId: string): void => {
|
||||
if (!directoryIds.has(tenantId)) {
|
||||
// Throw a generic error indistinguishable from "not found" to prevent IDOR
|
||||
throw new Error("Feedback record not found");
|
||||
}
|
||||
};
|
||||
|
||||
export const retrieveFeedbackRecordAction = authenticatedActionClient
|
||||
.inputSchema(ZRetrieveFeedbackRecordAction)
|
||||
.action(
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZRetrieveFeedbackRecordAction>;
|
||||
}) => {
|
||||
const [, workspaceDirectoryIds] = await Promise.all([
|
||||
ensureAccess(ctx.user.id, parsedInput.workspaceId, "read"),
|
||||
getWorkspaceDirectoryIds(parsedInput.workspaceId),
|
||||
]);
|
||||
|
||||
const recordResult = await retrieveFeedbackRecord(parsedInput.recordId);
|
||||
if (!recordResult.data || recordResult.error) {
|
||||
throw new Error("Feedback record not found");
|
||||
}
|
||||
|
||||
assertRecordBelongsToWorkspace(workspaceDirectoryIds, recordResult.data.tenant_id);
|
||||
|
||||
return recordResult.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const createFeedbackRecordAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateFeedbackRecordAction)
|
||||
.action(
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateFeedbackRecordAction>;
|
||||
}) => {
|
||||
await ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite");
|
||||
|
||||
const workspaceDirectoryIds = await getWorkspaceDirectoryIds(parsedInput.workspaceId);
|
||||
assertRecordBelongsToWorkspace(workspaceDirectoryIds, parsedInput.recordInput.tenant_id);
|
||||
|
||||
const { recordInput } = parsedInput;
|
||||
const createParams: FeedbackRecordCreateParams = {
|
||||
submission_id: recordInput.submission_id,
|
||||
tenant_id: recordInput.tenant_id,
|
||||
source_type: recordInput.source_type,
|
||||
field_id: recordInput.field_id,
|
||||
field_type: recordInput.field_type,
|
||||
collected_at: recordInput.collected_at,
|
||||
source_id: recordInput.source_id,
|
||||
source_name: recordInput.source_name,
|
||||
field_label: recordInput.field_label,
|
||||
field_group_id: recordInput.field_group_id,
|
||||
field_group_label: recordInput.field_group_label,
|
||||
value_text: recordInput.value_text,
|
||||
value_number: recordInput.value_number,
|
||||
value_boolean: recordInput.value_boolean,
|
||||
value_date: recordInput.value_date,
|
||||
metadata: recordInput.metadata,
|
||||
language: recordInput.language,
|
||||
user_identifier: recordInput.user_identifier,
|
||||
};
|
||||
|
||||
const createResult = await createFeedbackRecord(createParams);
|
||||
if (!createResult.data || createResult.error) {
|
||||
throw new Error(createResult.error?.message || "Failed to create feedback record");
|
||||
}
|
||||
|
||||
return createResult.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateFeedbackRecordAction = authenticatedActionClient
|
||||
.inputSchema(ZUpdateFeedbackRecordAction)
|
||||
.action(
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZUpdateFeedbackRecordAction>;
|
||||
}) => {
|
||||
const [, workspaceDirectoryIds] = await Promise.all([
|
||||
ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite"),
|
||||
getWorkspaceDirectoryIds(parsedInput.workspaceId),
|
||||
]);
|
||||
|
||||
const currentRecordResult = await retrieveFeedbackRecord(parsedInput.recordId);
|
||||
if (!currentRecordResult.data || currentRecordResult.error) {
|
||||
throw new Error("Feedback record not found");
|
||||
}
|
||||
|
||||
assertRecordBelongsToWorkspace(workspaceDirectoryIds, currentRecordResult.data.tenant_id);
|
||||
|
||||
const { updateInput } = parsedInput;
|
||||
const updateParams: FeedbackRecordUpdateParams = {
|
||||
...(updateInput.value_text !== undefined && { value_text: updateInput.value_text ?? undefined }),
|
||||
...(updateInput.value_number !== undefined && {
|
||||
value_number: updateInput.value_number ?? undefined,
|
||||
}),
|
||||
...(updateInput.value_boolean !== undefined && {
|
||||
value_boolean: updateInput.value_boolean ?? undefined,
|
||||
}),
|
||||
...(updateInput.value_date !== undefined && { value_date: updateInput.value_date ?? undefined }),
|
||||
...(updateInput.language !== undefined && { language: updateInput.language ?? undefined }),
|
||||
...(updateInput.metadata !== undefined && { metadata: updateInput.metadata }),
|
||||
...(updateInput.user_identifier !== undefined && {
|
||||
user_identifier: updateInput.user_identifier ?? undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
const updateResult = await updateFeedbackRecord(parsedInput.recordId, updateParams);
|
||||
if (!updateResult.data || updateResult.error) {
|
||||
throw new Error(updateResult.error?.message || "Failed to update feedback record");
|
||||
}
|
||||
|
||||
return updateResult.data;
|
||||
}
|
||||
);
|
||||
+814
@@ -0,0 +1,814 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import type { FeedbackRecordData } from "@/modules/hub/types";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormError,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/modules/ui/components/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/modules/ui/components/sheet";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import {
|
||||
createFeedbackRecordAction,
|
||||
retrieveFeedbackRecordAction,
|
||||
updateFeedbackRecordAction,
|
||||
} from "../actions";
|
||||
import {
|
||||
FIELD_TYPE_OPTIONS,
|
||||
SOURCE_TYPE_CUSTOM_VALUE,
|
||||
SOURCE_TYPE_PRESET_OPTIONS,
|
||||
type TFeedbackRecordFormValues,
|
||||
ZFeedbackRecordFormValues,
|
||||
} from "../lib/types";
|
||||
import {
|
||||
formatSourceType,
|
||||
getCreateDefaults,
|
||||
getReadOnlyMetadataEntries,
|
||||
getValueFieldByType,
|
||||
isPresetSourceType,
|
||||
mapRecordToValues,
|
||||
parseNumberValue,
|
||||
toISOOrUndefined,
|
||||
} from "../lib/utils";
|
||||
|
||||
type FeedbackRecordDrawerMode = "create" | "edit";
|
||||
|
||||
interface FeedbackRecordFormDrawerProps {
|
||||
mode: FeedbackRecordDrawerMode;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workspaceId: string;
|
||||
directories: { id: string; name: string }[];
|
||||
canWrite: boolean;
|
||||
recordId?: string;
|
||||
onSuccess: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const FeedbackRecordFormDrawer = ({
|
||||
mode,
|
||||
open,
|
||||
onOpenChange,
|
||||
workspaceId,
|
||||
directories,
|
||||
canWrite,
|
||||
recordId,
|
||||
onSuccess,
|
||||
}: Readonly<FeedbackRecordFormDrawerProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const [record, setRecord] = useState<FeedbackRecordData | null>(null);
|
||||
const [isLoadingRecord, setIsLoadingRecord] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
||||
|
||||
const defaultValues = useMemo(() => getCreateDefaults(directories), [directories]);
|
||||
|
||||
const form = useForm<TFeedbackRecordFormValues>({
|
||||
resolver: zodResolver(ZFeedbackRecordFormValues),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "metadataEntries",
|
||||
});
|
||||
|
||||
const fieldType = form.watch("field_type");
|
||||
const selectedValueField = getValueFieldByType(fieldType);
|
||||
const isEditMode = mode === "edit";
|
||||
const isReadOnly = isEditMode && !canWrite;
|
||||
|
||||
const [sourceTypeMode, setSourceTypeMode] = useState<string>("survey");
|
||||
const [customSourceType, setCustomSourceType] = useState("");
|
||||
|
||||
const readOnlyMetadataEntries = useMemo(() => (record ? getReadOnlyMetadataEntries(record) : []), [record]);
|
||||
|
||||
const resetForCreate = useCallback(() => {
|
||||
const nextDefaults = getCreateDefaults(directories);
|
||||
form.reset(nextDefaults);
|
||||
setRecord(null);
|
||||
setSourceTypeMode(nextDefaults.source_type);
|
||||
setCustomSourceType("");
|
||||
}, [directories, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (mode === "create") {
|
||||
resetForCreate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recordId) return;
|
||||
|
||||
const loadRecord = async () => {
|
||||
setIsLoadingRecord(true);
|
||||
const result = await retrieveFeedbackRecordAction({ workspaceId, recordId });
|
||||
|
||||
if (!result?.data) {
|
||||
toast.error(getFormattedErrorMessage(result) || t("workspace.unify.failed_to_load_feedback_records"));
|
||||
setIsLoadingRecord(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setRecord(result.data);
|
||||
form.reset(mapRecordToValues(result.data));
|
||||
const isPreset = isPresetSourceType(result.data.source_type);
|
||||
setSourceTypeMode(isPreset ? result.data.source_type : SOURCE_TYPE_CUSTOM_VALUE);
|
||||
setCustomSourceType(isPreset ? "" : result.data.source_type);
|
||||
setIsLoadingRecord(false);
|
||||
};
|
||||
|
||||
void loadRecord();
|
||||
}, [form, mode, open, recordId, resetForCreate, t, workspaceId]);
|
||||
|
||||
const requestClose = useCallback(() => {
|
||||
if (form.formState.isDirty && !isSubmitting) {
|
||||
setIsDiscardDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenChange(false);
|
||||
}, [form.formState.isDirty, isSubmitting, onOpenChange]);
|
||||
|
||||
const handleDrawerOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
if (nextOpen) {
|
||||
onOpenChange(true);
|
||||
return;
|
||||
}
|
||||
|
||||
requestClose();
|
||||
},
|
||||
[onOpenChange, requestClose]
|
||||
);
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
setIsDiscardDialogOpen(false);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const setStrictValueValidationError = (message: string) => {
|
||||
form.setError(selectedValueField, { type: "manual", message });
|
||||
};
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
form.clearErrors();
|
||||
|
||||
if (mode === "create") {
|
||||
const requiredValueError = t("workspace.unify.feedback_record_value_required");
|
||||
if (selectedValueField === "value_text" && !values.value_text?.trim()) {
|
||||
setStrictValueValidationError(requiredValueError);
|
||||
return;
|
||||
}
|
||||
if (selectedValueField === "value_number" && parseNumberValue(values.value_number ?? "") == null) {
|
||||
setStrictValueValidationError(requiredValueError);
|
||||
return;
|
||||
}
|
||||
if (selectedValueField === "value_boolean" && values.value_boolean === undefined) {
|
||||
setStrictValueValidationError(requiredValueError);
|
||||
return;
|
||||
}
|
||||
if (selectedValueField === "value_date" && !toISOOrUndefined(values.value_date)) {
|
||||
setStrictValueValidationError(requiredValueError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = Object.fromEntries(
|
||||
values.metadataEntries
|
||||
.map((entry) => ({
|
||||
key: entry.key.trim(),
|
||||
value: entry.value,
|
||||
}))
|
||||
.filter((entry) => entry.key.length > 0)
|
||||
.map((entry) => [entry.key, entry.value])
|
||||
);
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
if (mode === "create") {
|
||||
const sourceTypeValue =
|
||||
sourceTypeMode === SOURCE_TYPE_CUSTOM_VALUE ? customSourceType.trim() : values.source_type;
|
||||
|
||||
const createResult = await createFeedbackRecordAction({
|
||||
workspaceId,
|
||||
recordInput: {
|
||||
submission_id: values.submission_id.trim(),
|
||||
tenant_id: values.tenant_id,
|
||||
source_type: sourceTypeValue,
|
||||
source_id: values.source_id?.trim() ? values.source_id.trim() : null,
|
||||
source_name: values.source_name?.trim() ? values.source_name.trim() : null,
|
||||
field_id: values.field_id.trim(),
|
||||
field_label: values.field_label?.trim() ? values.field_label.trim() : null,
|
||||
field_type: values.field_type,
|
||||
field_group_id: values.field_group_id?.trim() || undefined,
|
||||
field_group_label: values.field_group_label?.trim() ? values.field_group_label.trim() : null,
|
||||
collected_at: toISOOrUndefined(values.collected_at),
|
||||
value_text: selectedValueField === "value_text" ? (values.value_text ?? "") : null,
|
||||
value_number:
|
||||
selectedValueField === "value_number"
|
||||
? (parseNumberValue(values.value_number ?? "") ?? undefined)
|
||||
: undefined,
|
||||
value_boolean: selectedValueField === "value_boolean" ? values.value_boolean : undefined,
|
||||
value_date: selectedValueField === "value_date" ? toISOOrUndefined(values.value_date) : undefined,
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||
language: values.language?.trim() || undefined,
|
||||
user_identifier: values.user_identifier?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createResult?.data) {
|
||||
toast.error(getFormattedErrorMessage(createResult));
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!recordId) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const preservedMetadata = Object.fromEntries(
|
||||
Object.entries(record?.metadata ?? {}).filter(([, value]) => typeof value !== "string")
|
||||
);
|
||||
|
||||
const updatePayload: {
|
||||
language: string | null;
|
||||
user_identifier: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
value_text?: string;
|
||||
value_number?: number | null;
|
||||
value_boolean?: boolean | null;
|
||||
value_date?: string | null;
|
||||
} = {
|
||||
language: values.language?.trim() || null,
|
||||
user_identifier: values.user_identifier?.trim() || null,
|
||||
metadata: { ...preservedMetadata, ...metadata },
|
||||
};
|
||||
|
||||
if (selectedValueField === "value_text") {
|
||||
updatePayload.value_text = values.value_text?.trim() ?? "";
|
||||
} else if (selectedValueField === "value_number") {
|
||||
updatePayload.value_number = parseNumberValue(values.value_number ?? "");
|
||||
} else if (selectedValueField === "value_boolean") {
|
||||
updatePayload.value_boolean = values.value_boolean ?? null;
|
||||
} else if (selectedValueField === "value_date") {
|
||||
updatePayload.value_date = toISOOrUndefined(values.value_date) ?? null;
|
||||
}
|
||||
|
||||
const updateResult = await updateFeedbackRecordAction({
|
||||
workspaceId,
|
||||
recordId,
|
||||
updateInput: updatePayload,
|
||||
});
|
||||
|
||||
if (!updateResult?.data) {
|
||||
toast.error(getFormattedErrorMessage(updateResult));
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
mode === "create"
|
||||
? t("workspace.unify.feedback_record_created_successfully")
|
||||
: t("workspace.unify.feedback_record_updated_successfully")
|
||||
);
|
||||
await onSuccess();
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
});
|
||||
|
||||
const drawerTitle =
|
||||
mode === "create"
|
||||
? t("workspace.unify.add_feedback_record")
|
||||
: t("workspace.unify.feedback_record_details");
|
||||
|
||||
const drawerDescription =
|
||||
mode === "create"
|
||||
? t("workspace.unify.add_feedback_record_description")
|
||||
: t("workspace.unify.feedback_record_details_description");
|
||||
|
||||
const valueBooleanStatus = form.watch("value_boolean");
|
||||
let valueBooleanLabel = t("common.not_set");
|
||||
if (valueBooleanStatus === true) {
|
||||
valueBooleanLabel = t("common.yes");
|
||||
} else if (valueBooleanStatus === false) {
|
||||
valueBooleanLabel = t("common.no");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={handleDrawerOpenChange}>
|
||||
<SheetContent className="w-full overflow-y-auto bg-white px-5 sm:max-w-2xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{drawerTitle}</SheetTitle>
|
||||
<SheetDescription>{drawerDescription}</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{isLoadingRecord ? (
|
||||
<div className="py-8 text-sm text-slate-500">{t("common.loading")}</div>
|
||||
) : (
|
||||
<FormProvider {...form}>
|
||||
<form className="space-y-4 py-4" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.id")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tenant_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.feedback_record_directory")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange} disabled>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("workspace.unify.select_feedback_record_directory")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{directories.map((directory) => (
|
||||
<SelectItem key={directory.id} value={directory.id}>
|
||||
{directory.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="submission_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.submission_id")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="collected_at"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.collected_at")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="datetime-local" disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="created_at"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.created_at")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="updated_at"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.updated_at")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEditMode ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="source_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.source_type")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={formatSourceType(field.value, t)} disabled />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<FormLabel>{t("workspace.unify.source_type")}</FormLabel>
|
||||
<Select
|
||||
value={sourceTypeMode}
|
||||
onValueChange={(value) => {
|
||||
setSourceTypeMode(value);
|
||||
if (value !== SOURCE_TYPE_CUSTOM_VALUE) {
|
||||
form.setValue("source_type", value, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
disabled={!canWrite}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("workspace.unify.select_feedback_record_source_type")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SOURCE_TYPE_PRESET_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={SOURCE_TYPE_CUSTOM_VALUE}>
|
||||
{t("workspace.unify.custom_source_type")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{sourceTypeMode === SOURCE_TYPE_CUSTOM_VALUE && (
|
||||
<Input
|
||||
value={customSourceType}
|
||||
onChange={(event) => {
|
||||
setCustomSourceType(event.target.value);
|
||||
form.setValue("source_type", event.target.value, { shouldDirty: true });
|
||||
}}
|
||||
placeholder={t("workspace.unify.custom_source_type_placeholder")}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
)}
|
||||
<FormError>{form.formState.errors.source_type?.message}</FormError>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="source_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.source_id")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="source_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.source_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="field_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.field_id")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="field_label"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.field_label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="field_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.field_type")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value as TFeedbackRecordFormValues["field_type"])
|
||||
}
|
||||
disabled={isEditMode || !canWrite}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FIELD_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="field_group_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.field_group_id")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="field_group_label"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.field_group_label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode || !canWrite} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value_text"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.value_text")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
disabled={selectedValueField !== "value_text" || isReadOnly || !canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value_number"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.value_number")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
type="number"
|
||||
step="any"
|
||||
disabled={selectedValueField !== "value_number" || isReadOnly || !canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.value_date")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
type="datetime-local"
|
||||
disabled={selectedValueField !== "value_date" || isReadOnly || !canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value_boolean"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.value_boolean")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3 rounded-md border border-slate-200 px-3 py-2">
|
||||
<Switch
|
||||
checked={field.value ?? false}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
disabled={selectedValueField !== "value_boolean" || isReadOnly || !canWrite}
|
||||
/>
|
||||
<span className="text-sm text-slate-600">{valueBooleanLabel}</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormError>{form.formState.errors[selectedValueField]?.message}</FormError>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.language")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={!canWrite || isReadOnly} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user_identifier"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.unify.user_identifier")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={!canWrite || isReadOnly} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>{t("workspace.unify.metadata")}</FormLabel>
|
||||
{canWrite && !isReadOnly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => append({ key: "", value: "" })}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="grid grid-cols-[1fr_1fr_auto] gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`metadataEntries.${index}.key`}
|
||||
render={({ field: entryField }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...entryField}
|
||||
placeholder={t("workspace.unify.metadata_key")}
|
||||
disabled={isReadOnly || !canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`metadataEntries.${index}.value`}
|
||||
render={({ field: entryField }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...entryField}
|
||||
placeholder={t("workspace.unify.metadata_value")}
|
||||
disabled={isReadOnly || !canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{canWrite && !isReadOnly && (
|
||||
<Button type="button" variant="outline" onClick={() => remove(index)}>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{readOnlyMetadataEntries.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("workspace.unify.metadata_read_only_entries")}
|
||||
</p>
|
||||
{readOnlyMetadataEntries.map((entry) => (
|
||||
<div
|
||||
key={entry.key}
|
||||
className="grid grid-cols-2 gap-2 rounded-md bg-slate-50 p-2 text-xs">
|
||||
<span className="font-medium text-slate-700">{entry.key}</span>
|
||||
<span className="truncate text-slate-600" title={entry.value}>
|
||||
{entry.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)}
|
||||
|
||||
<SheetFooter className="mt-2">
|
||||
<Button variant="outline" onClick={requestClose} disabled={isSubmitting}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button onClick={handleSubmit} loading={isSubmitting} disabled={isLoadingRecord}>
|
||||
{mode === "create" ? t("workspace.unify.add_feedback_record") : t("common.save")}
|
||||
</Button>
|
||||
)}
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<AlertDialog
|
||||
open={isDiscardDialogOpen}
|
||||
setOpen={setIsDiscardDialogOpen}
|
||||
headerText={t("workspace.unify.discard_feedback_record_changes_title")}
|
||||
mainText={t("workspace.unify.discard_feedback_record_changes_description")}
|
||||
confirmBtnLabel={t("common.discard")}
|
||||
declineBtnLabel={t("common.cancel")}
|
||||
declineBtnVariant="outline"
|
||||
onDecline={() => setIsDiscardDialogOpen(false)}
|
||||
onConfirm={handleDiscardChanges}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+12
-12
@@ -4,38 +4,38 @@ import { useTranslation } from "react-i18next";
|
||||
import type { FeedbackRecordData } from "@/modules/hub/types";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { UnifyConfigNavigation } from "../components/UnifyConfigNavigation";
|
||||
import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation";
|
||||
import { FeedbackRecordsTable } from "./feedback-records-table";
|
||||
|
||||
interface FeedbackRecordsPageClientProps {
|
||||
workspaceId: string;
|
||||
directories: { id: string; name: string }[];
|
||||
initialFrdId: string | null;
|
||||
initialRecords: FeedbackRecordData[];
|
||||
initialNextCursor?: string;
|
||||
frdMap: Record<string, string>;
|
||||
csvSources: { id: string; name: string }[];
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export function FeedbackRecordsPageClient({
|
||||
workspaceId,
|
||||
directories,
|
||||
initialFrdId,
|
||||
initialRecords,
|
||||
initialNextCursor,
|
||||
}: FeedbackRecordsPageClientProps) {
|
||||
frdMap,
|
||||
csvSources,
|
||||
canWrite,
|
||||
}: Readonly<FeedbackRecordsPageClientProps>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.unify.unify_feedback")}>
|
||||
<PageHeader pageTitle={t("workspace.unify.feedback_records")}>
|
||||
<UnifyConfigNavigation workspaceId={workspaceId} activeId="feedback-records" />
|
||||
</PageHeader>
|
||||
|
||||
<FeedbackRecordsTable
|
||||
workspaceId={workspaceId}
|
||||
directories={directories}
|
||||
initialFrdId={initialFrdId}
|
||||
initialRecords={initialRecords}
|
||||
initialNextCursor={initialNextCursor}
|
||||
frdMap={frdMap}
|
||||
csvSources={csvSources}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
"use client";
|
||||
|
||||
import { TFunction } from "i18next";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronDownIcon,
|
||||
HashIcon,
|
||||
MessageSquareTextIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
ToggleLeftIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, 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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { CsvImportModal } from "../../sources/components/csv-import-modal";
|
||||
import { formatSourceType } from "../lib/utils";
|
||||
import { FeedbackRecordFormDrawer } from "./feedback-record-form-drawer";
|
||||
|
||||
const RECORDS_PER_PAGE = 50;
|
||||
|
||||
const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
|
||||
text: <TypeIcon className="h-3.5 w-3.5" />,
|
||||
categorical: <HashIcon className="h-3.5 w-3.5" />,
|
||||
nps: <HashIcon className="h-3.5 w-3.5" />,
|
||||
csat: <HashIcon className="h-3.5 w-3.5" />,
|
||||
ces: <HashIcon className="h-3.5 w-3.5" />,
|
||||
rating: <HashIcon className="h-3.5 w-3.5" />,
|
||||
number: <HashIcon className="h-3.5 w-3.5" />,
|
||||
boolean: <ToggleLeftIcon className="h-3.5 w-3.5" />,
|
||||
date: <CalendarIcon className="h-3.5 w-3.5" />,
|
||||
};
|
||||
|
||||
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 formatDateForDisplay(new Date(record.value_date), locale);
|
||||
return "—";
|
||||
};
|
||||
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen) + "…";
|
||||
}
|
||||
|
||||
interface FeedbackRecordsTableProps {
|
||||
workspaceId: string;
|
||||
initialRecords: FeedbackRecordData[];
|
||||
frdMap: Record<string, string>;
|
||||
csvSources: { id: string; name: string }[];
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export const FeedbackRecordsTable = ({
|
||||
workspaceId,
|
||||
initialRecords,
|
||||
frdMap,
|
||||
csvSources,
|
||||
canWrite,
|
||||
}: Readonly<FeedbackRecordsTableProps>) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [records, setRecords] = useState<FeedbackRecordData[]>(initialRecords);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [drawerMode, setDrawerMode] = useState<"create" | "edit">("edit");
|
||||
const [drawerRecordId, setDrawerRecordId] = useState<string | undefined>();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [csvImportSource, setCsvImportSource] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
const directories = useMemo(
|
||||
() =>
|
||||
Object.entries(frdMap)
|
||||
.map(([id, name]) => ({ id, name }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[frdMap]
|
||||
);
|
||||
const handleRefresh = async () => {
|
||||
if (isRefreshing) return;
|
||||
setIsRefreshing(true);
|
||||
setError(null);
|
||||
|
||||
const toastId = toast.loading(t("workspace.unify.refreshing_feedback_records"));
|
||||
const directoryIds = Object.keys(frdMap);
|
||||
const results = await Promise.all(
|
||||
directoryIds.map((frdId) =>
|
||||
listFeedbackRecordsAction({
|
||||
workspaceId,
|
||||
frdId,
|
||||
limit: RECORDS_PER_PAGE,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const successfulRecords = results.flatMap((result) => result?.data?.data ?? []);
|
||||
|
||||
if (directoryIds.length > 0 && successfulRecords.length === 0) {
|
||||
const firstErrorResult = results.find((result) => !result?.data);
|
||||
const errorMessage = firstErrorResult ? getFormattedErrorMessage(firstErrorResult) : undefined;
|
||||
toast.error(errorMessage ?? t("workspace.unify.failed_to_load_feedback_records"), {
|
||||
id: toastId,
|
||||
});
|
||||
setIsRefreshing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedRecords = successfulRecords
|
||||
.toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1))
|
||||
.slice(0, RECORDS_PER_PAGE);
|
||||
setRecords(mergedRecords);
|
||||
setIsRefreshing(false);
|
||||
toast.success(t("workspace.unify.feedback_records_refreshed"), { id: toastId });
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="flex h-48 flex-col items-center justify-center gap-3 px-4 text-center">
|
||||
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
|
||||
<p className="text-sm text-slate-500">{error}</p>
|
||||
<Button variant="secondary" size="sm" onClick={handleRefresh}>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmpty = records.length === 0 && !isRefreshing;
|
||||
|
||||
const openEditDrawer = (recordId: string) => {
|
||||
setDrawerMode("edit");
|
||||
setDrawerRecordId(recordId);
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const openCreateDrawer = () => {
|
||||
setDrawerMode("create");
|
||||
setDrawerRecordId(undefined);
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const hasCsvSources = csvSources.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{isEmpty ? (
|
||||
<span />
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("workspace.unify.showing_count_loaded", {
|
||||
count: records.length,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{canWrite &&
|
||||
(hasCsvSources ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="secondary">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t("workspace.unify.add_feedback_record")}
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={openCreateDrawer}>
|
||||
{t("workspace.unify.add_feedback_record")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{csvSources.map((source) => (
|
||||
<DropdownMenuItem
|
||||
key={source.id}
|
||||
onClick={() => {
|
||||
setCsvImportSource(source);
|
||||
}}>
|
||||
{t("workspace.unify.import_via_source_name", { sourceName: source.name })}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button size="sm" variant="secondary" onClick={openCreateDrawer}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{t("workspace.unify.add_feedback_record")}
|
||||
</Button>
|
||||
))}
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/workspaces/${workspaceId}/feedback-sources`}>
|
||||
{t("workspace.unify.manage_feedback_sources")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label={t("workspace.unify.refresh_feedback_records")}>
|
||||
<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">
|
||||
<table className="w-full min-w-[900px]">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 text-left text-sm text-slate-900 [&>th]:font-semibold">
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.collected_at")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_type")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_name")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_label")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_type")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.value")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.user_identifier")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{isEmpty ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<p className="text-sm text-slate-500">{t("workspace.unify.no_feedback_records")}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{records.map((record) => (
|
||||
<FeedbackRecordRow
|
||||
key={record.id}
|
||||
record={record}
|
||||
workspaceId={workspaceId}
|
||||
locale={i18n.resolvedLanguage ?? i18n.language ?? "en-US"}
|
||||
t={t}
|
||||
onClick={() => openEditDrawer(record.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FeedbackRecordFormDrawer
|
||||
mode={drawerMode}
|
||||
open={isDrawerOpen}
|
||||
onOpenChange={setIsDrawerOpen}
|
||||
workspaceId={workspaceId}
|
||||
directories={directories}
|
||||
canWrite={canWrite}
|
||||
recordId={drawerMode === "edit" ? drawerRecordId : undefined}
|
||||
onSuccess={handleRefresh}
|
||||
/>
|
||||
|
||||
{csvImportSource && (
|
||||
<CsvImportModal
|
||||
open={csvImportSource !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCsvImportSource(null);
|
||||
}
|
||||
}}
|
||||
connectorId={csvImportSource.id}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FeedbackRecordRow = ({
|
||||
record,
|
||||
workspaceId,
|
||||
locale,
|
||||
t,
|
||||
onClick,
|
||||
}: {
|
||||
record: FeedbackRecordData;
|
||||
workspaceId: string;
|
||||
locale: string;
|
||||
t: TFunction;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const value = formatValue(record, t, locale);
|
||||
const isLongValue = value.length > 60;
|
||||
const isFormbricksSurveySource =
|
||||
(record.source_type === "formbricks" || record.source_type === "formbricks_survey") && !!record.source_id;
|
||||
const surveySummaryHref = `/workspaces/${workspaceId}/surveys/${record.source_id}/summary`;
|
||||
|
||||
return (
|
||||
<tr
|
||||
className="cursor-pointer text-sm text-slate-700 transition-colors hover:bg-slate-50"
|
||||
onClick={onClick}>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
|
||||
{formatDateTimeForDisplay(new Date(record.collected_at), locale)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3">
|
||||
<Badge text={formatSourceType(record.source_type, t)} type="gray" size="tiny" />
|
||||
</td>
|
||||
<td className="max-w-[150px] truncate px-4 py-3" title={record.source_name ?? undefined}>
|
||||
{isFormbricksSurveySource ? (
|
||||
<Link
|
||||
href={surveySummaryHref}
|
||||
className="text-slate-700 underline underline-offset-2 hover:text-slate-900"
|
||||
onClick={(event) => event.stopPropagation()}>
|
||||
{record.source_name ?? "—"}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{record.source_name ?? "—"}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="max-w-[200px] truncate px-4 py-3" title={record.field_label ?? undefined}>
|
||||
{record.field_label ?? record.field_id}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3">
|
||||
<span className="inline-flex items-center gap-1 text-slate-600">
|
||||
{FIELD_TYPE_ICONS[record.field_type] ?? <HashIcon className="h-3.5 w-3.5" />}
|
||||
{record.field_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="max-w-[250px] px-4 py-3">
|
||||
{isLongValue ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-default truncate">{truncate(value, 60)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-sm whitespace-pre-wrap">
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<span>{value}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="max-w-[120px] truncate px-4 py-3 text-slate-500" title={record.user_identifier}>
|
||||
{record.user_identifier ?? "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
-303
@@ -1,303 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TFunction } from "i18next";
|
||||
import {
|
||||
CalendarIcon,
|
||||
HashIcon,
|
||||
MessageSquareTextIcon,
|
||||
RefreshCwIcon,
|
||||
ToggleLeftIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
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;
|
||||
|
||||
const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
|
||||
text: <TypeIcon className="h-3.5 w-3.5" />,
|
||||
categorical: <HashIcon className="h-3.5 w-3.5" />,
|
||||
nps: <HashIcon className="h-3.5 w-3.5" />,
|
||||
csat: <HashIcon className="h-3.5 w-3.5" />,
|
||||
ces: <HashIcon className="h-3.5 w-3.5" />,
|
||||
rating: <HashIcon className="h-3.5 w-3.5" />,
|
||||
number: <HashIcon className="h-3.5 w-3.5" />,
|
||||
boolean: <ToggleLeftIcon className="h-3.5 w-3.5" />,
|
||||
date: <CalendarIcon className="h-3.5 w-3.5" />,
|
||||
};
|
||||
|
||||
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 formatDateForDisplay(new Date(record.value_date), locale);
|
||||
return "—";
|
||||
};
|
||||
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen) + "…";
|
||||
}
|
||||
|
||||
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);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchRecords = useCallback(
|
||||
async (frdId: string, cursor: string | undefined, append: boolean): Promise<string | null> => {
|
||||
const setLoading = append ? setIsLoadingMore : setIsRefreshing;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await listFeedbackRecordsAction({
|
||||
workspaceId,
|
||||
frdId,
|
||||
limit: RECORDS_PER_PAGE,
|
||||
cursor,
|
||||
});
|
||||
|
||||
if (!result?.data) {
|
||||
const message =
|
||||
getFormattedErrorMessage(result) ?? t("workspace.unify.failed_to_load_feedback_records");
|
||||
setError(message);
|
||||
setLoading(false);
|
||||
return message;
|
||||
}
|
||||
|
||||
const response = result.data;
|
||||
setRecords((prev) => (append ? [...prev, ...response.data] : response.data));
|
||||
setNextCursor(response.next_cursor);
|
||||
setLoading(false);
|
||||
return null;
|
||||
},
|
||||
[workspaceId, t]
|
||||
);
|
||||
|
||||
const handleFrdChange = (frdId: string) => {
|
||||
setSelectedFrdId(frdId);
|
||||
setRecords([]);
|
||||
setNextCursor(undefined);
|
||||
fetchRecords(frdId, undefined, false);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!selectedFrdId) return;
|
||||
fetchRecords(selectedFrdId, nextCursor, true);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!selectedFrdId || isRefreshing) return;
|
||||
const toastId = toast.loading(t("workspace.unify.refreshing_feedback_records"));
|
||||
const errorMessage = await fetchRecords(selectedFrdId, undefined, false);
|
||||
if (errorMessage) {
|
||||
toast.error(errorMessage, { id: toastId });
|
||||
return;
|
||||
}
|
||||
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 (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="flex h-48 flex-col items-center justify-center gap-3 px-4 text-center">
|
||||
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
|
||||
<p className="text-sm text-slate-500">{error}</p>
|
||||
<Button variant="secondary" size="sm" onClick={handleRefresh}>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<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"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label={t("workspace.unify.refresh_feedback_records")}>
|
||||
<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">
|
||||
<table className="w-full min-w-[900px]">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 text-left text-sm text-slate-900 [&>th]:font-semibold">
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.collected_at")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_type")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_name")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_label")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_type")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.value")}</th>
|
||||
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.user_identifier")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{isEmpty ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<p className="text-sm text-slate-500">{t("workspace.unify.no_feedback_records")}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{records.map((record) => (
|
||||
<FeedbackRecordRow key={record.id} record={record} locale={locale} t={t} />
|
||||
))}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button variant="secondary" size="sm" onClick={handleLoadMore} loading={isLoadingMore}>
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeedbackRecordRow = ({
|
||||
record,
|
||||
locale,
|
||||
t,
|
||||
}: {
|
||||
record: FeedbackRecordData;
|
||||
locale: string;
|
||||
t: TFunction;
|
||||
}) => {
|
||||
const value = formatValue(record, t, locale);
|
||||
const isLongValue = value.length > 60;
|
||||
|
||||
return (
|
||||
<tr className="text-sm text-slate-700 transition-colors hover:bg-slate-50">
|
||||
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
|
||||
{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" />
|
||||
</td>
|
||||
<td className="max-w-[150px] truncate px-4 py-3" title={record.source_name ?? undefined}>
|
||||
{record.source_name ?? "—"}
|
||||
</td>
|
||||
<td className="max-w-[200px] truncate px-4 py-3" title={record.field_label ?? undefined}>
|
||||
{record.field_label ?? record.field_id}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3">
|
||||
<span className="inline-flex items-center gap-1 text-slate-600">
|
||||
{FIELD_TYPE_ICONS[record.field_type] ?? <HashIcon className="h-3.5 w-3.5" />}
|
||||
{record.field_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="max-w-[250px] px-4 py-3">
|
||||
{isLongValue ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-default truncate">{truncate(value, 60)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-sm whitespace-pre-wrap">
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<span>{value}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="max-w-[120px] truncate px-4 py-3 text-slate-500" title={record.user_identifier}>
|
||||
{record.user_identifier ?? "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const FIELD_TYPE_OPTIONS = [
|
||||
"text",
|
||||
"categorical",
|
||||
"nps",
|
||||
"csat",
|
||||
"ces",
|
||||
"rating",
|
||||
"number",
|
||||
"boolean",
|
||||
"date",
|
||||
] as const;
|
||||
|
||||
export const SOURCE_TYPE_PRESET_OPTIONS = [
|
||||
"survey",
|
||||
"review",
|
||||
"feedback_form",
|
||||
"support",
|
||||
"social",
|
||||
"interview",
|
||||
"usability_test",
|
||||
"nps_campaign",
|
||||
] as const;
|
||||
|
||||
export const SOURCE_TYPE_CUSTOM_VALUE = "__custom__";
|
||||
|
||||
const ZMetadataEntry = z.object({
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const ZFeedbackRecordFormValues = z.object({
|
||||
id: z.string().optional(),
|
||||
tenant_id: z.string().min(1),
|
||||
submission_id: z.string().min(1),
|
||||
collected_at: z.string().min(1),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
source_type: z.string().min(1),
|
||||
source_id: z.string().optional(),
|
||||
source_name: z.string().optional(),
|
||||
field_id: z.string().min(1),
|
||||
field_label: z.string().optional(),
|
||||
field_type: z.enum(FIELD_TYPE_OPTIONS),
|
||||
field_group_id: z.string().optional(),
|
||||
field_group_label: z.string().optional(),
|
||||
value_text: z.string().optional(),
|
||||
value_number: z.string().optional(),
|
||||
value_boolean: z.boolean().optional(),
|
||||
value_date: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
user_identifier: z.string().optional(),
|
||||
metadataEntries: z.array(ZMetadataEntry),
|
||||
});
|
||||
|
||||
export type TFeedbackRecordFormValues = z.infer<typeof ZFeedbackRecordFormValues>;
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { FeedbackRecordData } from "@/modules/hub/types";
|
||||
import {
|
||||
formatSourceType,
|
||||
getCreateDefaults,
|
||||
getReadOnlyMetadataEntries,
|
||||
getValueFieldByType,
|
||||
isPresetSourceType,
|
||||
mapRecordToValues,
|
||||
parseNumberValue,
|
||||
toISOOrUndefined,
|
||||
toLocalDateTimeInput,
|
||||
} from "./utils";
|
||||
|
||||
vi.mock("uuid", () => ({ v7: () => "mock-uuid-v7" }));
|
||||
|
||||
const makeRecord = (overrides: Partial<FeedbackRecordData> = {}): FeedbackRecordData => ({
|
||||
id: "rec-1",
|
||||
tenant_id: "tenant-1",
|
||||
submission_id: "sub-1",
|
||||
collected_at: "2026-03-15T12:00:00.000Z",
|
||||
created_at: "2026-03-15T12:00:00.000Z",
|
||||
updated_at: "2026-03-15T12:00:00.000Z",
|
||||
source_type: "survey",
|
||||
field_id: "f1",
|
||||
field_type: "text",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("getValueFieldByType", () => {
|
||||
test.each([
|
||||
["boolean", "value_boolean"],
|
||||
["date", "value_date"],
|
||||
["nps", "value_number"],
|
||||
["csat", "value_number"],
|
||||
["ces", "value_number"],
|
||||
["rating", "value_number"],
|
||||
["number", "value_number"],
|
||||
["text", "value_text"],
|
||||
["categorical", "value_text"],
|
||||
] as const)("returns %s → %s", (input, expected) => {
|
||||
expect(getValueFieldByType(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toLocalDateTimeInput", () => {
|
||||
test("formats valid ISO date", () => {
|
||||
const result = toLocalDateTimeInput("2026-03-15T14:30:00.000Z");
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
test("returns empty string for invalid date", () => {
|
||||
expect(toLocalDateTimeInput("not-a-date")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toISOOrUndefined", () => {
|
||||
test("returns ISO string for valid input", () => {
|
||||
expect(toISOOrUndefined("2026-03-15T14:30")).toMatch(/2026-03-15/);
|
||||
});
|
||||
|
||||
test("returns undefined for empty string", () => {
|
||||
expect(toISOOrUndefined("")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for undefined", () => {
|
||||
expect(toISOOrUndefined(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for invalid date", () => {
|
||||
expect(toISOOrUndefined("not-a-date")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCreateDefaults", () => {
|
||||
test("uses first directory as tenant_id", () => {
|
||||
const dirs = [{ id: "dir-1", name: "Dir 1" }];
|
||||
const result = getCreateDefaults(dirs);
|
||||
expect(result.tenant_id).toBe("dir-1");
|
||||
expect(result.submission_id).toBe("mock-uuid-v7");
|
||||
expect(result.field_type).toBe("text");
|
||||
expect(result.metadataEntries).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles empty directories", () => {
|
||||
const result = getCreateDefaults([]);
|
||||
expect(result.tenant_id).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapRecordToValues", () => {
|
||||
test("maps a full record", () => {
|
||||
const record = makeRecord({
|
||||
value_text: "hello",
|
||||
value_number: 42,
|
||||
source_id: "s1",
|
||||
source_name: "Survey",
|
||||
metadata: { tag: "vip", nested: { a: 1 } },
|
||||
});
|
||||
const result = mapRecordToValues(record);
|
||||
expect(result.id).toBe("rec-1");
|
||||
expect(result.value_text).toBe("hello");
|
||||
expect(result.value_number).toBe("42");
|
||||
expect(result.source_id).toBe("s1");
|
||||
expect(result.metadataEntries).toEqual([{ key: "tag", value: "vip" }]);
|
||||
});
|
||||
|
||||
test("handles nullish optional fields", () => {
|
||||
const record = makeRecord({ value_number: undefined, source_id: undefined });
|
||||
const result = mapRecordToValues(record);
|
||||
expect(result.value_number).toBe("");
|
||||
expect(result.source_id).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReadOnlyMetadataEntries", () => {
|
||||
test("returns only non-string metadata values", () => {
|
||||
const record = makeRecord({ metadata: { tag: "vip", count: 5, nested: { a: 1 } } });
|
||||
const result = getReadOnlyMetadataEntries(record);
|
||||
expect(result).toEqual([
|
||||
{ key: "count", value: "5" },
|
||||
{ key: "nested", value: '{"a":1}' },
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns empty array when no metadata", () => {
|
||||
expect(getReadOnlyMetadataEntries(makeRecord())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseNumberValue", () => {
|
||||
test.each([
|
||||
["42", 42],
|
||||
["3.14", 3.14],
|
||||
["-1", -1],
|
||||
["", null],
|
||||
[" ", null],
|
||||
["abc", null],
|
||||
["Infinity", null],
|
||||
])("parseNumberValue(%s) → %s", (input, expected) => {
|
||||
expect(parseNumberValue(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPresetSourceType", () => {
|
||||
test("returns true for preset values", () => {
|
||||
expect(isPresetSourceType("survey")).toBe(true);
|
||||
expect(isPresetSourceType("nps_campaign")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for custom values", () => {
|
||||
expect(isPresetSourceType("custom_type")).toBe(false);
|
||||
expect(isPresetSourceType("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSourceType", () => {
|
||||
const t = ((key: string) => key) as any;
|
||||
|
||||
test("maps known source types", () => {
|
||||
expect(formatSourceType("formbricks", t)).toBe("workspace.unify.formbricks_surveys");
|
||||
expect(formatSourceType("formbricks_survey", t)).toBe("workspace.unify.formbricks_surveys");
|
||||
expect(formatSourceType("csv", t)).toBe("workspace.unify.csv_import");
|
||||
});
|
||||
|
||||
test("returns raw value for unknown types", () => {
|
||||
expect(formatSourceType("custom", t)).toBe("custom");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import type { FeedbackRecordData } from "@/modules/hub/types";
|
||||
import { SOURCE_TYPE_PRESET_OPTIONS, type TFeedbackRecordFormValues } from "./types";
|
||||
|
||||
export const getValueFieldByType = (
|
||||
fieldType: TFeedbackRecordFormValues["field_type"]
|
||||
): "value_text" | "value_number" | "value_boolean" | "value_date" => {
|
||||
switch (fieldType) {
|
||||
case "boolean":
|
||||
return "value_boolean";
|
||||
case "date":
|
||||
return "value_date";
|
||||
case "nps":
|
||||
case "csat":
|
||||
case "ces":
|
||||
case "rating":
|
||||
case "number":
|
||||
return "value_number";
|
||||
default:
|
||||
return "value_text";
|
||||
}
|
||||
};
|
||||
|
||||
export const toLocalDateTimeInput = (isoDate: string): string => {
|
||||
const date = new Date(isoDate);
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
export const toISOOrUndefined = (dateTimeValue: string | undefined): string | undefined => {
|
||||
if (!dateTimeValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = new Date(dateTimeValue);
|
||||
if (!Number.isFinite(parsed.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed.toISOString();
|
||||
};
|
||||
|
||||
export const getCreateDefaults = (directories: { id: string; name: string }[]): TFeedbackRecordFormValues => {
|
||||
const now = new Date();
|
||||
const defaultDirectoryId = directories[0]?.id ?? "";
|
||||
|
||||
return {
|
||||
id: "",
|
||||
tenant_id: defaultDirectoryId,
|
||||
submission_id: uuidv7(),
|
||||
collected_at: toLocalDateTimeInput(now.toISOString()),
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
source_type: "survey",
|
||||
source_id: "",
|
||||
source_name: "",
|
||||
field_id: "",
|
||||
field_label: "",
|
||||
field_type: "text",
|
||||
field_group_id: "",
|
||||
field_group_label: "",
|
||||
value_text: "",
|
||||
value_number: "",
|
||||
value_boolean: undefined,
|
||||
value_date: "",
|
||||
language: "",
|
||||
user_identifier: "",
|
||||
metadataEntries: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRecordToValues = (record: FeedbackRecordData): TFeedbackRecordFormValues => {
|
||||
const metadataEntries = Object.entries(record.metadata ?? {})
|
||||
.filter(([, value]) => typeof value === "string")
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: value as string,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
tenant_id: record.tenant_id,
|
||||
submission_id: record.submission_id,
|
||||
collected_at: toLocalDateTimeInput(record.collected_at),
|
||||
created_at: record.created_at ? toLocalDateTimeInput(record.created_at) : "",
|
||||
updated_at: record.updated_at ? toLocalDateTimeInput(record.updated_at) : "",
|
||||
source_type: record.source_type,
|
||||
source_id: record.source_id ?? "",
|
||||
source_name: record.source_name ?? "",
|
||||
field_id: record.field_id,
|
||||
field_label: record.field_label ?? "",
|
||||
field_type: record.field_type,
|
||||
field_group_id: record.field_group_id ?? "",
|
||||
field_group_label: record.field_group_label ?? "",
|
||||
value_text: record.value_text ?? "",
|
||||
value_number: record.value_number == null ? "" : String(record.value_number),
|
||||
value_boolean: record.value_boolean,
|
||||
value_date: record.value_date ? toLocalDateTimeInput(record.value_date) : "",
|
||||
language: record.language ?? "",
|
||||
user_identifier: record.user_identifier ?? "",
|
||||
metadataEntries,
|
||||
};
|
||||
};
|
||||
|
||||
export const getReadOnlyMetadataEntries = (record: FeedbackRecordData): { key: string; value: string }[] => {
|
||||
return Object.entries(record.metadata ?? {})
|
||||
.filter(([, value]) => typeof value !== "string")
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
}));
|
||||
};
|
||||
|
||||
export const parseNumberValue = (value: string): number | null => {
|
||||
if (value.trim() === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
export const isPresetSourceType = (value: string): value is (typeof SOURCE_TYPE_PRESET_OPTIONS)[number] =>
|
||||
(SOURCE_TYPE_PRESET_OPTIONS as readonly string[]).includes(value);
|
||||
|
||||
export const formatSourceType = (sourceType: string, t: TFunction): string => {
|
||||
switch (sourceType) {
|
||||
case "formbricks":
|
||||
case "formbricks_survey":
|
||||
return t("workspace.unify.formbricks_surveys");
|
||||
case "csv":
|
||||
return t("workspace.unify.csv_import");
|
||||
default:
|
||||
return sourceType;
|
||||
}
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getConnectorsWithMappings } from "@/lib/connector/service";
|
||||
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";
|
||||
import { FeedbackRecordsPageClient } from "./components/feedback-records-page-client";
|
||||
|
||||
const INITIAL_PAGE_SIZE = 10;
|
||||
const INITIAL_PAGE_SIZE = 50;
|
||||
|
||||
export default async function UnifyFeedbackRecordsPage(props: {
|
||||
readonly 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,31 +22,40 @@ export default async function UnifyFeedbackRecordsPage(props: {
|
||||
}
|
||||
|
||||
const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess;
|
||||
const canWrite = isOwner || isManager || hasReadWriteAccess || hasManageAccess;
|
||||
if (!hasAccess) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const frds = await getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId);
|
||||
const [frds, connectors] = await Promise.all([
|
||||
getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId),
|
||||
getConnectorsWithMappings(params.workspaceId),
|
||||
]);
|
||||
|
||||
// Preload first FRD's records server-side for fast initial render
|
||||
const initialFrdId = frds[0]?.id;
|
||||
let initialRecords: FeedbackRecordListResponse | null = null;
|
||||
const results = await Promise.all(
|
||||
frds.map((frd) => listFeedbackRecords({ tenant_id: frd.id, limit: INITIAL_PAGE_SIZE }))
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Don't crash if Hub is unreachable — show empty state
|
||||
const successfulResults = results.filter((r) => !r.error);
|
||||
|
||||
const merged = successfulResults
|
||||
.flatMap((r) => r.data?.data ?? [])
|
||||
.toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1))
|
||||
.slice(0, INITIAL_PAGE_SIZE);
|
||||
|
||||
const frdMap = Object.fromEntries(frds.map((f) => [f.id, f.name]));
|
||||
const csvSources = connectors
|
||||
.filter((connector) => connector.type === "csv")
|
||||
.map((connector) => ({ id: connector.id, name: connector.name }));
|
||||
|
||||
return (
|
||||
<FeedbackRecordsPageClient
|
||||
workspaceId={params.workspaceId}
|
||||
directories={frds}
|
||||
initialFrdId={initialFrdId ?? null}
|
||||
initialRecords={initialRecords?.data ?? []}
|
||||
initialNextCursor={initialRecords?.next_cursor}
|
||||
initialRecords={merged}
|
||||
frdMap={frdMap}
|
||||
csvSources={csvSources}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+35
-1
@@ -304,6 +304,7 @@ checksums:
|
||||
common/not_authenticated: fed6c62208524ea6782b5f9c07a95a4f
|
||||
common/not_authorized: 4be80383fe1a6f52c61138f1aa8d01d4
|
||||
common/not_connected: 91ebf07fff6b2ead94d85bd17212e0ba
|
||||
common/not_set: 380482630d60ee2d1531b31246caa467
|
||||
common/note: e0337f202c911423275f834edeffc54b
|
||||
common/notifications: c52df856139b50dbb1cae7bfb1cf73bb
|
||||
common/number: 2789f8391f63e7200a5521078aab017d
|
||||
@@ -404,6 +405,7 @@ checksums:
|
||||
common/some_files_failed_to_upload: a0e26efeb29ae905257ecf93b112dff0
|
||||
common/something_went_wrong: a3cd2f01c073f1f5ff436d4b132d39cf
|
||||
common/something_went_wrong_please_try_again: c62a7718d9a1e9c4ffb707807550f836
|
||||
common/soon: b12e79beb0aef9414a445a1b95dd4322
|
||||
common/sort_by: 8adf3dbc5668379558957662f0c43563
|
||||
common/start_free_trial: e346e4ed7d138dcc873db187922369da
|
||||
common/status: 4e1fcce15854d824919b4a582c697c90
|
||||
@@ -2441,10 +2443,14 @@ checksums:
|
||||
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/pause_connectors_confirmation_description: a3c2c56daed9f2a9e6a853cb8b924bad
|
||||
workspace/settings/feedback_record_directories/pause_connectors_confirmation_title: 09041363c55fb2686f8115df6fa2afc1
|
||||
workspace/settings/feedback_record_directories/select_workspaces_placeholder: 7d8c8f5910b264525f73bd32107765db
|
||||
workspace/settings/feedback_record_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de
|
||||
workspace/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f
|
||||
workspace/settings/feedback_record_directories/unarchive: 671fc7e9d7c8cb4d182a25a46551c168
|
||||
workspace/settings/feedback_record_directories/unarchive_workspace_conflict: 82f4b8ebaf41589cfb96e6398dafcc76
|
||||
workspace/settings/feedback_record_directories/workspace_access: 32407b39cf878fb579559c1ed3660892
|
||||
workspace/settings/general/ai_data_analysis_enabled: 45fabb594da6851f73fef50ca40fe525
|
||||
workspace/settings/general/ai_data_analysis_enabled_description: 46d4f0bdf4ebf89e78f79cc961a2de83
|
||||
workspace/settings/general/ai_enabled: 3cb1fce89c525e754448d5bd143eb6b5
|
||||
@@ -3453,12 +3459,15 @@ checksums:
|
||||
workspace/teams/permission: cc2ed7274bd8267f9e0a10b079584d8b
|
||||
workspace/teams/team_name: d1a5f99dbf503ca53f06b3a98b511d02
|
||||
workspace/teams/team_settings_description: 52f91883b9ceb6de83efbf8efd4f11c0
|
||||
workspace/unify/add_feedback_record: 19cf2b1fef0ca1400f2400e7ee681ea0
|
||||
workspace/unify/add_feedback_record_description: 94bca46246ba7353049b33742554b4c0
|
||||
workspace/unify/add_feedback_source: d046fb437ac478ca30b7b59d6afa8e45
|
||||
workspace/unify/add_source: 4cc055cbd6312cf0a5db1edf537ce65e
|
||||
workspace/unify/allowed_values: 430e0721aa2c52745ef8f8b6918bb7d2
|
||||
workspace/unify/api_ingestion: a14642d27bbb6843f9f4903b6555dfbb
|
||||
workspace/unify/api_ingestion_manage_api_keys: 116786a004fb7b16ead8a5b7a6a2debe
|
||||
workspace/unify/api_ingestion_settings_description: a2597917ca1c724607d1d32178d670b3
|
||||
workspace/unify/auto_generated: 6e83e8febd63275692c444cb8074531d
|
||||
workspace/unify/change_file: c5163ac18bf443370228a8ecbb0b07da
|
||||
workspace/unify/click_load_sample_csv: 0ee0bf93f10f02863fc658b359706316
|
||||
workspace/unify/click_to_upload: 74a7e7d79a88b6bbfd9f22084bffdb9b
|
||||
@@ -3483,8 +3492,12 @@ checksums:
|
||||
workspace/unify/csv_import_duplicate_warning: 56625e4613b93690e95661e5faaa4b27
|
||||
workspace/unify/csv_inconsistent_columns: b308be183a41a581707eb5c4c0797ad6
|
||||
workspace/unify/csv_max_records: 21ce7adae30821d40a553bcf37f39bbf
|
||||
workspace/unify/custom_source_type: d931a8a74d3a5becd568e398107979da
|
||||
workspace/unify/custom_source_type_placeholder: f139e3e5d70dbf426d7c6b5ab2b198cc
|
||||
workspace/unify/default_connector_name_csv: ef4060fef24c4fec064987b9d2a9fa4b
|
||||
workspace/unify/default_connector_name_formbricks: e7afdf7cc1cd7bcf75e7b5d64903a110
|
||||
workspace/unify/discard_feedback_record_changes_description: 48ccde99858dcbeb4d679749d0f51941
|
||||
workspace/unify/discard_feedback_record_changes_title: 52df2800f7b0e8a1d04c47113e019a3e
|
||||
workspace/unify/drop_a_field_here: 884f3025e618e0a5dcbcb5567335d1bb
|
||||
workspace/unify/drop_field_or: 5287a8af30f2961ce5a8f14f73ddc353
|
||||
workspace/unify/edit_csv_mapping: 4f3bad444664d58ffe8ace3dc9e200f9
|
||||
@@ -3494,15 +3507,23 @@ checksums:
|
||||
workspace/unify/enum: 96fc644f35edd6b1c09d1d503f078acc
|
||||
workspace/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8
|
||||
workspace/unify/feedback_date: ddba5d3270d4a6394d29721025a04400
|
||||
workspace/unify/feedback_record_created_successfully: 0ff30472085f1313a5ad53837c83e7c1
|
||||
workspace/unify/feedback_record_details: 823f3353db049a9d263ef31405054cda
|
||||
workspace/unify/feedback_record_details_description: 0b6f908154161241ce6bdeb4a2acaecd
|
||||
workspace/unify/feedback_record_directory: 89a08a540d1c6eb9f0b1a4b8f56e8aca
|
||||
workspace/unify/feedback_record_fields: 88c0f13afeb88fe751f85e79b0f73064
|
||||
workspace/unify/feedback_record_mcp: cdddbef2944489820fd7f376a49c2803
|
||||
workspace/unify/feedback_record_updated_successfully: cb40ef4b924e21fa627ebe6809d1d826
|
||||
workspace/unify/feedback_record_value_required: b54d4d86f82071a93dc979e8eb359cf0
|
||||
workspace/unify/feedback_records: e24cf48bb6985910f4ffe5e00512d388
|
||||
workspace/unify/feedback_records_refreshed: 4b27a8e2a8dbe8afa945d9f874aa7ef1
|
||||
workspace/unify/feedback_sources: e58ec9be19db8789e7096a756d24f2b2
|
||||
workspace/unify/feedback_sources_directory_access_multiple: 11d613bc1e9825aa6faa3db17ae678eb
|
||||
workspace/unify/feedback_sources_directory_access_single: c9da6b30d410a0ca6302a00a5747dc19
|
||||
workspace/unify/feedback_sources_settings_description: 45f162f2f81cd195c23cb3ec490bb3df
|
||||
workspace/unify/field_group_id: 17024bb46ff1e088afb6a279dc85aad4
|
||||
workspace/unify/field_group_label: 3df09c3b6fd22310359cf955ecff5c8e
|
||||
workspace/unify/field_id: 7791b5d581b7a525dcadf11ec73c6ab7
|
||||
workspace/unify/field_label: 6384505ca0e40010c666b712511132a6
|
||||
workspace/unify/field_type: 2581066dc304c853a4a817c20996fa08
|
||||
workspace/unify/formbricks_surveys: eba2fce04ee68f02626e5509adf7d66a
|
||||
@@ -3513,12 +3534,18 @@ checksums:
|
||||
workspace/unify/import_historical_responses: d7941f65344b6bfba56a40cc53a063b4
|
||||
workspace/unify/import_historical_responses_description: c860f7c6dbe8b74383ecf9cae9c219a0
|
||||
workspace/unify/import_rows: d2963498a7d2766264c4d67db677e8ff
|
||||
workspace/unify/import_via_source_name: eae32ae2fc87f925ca016fe8283bcbfd
|
||||
workspace/unify/importing_data: a6d4478379a0faee05cd2c10ffe74984
|
||||
workspace/unify/importing_historical_data: f5be578704ec26dc4ec573309e9fff20
|
||||
workspace/unify/invalid_enum_values: e6ca8740dab72f64e8dc5780b5cffcc6
|
||||
workspace/unify/invalid_values_found: 5011dc9c0294a222033f9910ea919b8a
|
||||
workspace/unify/load_sample_csv: ad21fa63f4a3df96a5939c753be21f4e
|
||||
workspace/unify/manage_directories: 460e00e1cbf1f51de57a2548546e33d7
|
||||
workspace/unify/manage_feedback_sources: 6aa6a82334ab680b5aa187b7245e8ec8
|
||||
workspace/unify/metadata: 695d4f7da261ba76e3be4de495491028
|
||||
workspace/unify/metadata_key: c478d228673f59fa556208ece60452f6
|
||||
workspace/unify/metadata_read_only_entries: 1934fee46c0a117f4926b61cc3d2d602
|
||||
workspace/unify/metadata_value: 8d69be1f5a20d9473a33c35670dff216
|
||||
workspace/unify/missing_feedback_source_title: 9ab1b8d54b4da72dd00ce03fe3b698b5
|
||||
workspace/unify/no_feedback_record_directory_available: b8126ef5d6276d9655a9b27ffcaca824
|
||||
workspace/unify/no_feedback_records: 16a905c40f6d47a5e8f93b3d8c6f6693
|
||||
@@ -3535,6 +3562,7 @@ checksums:
|
||||
workspace/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20
|
||||
workspace/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862
|
||||
workspace/unify/select_feedback_record_directory: 88afbf2c2a322249908ee5d00ec5f65d
|
||||
workspace/unify/select_feedback_record_source_type: 10997fcbea2f93e756888cf7a7476fdf
|
||||
workspace/unify/select_questions: 13c79b8c284423eb6140534bf2137e56
|
||||
workspace/unify/select_source_type_description: fd7e3c49b81f8e89f294c8fd94efcdfc
|
||||
workspace/unify/select_survey: bac52e59c7847417bef6fe7b7096b475
|
||||
@@ -3549,15 +3577,17 @@ checksums:
|
||||
workspace/unify/source_connect_feedback_record_mcp_description: a3f56e2a6e403f4021e83f1b1a466d95
|
||||
workspace/unify/source_connect_formbricks_description: 77bda4e1d485d76770ba2221f1faf9ff
|
||||
workspace/unify/source_fields: 1bae074990e64cbfd820a0b6462397be
|
||||
workspace/unify/source_id: 134a9a7d473508c5623ac724a5ba4be9
|
||||
workspace/unify/source_name: 157675beca12efcd8ec512c5256b1a61
|
||||
workspace/unify/source_type: d1ff69af76c687eb189db72030717570
|
||||
workspace/unify/source_type_cannot_be_changed: bb5232c6e92df7f88731310fabbb1eb1
|
||||
workspace/unify/sources: ecbbe6e49baa335c5afd7b04b609d006
|
||||
workspace/unify/status_error: 3c95bcb32c2104b99a46f5b3dd015248
|
||||
workspace/unify/status_live_sync: 7e794257419414f57d34845ef38d0939
|
||||
workspace/unify/status_paused: edb1f7b7219e1c9b7aa67159090d6991
|
||||
workspace/unify/status_ready: 437c0eea608e15ad5cdab94bde2f4b48
|
||||
workspace/unify/submission_id: 02edf76883b47079dbe20f3f36b7c1a7
|
||||
workspace/unify/survey_has_no_questions: c08514b6bce5eb464a4492239be5934d
|
||||
workspace/unify/topics_and_subtopics: 1148eca01a1993fadca932efcdea7641
|
||||
workspace/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5
|
||||
workspace/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396
|
||||
workspace/unify/updated_at: 8fdb85248e591254973403755dcc3724
|
||||
@@ -3565,6 +3595,10 @@ checksums:
|
||||
workspace/unify/upload_csv_file: b77797b68cb46a614b3adaa4db24d4c2
|
||||
workspace/unify/user_identifier: 61073457a5c3901084b557d065f876be
|
||||
workspace/unify/value: 34b0eaa85808b15cbc4be94c64d0146b
|
||||
workspace/unify/value_boolean: bbdcd3f46954b6304b9069e94e1371ab
|
||||
workspace/unify/value_date: c8d705d1975affc01c002324725fec3f
|
||||
workspace/unify/value_number: 1f14da79d14bd7b1c2324141f4470675
|
||||
workspace/unify/value_text: e097a597cc507c716401ad18255de578
|
||||
workspace/xm-templates/ces: e2ea309b2f7f13257967b966c2fda1e9
|
||||
workspace/xm-templates/ces_description: c8d9794dd17d5ab85a979f1b3e1bc935
|
||||
workspace/xm-templates/csat: fdfc1dc6214cce661dcdc32a71d80337
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Du bist nicht authentifiziert, um diese Aktion durchzuführen.",
|
||||
"not_authorized": "Nicht autorisiert",
|
||||
"not_connected": "Nicht verbunden",
|
||||
"not_set": "Nicht festgelegt",
|
||||
"note": "Hinweis",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"number": "Nummer",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Sieh nach, welche Teams auf diesen Workspace zugreifen können."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Feedback-Datensatz hinzufügen",
|
||||
"add_feedback_record_description": "Erstellen Sie manuell einen Feedback-Datensatz.",
|
||||
"add_feedback_source": "Feedback-Quelle hinzufügen",
|
||||
"add_source": "Quelle hinzufügen",
|
||||
"allowed_values": "Zulässige Werte: {values}",
|
||||
"api_ingestion": "API-Erfassung",
|
||||
"api_ingestion_manage_api_keys": "API-Schlüssel verwalten",
|
||||
"api_ingestion_settings_description": "Sende Feedback-Datensätze über die Management-API.",
|
||||
"auto_generated": "Automatisch generiert",
|
||||
"change_file": "Datei ändern",
|
||||
"click_load_sample_csv": "Klick auf 'Beispiel-CSV laden', um Spalten zu sehen",
|
||||
"click_to_upload": "Zum Hochladen klicken",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Wenn Du die Daten zweimal importierst, entstehen doppelte Einträge.",
|
||||
"csv_inconsistent_columns": "Zeile {row} hat inkonsistente Spalten. Alle Zeilen müssen die gleichen Überschriften haben.",
|
||||
"csv_max_records": "Maximal {max} Einträge erlaubt.",
|
||||
"custom_source_type": "Benutzerdefinierter Quelltyp",
|
||||
"custom_source_type_placeholder": "Geben Sie den benutzerdefinierten Quelltyp ein",
|
||||
"default_connector_name_csv": "CSV-Import",
|
||||
"default_connector_name_formbricks": "Formbricks-Umfrage-Verbindung",
|
||||
"discard_feedback_record_changes_description": "Ihre Änderungen gehen verloren, wenn Sie diese Schublade schließen.",
|
||||
"discard_feedback_record_changes_title": "Nicht gespeicherte Änderungen verwerfen?",
|
||||
"drop_a_field_here": "Ziehe ein Feld hierher",
|
||||
"drop_field_or": "Feld ablegen oder",
|
||||
"edit_csv_mapping": "CSV-Zuordnung bearbeiten",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "Aufzählung",
|
||||
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
|
||||
"feedback_date": "Aktuelles Datum",
|
||||
"feedback_record_created_successfully": "Feedback-Datensatz erfolgreich erstellt",
|
||||
"feedback_record_details": "Details zum Feedback-Datensatz",
|
||||
"feedback_record_details_description": "Überprüfen und aktualisieren Sie die Felder des Feedback-Datensatzes.",
|
||||
"feedback_record_directory": "Feedback-Datensatz-Verzeichnis",
|
||||
"feedback_record_fields": "Feedback-Eintragsfelder",
|
||||
"feedback_record_mcp": "Feedback-Datensatz MCP",
|
||||
"feedback_record_updated_successfully": "Feedback-Datensatz erfolgreich aktualisiert",
|
||||
"feedback_record_value_required": "Für den ausgewählten Feldtyp ist ein Wert erforderlich",
|
||||
"feedback_records": "Feedback-Einträge",
|
||||
"feedback_records_refreshed": "Feedback-Einträge aktualisiert",
|
||||
"feedback_sources": "Feedback-Quellen",
|
||||
"feedback_sources_directory_access_multiple": "Neue Datensätze aus diesen Quellen werden gespeichert in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Neue Datensätze aus dieser Quelle werden gespeichert in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Verbinde und verwalte alle Feedback-Quellen für diesen Workspace.",
|
||||
"field_group_id": "Feldgruppen-ID",
|
||||
"field_group_label": "Feldgruppenbezeichnung",
|
||||
"field_id": "Feld-ID",
|
||||
"field_label": "Feldbezeichnung",
|
||||
"field_type": "Feldtyp",
|
||||
"formbricks_surveys": "Formbricks-Umfragen",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Bisherige Antworten importieren",
|
||||
"import_historical_responses_description": "Importiere jetzt vorhandene Antworten aus dieser Umfrage.",
|
||||
"import_rows": "{count} Zeilen importieren",
|
||||
"import_via_source_name": "Import über „{sourceName}“",
|
||||
"importing_data": "Daten werden importiert...",
|
||||
"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_sample_csv": "Beispiel-CSV laden",
|
||||
"manage_directories": "Verzeichnisse verwalten",
|
||||
"manage_feedback_sources": "Feedbackquellen verwalten",
|
||||
"metadata": "Metadaten",
|
||||
"metadata_key": "Metadatenschlüssel",
|
||||
"metadata_read_only_entries": "Schreibgeschützte Metadatenwerte (keine Zeichenfolge)",
|
||||
"metadata_value": "Metadatenwert",
|
||||
"missing_feedback_source_title": "Feedback-Quelle fehlt?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,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_feedback_record_directory": "Verzeichnis auswählen",
|
||||
"select_feedback_record_source_type": "Wählen Sie den Quelltyp aus",
|
||||
"select_questions": "Fragen auswählen",
|
||||
"select_source_type_description": "Wähle die Art der Feedback-Quelle aus, die Du verbinden möchtest.",
|
||||
"select_survey": "Umfrage auswählen",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Sende Feedback-Datensätze über die MCP-Integration.",
|
||||
"source_connect_formbricks_description": "Feedback aus Deinen Formbricks-Umfragen verbinden",
|
||||
"source_fields": "Quellfelder",
|
||||
"source_id": "Quell-ID",
|
||||
"source_name": "Quellenname",
|
||||
"source_type": "Quellentyp",
|
||||
"source_type_cannot_be_changed": "Quellentyp kann nicht geändert werden",
|
||||
"sources": "Quellen",
|
||||
"status_error": "Fehler",
|
||||
"status_live_sync": "Live-Synchronisierung",
|
||||
"status_paused": "Pausiert",
|
||||
"status_ready": "Bereit",
|
||||
"submission_id": "Einreichungs-ID",
|
||||
"survey_has_no_questions": "Diese Umfrage hat keine Fragen",
|
||||
"topics_and_subtopics": "Themen & Unterthemen",
|
||||
"unify_feedback": "Feedback vereinheitlichen",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Lade eine CSV-Datei hoch, um Feedback-Daten zu importieren.",
|
||||
"upload_csv_file": "CSV-Datei hochladen",
|
||||
"user_identifier": "Benutzer",
|
||||
"value": "Wert"
|
||||
"value": "Wert",
|
||||
"value_boolean": "Wert (Boolescher Wert)",
|
||||
"value_date": "Wert (Datum)",
|
||||
"value_number": "Wert (Anzahl)",
|
||||
"value_text": "Wert (Text)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "You are not authenticated to perform this action.",
|
||||
"not_authorized": "Not authorized",
|
||||
"not_connected": "Not Connected",
|
||||
"not_set": "Not set",
|
||||
"note": "Note",
|
||||
"notifications": "Notifications",
|
||||
"number": "Number",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "See which teams can access this workspace."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Add feedback record",
|
||||
"add_feedback_record_description": "Create a feedback record manually.",
|
||||
"add_feedback_source": "Add Feedback Source",
|
||||
"add_source": "Add source",
|
||||
"allowed_values": "Allowed values: {values}",
|
||||
"api_ingestion": "API ingestion",
|
||||
"api_ingestion_manage_api_keys": "Manage API keys",
|
||||
"api_ingestion_settings_description": "Send feedback records using the Management API.",
|
||||
"auto_generated": "Auto-generated",
|
||||
"change_file": "Change file",
|
||||
"click_load_sample_csv": "Click 'Load sample CSV' to see columns",
|
||||
"click_to_upload": "Click to upload",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importing data twice will create duplicate records.",
|
||||
"csv_inconsistent_columns": "Row {row} has inconsistent columns. All rows must have the same headers.",
|
||||
"csv_max_records": "Maximum {max} records allowed.",
|
||||
"custom_source_type": "Custom source type",
|
||||
"custom_source_type_placeholder": "Enter custom source type",
|
||||
"default_connector_name_csv": "CSV Import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey Connection",
|
||||
"discard_feedback_record_changes_description": "Your changes will be lost if you close this drawer.",
|
||||
"discard_feedback_record_changes_title": "Discard unsaved changes?",
|
||||
"drop_a_field_here": "Drop a field here",
|
||||
"drop_field_or": "Drop field or",
|
||||
"edit_csv_mapping": "Edit CSV mapping",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Failed to load feedback records",
|
||||
"feedback_date": "Current date",
|
||||
"feedback_record_created_successfully": "Feedback record created successfully",
|
||||
"feedback_record_details": "Feedback record details",
|
||||
"feedback_record_details_description": "Review and update feedback record fields.",
|
||||
"feedback_record_directory": "Feedback Record Directory",
|
||||
"feedback_record_fields": "Feedback Record Fields",
|
||||
"feedback_record_mcp": "Feedback Record MCP",
|
||||
"feedback_record_updated_successfully": "Feedback record updated successfully",
|
||||
"feedback_record_value_required": "A value is required for the selected field type",
|
||||
"feedback_records": "Feedback Records",
|
||||
"feedback_records_refreshed": "Feedback records refreshed",
|
||||
"feedback_sources": "Feedback Sources",
|
||||
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
|
||||
"field_group_id": "Field Group ID",
|
||||
"field_group_label": "Field Group Label",
|
||||
"field_id": "Field ID",
|
||||
"field_label": "Field Label",
|
||||
"field_type": "Field Type",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Import historical responses",
|
||||
"import_historical_responses_description": "Import existing responses from this survey now.",
|
||||
"import_rows": "Import {count} rows",
|
||||
"import_via_source_name": "Import via \"{sourceName}\"",
|
||||
"importing_data": "Importing data...",
|
||||
"importing_historical_data": "Importing historical data...",
|
||||
"invalid_enum_values": "Invalid values in column mapped to {field}",
|
||||
"invalid_values_found": "Found: {values} (rows: {rows}) {extra}",
|
||||
"load_sample_csv": "Load sample CSV",
|
||||
"manage_directories": "Manage directories",
|
||||
"manage_feedback_sources": "Manage feedback sources",
|
||||
"metadata": "Metadata",
|
||||
"metadata_key": "Metadata key",
|
||||
"metadata_read_only_entries": "Read-only metadata values (non-string)",
|
||||
"metadata_value": "Metadata value",
|
||||
"missing_feedback_source_title": "Missing feedback source?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Select a survey to see its questions",
|
||||
"select_a_value": "Select a value...",
|
||||
"select_feedback_record_directory": "Select a directory",
|
||||
"select_feedback_record_source_type": "Select source type",
|
||||
"select_questions": "Select questions",
|
||||
"select_source_type_description": "Select the type of feedback source you want to connect.",
|
||||
"select_survey": "Select Survey",
|
||||
@@ -3714,6 +3737,7 @@
|
||||
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
|
||||
"source_connect_formbricks_description": "Connect feedback from your Formbricks surveys",
|
||||
"source_fields": "Source Fields",
|
||||
"source_id": "Source ID",
|
||||
"source_name": "Source Name",
|
||||
"source_type": "Source Type",
|
||||
"source_type_cannot_be_changed": "Source type cannot be changed",
|
||||
@@ -3721,6 +3745,7 @@
|
||||
"status_live_sync": "Live sync",
|
||||
"status_paused": "Paused",
|
||||
"status_ready": "Ready",
|
||||
"submission_id": "Submission ID",
|
||||
"survey_has_no_questions": "This survey has no questions",
|
||||
"topics_and_subtopics": "Topics & Subtopics",
|
||||
"unify_feedback": "Unify Feedback",
|
||||
@@ -3729,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Upload a CSV file to import feedback data.",
|
||||
"upload_csv_file": "Upload CSV File",
|
||||
"user_identifier": "User",
|
||||
"value": "Value"
|
||||
"value": "Value",
|
||||
"value_boolean": "Value (Boolean)",
|
||||
"value_date": "Value (Date)",
|
||||
"value_number": "Value (Number)",
|
||||
"value_text": "Value (Text)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "No estás autenticado para realizar esta acción.",
|
||||
"not_authorized": "No autorizado",
|
||||
"not_connected": "No conectado",
|
||||
"not_set": "No establecido",
|
||||
"note": "Nota",
|
||||
"notifications": "Notificaciones",
|
||||
"number": "Número",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Consulta qué equipos pueden acceder a este espacio de trabajo."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Agregar registro de comentarios",
|
||||
"add_feedback_record_description": "Cree un registro de comentarios manualmente.",
|
||||
"add_feedback_source": "Añadir fuente de feedback",
|
||||
"add_source": "Añadir fuente",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"api_ingestion": "Ingesta de API",
|
||||
"api_ingestion_manage_api_keys": "Gestionar claves de API",
|
||||
"api_ingestion_settings_description": "Envía registros de feedback mediante la API de gestión.",
|
||||
"auto_generated": "Generado automáticamente",
|
||||
"change_file": "Cambiar archivo",
|
||||
"click_load_sample_csv": "Haz clic en 'Cargar CSV de muestra' para ver las columnas",
|
||||
"click_to_upload": "Haz clic para subir",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importar datos dos veces creará registros duplicados.",
|
||||
"csv_inconsistent_columns": "La fila {row} tiene columnas inconsistentes. Todas las filas deben tener los mismos encabezados.",
|
||||
"csv_max_records": "Máximo de {max} registros permitidos.",
|
||||
"custom_source_type": "Tipo de fuente personalizado",
|
||||
"custom_source_type_placeholder": "Ingrese el tipo de fuente personalizado",
|
||||
"default_connector_name_csv": "Importación CSV",
|
||||
"default_connector_name_formbricks": "Conexión de encuesta de Formbricks",
|
||||
"discard_feedback_record_changes_description": "Sus cambios se perderán si cierra este cajón.",
|
||||
"discard_feedback_record_changes_title": "¿Descartar los cambios no guardados?",
|
||||
"drop_a_field_here": "Suelta un campo aquí",
|
||||
"drop_field_or": "Suelta el campo o",
|
||||
"edit_csv_mapping": "Editar mapeo de CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Error al cargar los registros de comentarios",
|
||||
"feedback_date": "Fecha actual",
|
||||
"feedback_record_created_successfully": "Registro de comentarios creado correctamente",
|
||||
"feedback_record_details": "Detalles del registro de comentarios",
|
||||
"feedback_record_details_description": "Revise y actualice los campos del registro de comentarios.",
|
||||
"feedback_record_directory": "Directorio de Registros de Comentarios",
|
||||
"feedback_record_fields": "Campos de registro de comentarios",
|
||||
"feedback_record_mcp": "MCP de registros de feedback",
|
||||
"feedback_record_updated_successfully": "Registro de comentarios actualizado correctamente",
|
||||
"feedback_record_value_required": "Se requiere un valor para el tipo de campo seleccionado",
|
||||
"feedback_records": "Registros de comentarios",
|
||||
"feedback_records_refreshed": "Registros de comentarios actualizados",
|
||||
"feedback_sources": "Fuentes de feedback",
|
||||
"feedback_sources_directory_access_multiple": "Los nuevos registros de estas fuentes se almacenarán en: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Los nuevos registros de esta fuente se almacenarán en: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Conecta y gestiona todas las fuentes de feedback para este espacio de trabajo.",
|
||||
"field_group_id": "ID de grupo de campos",
|
||||
"field_group_label": "Etiqueta de grupo de campos",
|
||||
"field_id": "ID de campo",
|
||||
"field_label": "Etiqueta de campo",
|
||||
"field_type": "Tipo de campo",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Importar respuestas históricas",
|
||||
"import_historical_responses_description": "Importa las respuestas existentes de esta encuesta ahora.",
|
||||
"import_rows": "Importar {count} filas",
|
||||
"import_via_source_name": "Importar mediante \"{sourceName}\"",
|
||||
"importing_data": "Importando datos...",
|
||||
"importing_historical_data": "Importando datos históricos...",
|
||||
"invalid_enum_values": "Valores no válidos en la columna asignada a {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (filas: {rows}) {extra}",
|
||||
"load_sample_csv": "Cargar CSV de muestra",
|
||||
"manage_directories": "Gestionar directorios",
|
||||
"manage_feedback_sources": "Administrar fuentes de comentarios",
|
||||
"metadata": "Metadatos",
|
||||
"metadata_key": "Clave de metadatos",
|
||||
"metadata_read_only_entries": "Valores de metadatos de solo lectura (no cadenas)",
|
||||
"metadata_value": "Valor de metadatos",
|
||||
"missing_feedback_source_title": "¿Falta alguna fuente de feedback?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas",
|
||||
"select_a_value": "Selecciona un valor...",
|
||||
"select_feedback_record_directory": "Selecciona un directorio",
|
||||
"select_feedback_record_source_type": "Seleccionar tipo de fuente",
|
||||
"select_questions": "Seleccionar preguntas",
|
||||
"select_source_type_description": "Selecciona el tipo de fuente de feedback que quieres conectar.",
|
||||
"select_survey": "Seleccionar encuesta",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Envía registros de feedback a través de la integración MCP.",
|
||||
"source_connect_formbricks_description": "Conectar feedback de tus encuestas de Formbricks",
|
||||
"source_fields": "Campos de origen",
|
||||
"source_id": "ID de fuente",
|
||||
"source_name": "Nombre de origen",
|
||||
"source_type": "Tipo de fuente",
|
||||
"source_type_cannot_be_changed": "El tipo de origen no se puede cambiar",
|
||||
"sources": "Orígenes",
|
||||
"status_error": "Error",
|
||||
"status_live_sync": "Sincronización en vivo",
|
||||
"status_paused": "Pausado",
|
||||
"status_ready": "Listo",
|
||||
"submission_id": "ID de envío",
|
||||
"survey_has_no_questions": "Esta encuesta no tiene preguntas",
|
||||
"topics_and_subtopics": "Temas y subtemas",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Sube un archivo CSV para importar datos de comentarios.",
|
||||
"upload_csv_file": "Subir archivo CSV",
|
||||
"user_identifier": "Usuario",
|
||||
"value": "Valor"
|
||||
"value": "Valor",
|
||||
"value_boolean": "Valor (booleano)",
|
||||
"value_date": "Valor (Fecha)",
|
||||
"value_number": "Valor (Número)",
|
||||
"value_text": "Valor (Texto)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Vous n'êtes pas authentifié pour effectuer cette action.",
|
||||
"not_authorized": "Non autorisé",
|
||||
"not_connected": "Non connecté",
|
||||
"not_set": "Non défini",
|
||||
"note": "Remarque",
|
||||
"notifications": "Notifications",
|
||||
"number": "Numéro",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Voir quelles équipes peuvent accéder à cet espace de travail."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Ajouter un enregistrement de commentaires",
|
||||
"add_feedback_record_description": "Créez manuellement un enregistrement de commentaires.",
|
||||
"add_feedback_source": "Ajouter une source de feedback",
|
||||
"add_source": "Ajouter une source",
|
||||
"allowed_values": "Valeurs autorisées : {values}",
|
||||
"api_ingestion": "Ingestion par API",
|
||||
"api_ingestion_manage_api_keys": "Gérer les clés API",
|
||||
"api_ingestion_settings_description": "Envoyer des enregistrements de feedback via l'API de gestion.",
|
||||
"auto_generated": "Généré automatiquement",
|
||||
"change_file": "Changer de fichier",
|
||||
"click_load_sample_csv": "Clique sur « Charger un exemple CSV » pour voir les colonnes",
|
||||
"click_to_upload": "Clique pour charger",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importer les données deux fois créera des enregistrements en double.",
|
||||
"csv_inconsistent_columns": "La ligne {row} a des colonnes incohérentes. Toutes les lignes doivent avoir les mêmes en-têtes.",
|
||||
"csv_max_records": "Maximum {max} enregistrements autorisés.",
|
||||
"custom_source_type": "Type de source personnalisé",
|
||||
"custom_source_type_placeholder": "Entrez le type de source personnalisé",
|
||||
"default_connector_name_csv": "Importation CSV",
|
||||
"default_connector_name_formbricks": "Connexion de sondage Formbricks",
|
||||
"discard_feedback_record_changes_description": "Vos modifications seront perdues si vous fermez ce tiroir.",
|
||||
"discard_feedback_record_changes_title": "Supprimer les modifications non enregistrées ?",
|
||||
"drop_a_field_here": "Déposez un champ ici",
|
||||
"drop_field_or": "Déposez un champ ou",
|
||||
"edit_csv_mapping": "Modifier le mappage CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback",
|
||||
"feedback_date": "Date actuelle",
|
||||
"feedback_record_created_successfully": "Enregistrement de commentaires créé avec succès",
|
||||
"feedback_record_details": "Détails de l'enregistrement des commentaires",
|
||||
"feedback_record_details_description": "Examiner et mettre à jour les champs d’enregistrement des commentaires.",
|
||||
"feedback_record_directory": "Répertoire d'enregistrements de retour d'expérience",
|
||||
"feedback_record_fields": "Champs d'enregistrement de feedback",
|
||||
"feedback_record_mcp": "MCP d'enregistrement de feedback",
|
||||
"feedback_record_updated_successfully": "L'enregistrement des commentaires a été mis à jour avec succès",
|
||||
"feedback_record_value_required": "Une valeur est requise pour le type de champ sélectionné",
|
||||
"feedback_records": "Enregistrements de feedback",
|
||||
"feedback_records_refreshed": "Enregistrements de feedback actualisés",
|
||||
"feedback_sources": "Sources de feedback",
|
||||
"feedback_sources_directory_access_multiple": "Les nouveaux enregistrements de ces sources seront stockés dans : {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Les nouveaux enregistrements de cette source seront stockés dans : {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connecte et gère toutes les sources de feedback pour cet espace de travail.",
|
||||
"field_group_id": "ID de groupe de champs",
|
||||
"field_group_label": "Libellé du groupe de champs",
|
||||
"field_id": "Identifiant du champ",
|
||||
"field_label": "Libellé du champ",
|
||||
"field_type": "Type de champ",
|
||||
"formbricks_surveys": "Sondages Formbricks",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Importer les réponses historiques",
|
||||
"import_historical_responses_description": "Importe les réponses existantes de cette enquête maintenant.",
|
||||
"import_rows": "Importer {count} lignes",
|
||||
"import_via_source_name": "Importer via \"{sourceName}\"",
|
||||
"importing_data": "Importation des données...",
|
||||
"importing_historical_data": "Importation des données historiques...",
|
||||
"invalid_enum_values": "Valeurs non valides dans la colonne mappée à {field}",
|
||||
"invalid_values_found": "Trouvées : {values} (lignes : {rows}) {extra}",
|
||||
"load_sample_csv": "Charger un exemple de CSV",
|
||||
"manage_directories": "Gérer les répertoires",
|
||||
"manage_feedback_sources": "Gérer les sources de commentaires",
|
||||
"metadata": "Métadonnées",
|
||||
"metadata_key": "Clé de métadonnées",
|
||||
"metadata_read_only_entries": "Valeurs de métadonnées en lecture seule (non-chaîne)",
|
||||
"metadata_value": "Valeur des métadonnées",
|
||||
"missing_feedback_source_title": "Il manque une source de feedback ?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions",
|
||||
"select_a_value": "Sélectionnez une valeur...",
|
||||
"select_feedback_record_directory": "Sélectionner un répertoire",
|
||||
"select_feedback_record_source_type": "Sélectionnez le type de source",
|
||||
"select_questions": "Sélectionner les questions",
|
||||
"select_source_type_description": "Sélectionnez le type de source de feedback que vous souhaitez connecter.",
|
||||
"select_survey": "Sélectionner l'enquête",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Envoyer des enregistrements de feedback via l'intégration MCP.",
|
||||
"source_connect_formbricks_description": "Connecter les feedbacks de vos enquêtes Formbricks",
|
||||
"source_fields": "Champs source",
|
||||
"source_id": "Identifiant de la source",
|
||||
"source_name": "Nom de la source",
|
||||
"source_type": "Type de source",
|
||||
"source_type_cannot_be_changed": "Le type de source ne peut pas être modifié",
|
||||
"sources": "Sources",
|
||||
"status_error": "Erreur",
|
||||
"status_live_sync": "Synchronisation en direct",
|
||||
"status_paused": "En pause",
|
||||
"status_ready": "Prêt",
|
||||
"submission_id": "ID de soumission",
|
||||
"survey_has_no_questions": "Ce sondage n'a pas de questions",
|
||||
"topics_and_subtopics": "Thèmes et sous-thèmes",
|
||||
"unify_feedback": "Unifier les retours",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Téléchargez un fichier CSV pour importer des données de feedback.",
|
||||
"upload_csv_file": "Télécharger un fichier CSV",
|
||||
"user_identifier": "Utilisateur",
|
||||
"value": "Valeur"
|
||||
"value": "Valeur",
|
||||
"value_boolean": "Valeur (booléenne)",
|
||||
"value_date": "Valeur (Date)",
|
||||
"value_number": "Valeur (Nombre)",
|
||||
"value_text": "Valeur (texte)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Nincs jogosultsága ennek a műveletnek a végrehajtásához.",
|
||||
"not_authorized": "Nincs felhatalmazva",
|
||||
"not_connected": "Nincs kapcsolódva",
|
||||
"not_set": "Nincs beállítva",
|
||||
"note": "Jegyzet",
|
||||
"notifications": "Értesítések",
|
||||
"number": "Szám",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Annak megtekintése, hogy mely csapatok férhetnek hozzá ehhez a munkaterülethez."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Visszajelzés hozzáadása",
|
||||
"add_feedback_record_description": "Készítsen visszajelzési rekordot manuálisan.",
|
||||
"add_feedback_source": "Visszajelzési forrás hozzáadása",
|
||||
"add_source": "Forrás hozzáadása",
|
||||
"allowed_values": "Engedélyezett értékek: {values}",
|
||||
"api_ingestion": "API betöltés",
|
||||
"api_ingestion_manage_api_keys": "API kulcsok kezelése",
|
||||
"api_ingestion_settings_description": "Visszajelzési rekordok küldése a Management API használatával.",
|
||||
"auto_generated": "Automatikusan generált",
|
||||
"change_file": "Fájl módosítása",
|
||||
"click_load_sample_csv": "Kattintson a 'Minta CSV betöltése' gombra az oszlopok megtekintéséhez",
|
||||
"click_to_upload": "Kattintson a feltöltéshez",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Az adatok kétszeri importálása duplikált rekordokat hoz létre.",
|
||||
"csv_inconsistent_columns": "A(z) {row}. sor inkonzisztens oszlopokat tartalmaz. Minden sornak ugyanazokkal a fejlécekkel kell rendelkeznie.",
|
||||
"csv_max_records": "Maximum {max} rekord engedélyezett.",
|
||||
"custom_source_type": "Egyéni forrástípus",
|
||||
"custom_source_type_placeholder": "Adja meg az egyéni forrástípust",
|
||||
"default_connector_name_csv": "CSV importálás",
|
||||
"default_connector_name_formbricks": "Formbricks kérdőív kapcsolat",
|
||||
"discard_feedback_record_changes_description": "A módosítások elvesznek, ha bezárja ezt a fiókot.",
|
||||
"discard_feedback_record_changes_title": "Elveti a nem mentett módosításokat?",
|
||||
"drop_a_field_here": "Húzz ide egy mezőt",
|
||||
"drop_field_or": "Húzz ide egy mezőt vagy",
|
||||
"edit_csv_mapping": "CSV leképezés szerkesztése",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"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_created_successfully": "A visszajelzési rekord sikeresen létrehozva",
|
||||
"feedback_record_details": "A visszajelzési rekord részletei",
|
||||
"feedback_record_details_description": "Tekintse át és frissítse a visszajelzési rekordmezőket.",
|
||||
"feedback_record_directory": "Visszajelzési Rekord Könyvtár",
|
||||
"feedback_record_fields": "Visszajelzési rekord mezők",
|
||||
"feedback_record_mcp": "Visszajelzési rekord MCP",
|
||||
"feedback_record_updated_successfully": "A visszajelzési rekord sikeresen frissítve",
|
||||
"feedback_record_value_required": "A kiválasztott mezőtípushoz értéket kell megadni",
|
||||
"feedback_records": "Visszajelzési rekordok",
|
||||
"feedback_records_refreshed": "Visszajelzési rekordok frissítve",
|
||||
"feedback_sources": "Visszajelzési források",
|
||||
"feedback_sources_directory_access_multiple": "Az ezekből a forrásokból származó új rekordok a következő helyen lesznek tárolva: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Az ebből a forrásból származó új rekordok a következő helyen lesznek tárolva: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Összes visszajelzési forrás csatlakoztatása és kezelése ezen munkaterület számára.",
|
||||
"field_group_id": "Mezőcsoport azonosítója",
|
||||
"field_group_label": "Mezőcsoport címke",
|
||||
"field_id": "Mezőazonosító",
|
||||
"field_label": "Mező címke",
|
||||
"field_type": "Mező típus",
|
||||
"formbricks_surveys": "Formbricks kérdőívek",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Korábbi válaszok importálása",
|
||||
"import_historical_responses_description": "Meglévő válaszok importálása ebből a felmérésből most.",
|
||||
"import_rows": "{count} sor importálása",
|
||||
"import_via_source_name": "Importálás a következőn keresztül: \"{sourceName}\"",
|
||||
"importing_data": "Adatok importálása...",
|
||||
"importing_historical_data": "Történeti adatok importálása...",
|
||||
"invalid_enum_values": "Érvénytelen értékek a(z) {field} mezőhöz rendelt oszlopban",
|
||||
"invalid_values_found": "Talált értékek: {values} (sorok: {rows}) {extra}",
|
||||
"load_sample_csv": "Minta CSV betöltése",
|
||||
"manage_directories": "Könyvtárak kezelése",
|
||||
"manage_feedback_sources": "Visszajelzési források kezelése",
|
||||
"metadata": "Metaadatok",
|
||||
"metadata_key": "Metaadatkulcs",
|
||||
"metadata_read_only_entries": "Csak olvasható metaadatértékek (nem karakterlánc)",
|
||||
"metadata_value": "A metaadat értéke",
|
||||
"missing_feedback_source_title": "Hiányzik egy visszajelzési forrá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.",
|
||||
@@ -3700,6 +3722,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_feedback_record_directory": "Válasszon egy könyvtárat",
|
||||
"select_feedback_record_source_type": "Válassza ki a forrás típusát",
|
||||
"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_survey": "Kérdőív kiválasztása",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Visszajelzési rekordok küldése az MCP integráción keresztül.",
|
||||
"source_connect_formbricks_description": "Visszajelzések csatlakoztatása a Formbricks kérdőívekből",
|
||||
"source_fields": "Forrásmezők",
|
||||
"source_id": "Forrásazonosító",
|
||||
"source_name": "Forrásnév",
|
||||
"source_type": "Forrás típus",
|
||||
"source_type_cannot_be_changed": "A forrástípus nem módosítható",
|
||||
"sources": "Források",
|
||||
"status_error": "Hiba",
|
||||
"status_live_sync": "Élő szinkronizálás",
|
||||
"status_paused": "Szüneteltetve",
|
||||
"status_ready": "Kész",
|
||||
"submission_id": "Beküldés azonosítója",
|
||||
"survey_has_no_questions": "Ez a felmérés nem tartalmaz kérdéseket",
|
||||
"topics_and_subtopics": "Témák és altémák",
|
||||
"unify_feedback": "Visszajelzések egyesítése",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Tölts fel egy CSV fájlt a visszajelzési adatok importálásához.",
|
||||
"upload_csv_file": "CSV fájl feltöltése",
|
||||
"user_identifier": "Felhasználó",
|
||||
"value": "Érték"
|
||||
"value": "Érték",
|
||||
"value_boolean": "Érték (logikai)",
|
||||
"value_date": "Érték (dátum)",
|
||||
"value_number": "Érték (szám)",
|
||||
"value_text": "Érték (szöveg)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "このアクションを実行するための認証がされていません。",
|
||||
"not_authorized": "権限がありません",
|
||||
"not_connected": "未接続",
|
||||
"not_set": "未設定",
|
||||
"note": "メモ",
|
||||
"notifications": "通知",
|
||||
"number": "数値",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "このワークスペースにアクセスできるチームを確認します。"
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "フィードバックレコードを追加する",
|
||||
"add_feedback_record_description": "フィードバック記録を手動で作成します。",
|
||||
"add_feedback_source": "フィードバックソースを追加",
|
||||
"add_source": "ソースを追加",
|
||||
"allowed_values": "許可される値: {values}",
|
||||
"api_ingestion": "API取り込み",
|
||||
"api_ingestion_manage_api_keys": "APIキーを管理",
|
||||
"api_ingestion_settings_description": "管理APIを使用してフィードバックレコードを送信します。",
|
||||
"auto_generated": "自動生成",
|
||||
"change_file": "ファイルを変更",
|
||||
"click_load_sample_csv": "「サンプルCSVを読み込む」をクリックして列を表示",
|
||||
"click_to_upload": "クリックしてアップロード",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "データを2回インポートすると、重複したレコードが作成されます。",
|
||||
"csv_inconsistent_columns": "行 {row} の列が一致しません。すべての行は同じヘッダーを持つ必要があります。",
|
||||
"csv_max_records": "最大 {max} 件のレコードまで許可されています。",
|
||||
"custom_source_type": "カスタムソースタイプ",
|
||||
"custom_source_type_placeholder": "カスタムソースタイプを入力してください",
|
||||
"default_connector_name_csv": "CSVインポート",
|
||||
"default_connector_name_formbricks": "Formbricks フォーム接続",
|
||||
"discard_feedback_record_changes_description": "このドロワーを閉じると、変更内容は失われます。",
|
||||
"discard_feedback_record_changes_title": "保存されていない変更を破棄しますか?",
|
||||
"drop_a_field_here": "ここにフィールドをドロップ",
|
||||
"drop_field_or": "フィールドをドロップまたは",
|
||||
"edit_csv_mapping": "CSVマッピングを編集",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "列挙型",
|
||||
"failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました",
|
||||
"feedback_date": "現在の日付",
|
||||
"feedback_record_created_successfully": "フィードバックレコードが正常に作成されました",
|
||||
"feedback_record_details": "フィードバック記録の詳細",
|
||||
"feedback_record_details_description": "フィードバック レコード フィールドを確認して更新します。",
|
||||
"feedback_record_directory": "フィードバックレコードディレクトリ",
|
||||
"feedback_record_fields": "フィードバックレコードフィールド",
|
||||
"feedback_record_mcp": "フィードバックレコードMCP",
|
||||
"feedback_record_updated_successfully": "フィードバックレコードが正常に更新されました",
|
||||
"feedback_record_value_required": "選択したフィールド タイプには値が必要です",
|
||||
"feedback_records": "フィードバックレコード",
|
||||
"feedback_records_refreshed": "フィードバックレコードを更新しました",
|
||||
"feedback_sources": "フィードバックソース",
|
||||
"feedback_sources_directory_access_multiple": "これらのソースからの新しいレコードは次の場所に保存されます:{directoryNames}",
|
||||
"feedback_sources_directory_access_single": "このソースからの新しいレコードは次の場所に保存されます:{directoryNames}",
|
||||
"feedback_sources_settings_description": "このワークスペースのすべてのフィードバックソースを接続・管理します。",
|
||||
"field_group_id": "フィールドグループID",
|
||||
"field_group_label": "フィールドグループラベル",
|
||||
"field_id": "フィールドID",
|
||||
"field_label": "フィールドラベル",
|
||||
"field_type": "フィールドタイプ",
|
||||
"formbricks_surveys": "Formbricks フォーム",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "過去の回答をインポート",
|
||||
"import_historical_responses_description": "このアンケートから既存の回答を今すぐインポートします。",
|
||||
"import_rows": "{count}行をインポート",
|
||||
"import_via_source_name": "「{sourceName}」経由でインポート",
|
||||
"importing_data": "データをインポート中...",
|
||||
"importing_historical_data": "過去のデータをインポート中...",
|
||||
"invalid_enum_values": "{field}にマッピングされた列に無効な値があります",
|
||||
"invalid_values_found": "検出された値: {values}(行: {rows}){extra}",
|
||||
"load_sample_csv": "サンプルCSVを読み込む",
|
||||
"manage_directories": "ディレクトリを管理",
|
||||
"manage_feedback_sources": "フィードバックソースを管理する",
|
||||
"metadata": "メタデータ",
|
||||
"metadata_key": "メタデータキー",
|
||||
"metadata_read_only_entries": "読み取り専用メタデータ値 (非文字列)",
|
||||
"metadata_value": "メタデータ値",
|
||||
"missing_feedback_source_title": "フィードバックソースが見つかりませんか?",
|
||||
"no_feedback_record_directory_available": "このワークスペースにフィードバックレコードディレクトリが割り当てられていません。まず作成または割り当てを行ってください。",
|
||||
"no_feedback_records": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "フォームを選択して質問を表示",
|
||||
"select_a_value": "値を選択...",
|
||||
"select_feedback_record_directory": "ディレクトリを選択",
|
||||
"select_feedback_record_source_type": "ソースタイプを選択してください",
|
||||
"select_questions": "質問を選択",
|
||||
"select_source_type_description": "接続するフィードバックソースの種類を選択してください。",
|
||||
"select_survey": "フォームを選択",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "MCP統合を通じてフィードバックレコードを送信します。",
|
||||
"source_connect_formbricks_description": "Formbricksフォームからフィードバックを接続",
|
||||
"source_fields": "ソースフィールド",
|
||||
"source_id": "ソースID",
|
||||
"source_name": "ソース名",
|
||||
"source_type": "ソースタイプ",
|
||||
"source_type_cannot_be_changed": "ソースタイプは変更できません",
|
||||
"sources": "ソース",
|
||||
"status_error": "エラー",
|
||||
"status_live_sync": "リアルタイム同期",
|
||||
"status_paused": "一時停止",
|
||||
"status_ready": "準備完了",
|
||||
"submission_id": "提出ID",
|
||||
"survey_has_no_questions": "このアンケートには質問がありません",
|
||||
"topics_and_subtopics": "トピックとサブトピック",
|
||||
"unify_feedback": "フィードバックを統合",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "CSVファイルをアップロードして、フィードバックデータをインポートします。",
|
||||
"upload_csv_file": "CSVファイルをアップロード",
|
||||
"user_identifier": "ユーザー",
|
||||
"value": "値"
|
||||
"value": "値",
|
||||
"value_boolean": "値 (ブール値)",
|
||||
"value_date": "値 (日付)",
|
||||
"value_number": "値(数値)",
|
||||
"value_text": "値 (テキスト)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "U bent niet geverifieerd om deze actie uit te voeren.",
|
||||
"not_authorized": "Niet geautoriseerd",
|
||||
"not_connected": "Niet verbonden",
|
||||
"not_set": "Niet ingesteld",
|
||||
"note": "Opmerking",
|
||||
"notifications": "Meldingen",
|
||||
"number": "Nummer",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Bekijk welke teams toegang hebben tot deze workspace."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Feedbackrecord toevoegen",
|
||||
"add_feedback_record_description": "Maak handmatig een feedbackrecord.",
|
||||
"add_feedback_source": "Feedbackbron toevoegen",
|
||||
"add_source": "Bron toevoegen",
|
||||
"allowed_values": "Toegestane waarden: {values}",
|
||||
"api_ingestion": "API-inname",
|
||||
"api_ingestion_manage_api_keys": "API-sleutels beheren",
|
||||
"api_ingestion_settings_description": "Verstuur feedbackrecords via de Management API.",
|
||||
"auto_generated": "Automatisch gegenereerd",
|
||||
"change_file": "Bestand wijzigen",
|
||||
"click_load_sample_csv": "Klik op 'Voorbeeld CSV laden' om kolommen te zien",
|
||||
"click_to_upload": "Klik om te uploaden",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Gegevens twee keer importeren zal dubbele records aanmaken.",
|
||||
"csv_inconsistent_columns": "Rij {row} heeft inconsistente kolommen. Alle rijen moeten dezelfde headers hebben.",
|
||||
"csv_max_records": "Maximaal {max} records toegestaan.",
|
||||
"custom_source_type": "Aangepast brontype",
|
||||
"custom_source_type_placeholder": "Voer een aangepast brontype in",
|
||||
"default_connector_name_csv": "CSV import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey verbinding",
|
||||
"discard_feedback_record_changes_description": "Als u deze lade sluit, gaan uw wijzigingen verloren.",
|
||||
"discard_feedback_record_changes_title": "Niet-opgeslagen wijzigingen verwijderen?",
|
||||
"drop_a_field_here": "Zet hier een veld neer",
|
||||
"drop_field_or": "Zet veld neer of",
|
||||
"edit_csv_mapping": "CSV-mapping bewerken",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Kan feedbackrecords niet laden",
|
||||
"feedback_date": "Huidige datum",
|
||||
"feedback_record_created_successfully": "Feedbackrecord is succesvol aangemaakt",
|
||||
"feedback_record_details": "Details van feedbackrecord",
|
||||
"feedback_record_details_description": "Controleer en update de feedbackrecordvelden.",
|
||||
"feedback_record_directory": "Feedbackrecordmap",
|
||||
"feedback_record_fields": "Feedbackrecordvelden",
|
||||
"feedback_record_mcp": "Feedbackrecord MCP",
|
||||
"feedback_record_updated_successfully": "Feedbackrecord is succesvol bijgewerkt",
|
||||
"feedback_record_value_required": "Er is een waarde vereist voor het geselecteerde veldtype",
|
||||
"feedback_records": "Feedbackrecords",
|
||||
"feedback_records_refreshed": "Feedbackrecords vernieuwd",
|
||||
"feedback_sources": "Feedbackbronnen",
|
||||
"feedback_sources_directory_access_multiple": "Nieuwe records van deze bronnen worden opgeslagen in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Nieuwe records van deze bron worden opgeslagen in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Verbind en beheer alle feedbackbronnen voor deze werkruimte.",
|
||||
"field_group_id": "Veldgroep-ID",
|
||||
"field_group_label": "Veldgroeplabel",
|
||||
"field_id": "Veld-ID",
|
||||
"field_label": "Veldlabel",
|
||||
"field_type": "Veldtype",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Historische reacties importeren",
|
||||
"import_historical_responses_description": "Importeer bestaande reacties van deze enquête nu.",
|
||||
"import_rows": "{count, plural, one {Importeer 1 rij} other {Importeer # rijen}}",
|
||||
"import_via_source_name": "Importeren via \"{sourceName}\"",
|
||||
"importing_data": "Gegevens importeren...",
|
||||
"importing_historical_data": "Historische gegevens importeren...",
|
||||
"invalid_enum_values": "Ongeldige waarden in kolom gekoppeld aan {field}",
|
||||
"invalid_values_found": "Gevonden: {values} (rijen: {rows}) {extra}",
|
||||
"load_sample_csv": "Voorbeeld-CSV laden",
|
||||
"manage_directories": "Mappen beheren",
|
||||
"manage_feedback_sources": "Beheer feedbackbronnen",
|
||||
"metadata": "Metagegevens",
|
||||
"metadata_key": "Metagegevenssleutel",
|
||||
"metadata_read_only_entries": "Alleen-lezen metadatawaarden (niet-tekenreeks)",
|
||||
"metadata_value": "Metagegevenswaarde",
|
||||
"missing_feedback_source_title": "Mis je een feedbackbron?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien",
|
||||
"select_a_value": "Selecteer een waarde...",
|
||||
"select_feedback_record_directory": "Selecteer een map",
|
||||
"select_feedback_record_source_type": "Selecteer brontype",
|
||||
"select_questions": "Selecteer vragen",
|
||||
"select_source_type_description": "Selecteer het type feedbackbron dat je wilt verbinden.",
|
||||
"select_survey": "Selecteer enquête",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Verstuur feedbackrecords via de MCP-integratie.",
|
||||
"source_connect_formbricks_description": "Verbind feedback van je Formbricks-enquêtes",
|
||||
"source_fields": "Bronvelden",
|
||||
"source_id": "Bron-ID",
|
||||
"source_name": "Bronnaam",
|
||||
"source_type": "Brontype",
|
||||
"source_type_cannot_be_changed": "Brontype kan niet worden gewijzigd",
|
||||
"sources": "Bronnen",
|
||||
"status_error": "Fout",
|
||||
"status_live_sync": "Live synchronisatie",
|
||||
"status_paused": "Gepauzeerd",
|
||||
"status_ready": "Klaar",
|
||||
"submission_id": "Inzendings-ID",
|
||||
"survey_has_no_questions": "Deze enquête heeft geen vragen",
|
||||
"topics_and_subtopics": "Onderwerpen en subonderwerpen",
|
||||
"unify_feedback": "Feedback verenigen",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Upload een CSV-bestand om feedbackgegevens te importeren.",
|
||||
"upload_csv_file": "CSV-bestand uploaden",
|
||||
"user_identifier": "Gebruiker",
|
||||
"value": "Waarde"
|
||||
"value": "Waarde",
|
||||
"value_boolean": "Waarde (Booleaans)",
|
||||
"value_date": "Waarde (datum)",
|
||||
"value_number": "Waarde (getal)",
|
||||
"value_text": "Waarde (tekst)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Você não está autenticado para realizar essa ação.",
|
||||
"not_authorized": "Não autorizado",
|
||||
"not_connected": "Desconectado",
|
||||
"not_set": "Não definido",
|
||||
"note": "Nota",
|
||||
"notifications": "Notificações",
|
||||
"number": "Número",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Veja quais equipes podem acessar este workspace."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Adicionar registro de feedback",
|
||||
"add_feedback_record_description": "Crie um registro de feedback manualmente.",
|
||||
"add_feedback_source": "Adicionar fonte de feedback",
|
||||
"add_source": "Adicionar fonte",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"api_ingestion": "Ingestão de API",
|
||||
"api_ingestion_manage_api_keys": "Gerenciar chaves de API",
|
||||
"api_ingestion_settings_description": "Envie registros de feedback usando a API de Gerenciamento.",
|
||||
"auto_generated": "Gerado automaticamente",
|
||||
"change_file": "Alterar arquivo",
|
||||
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
|
||||
"click_to_upload": "Clique para fazer upload",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importar dados duas vezes criará registros duplicados.",
|
||||
"csv_inconsistent_columns": "A linha {row} possui colunas inconsistentes. Todas as linhas devem ter os mesmos cabeçalhos.",
|
||||
"csv_max_records": "Máximo de {max} registros permitidos.",
|
||||
"custom_source_type": "Tipo de origem personalizado",
|
||||
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
|
||||
"default_connector_name_csv": "Importação CSV",
|
||||
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
|
||||
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
|
||||
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
|
||||
"drop_a_field_here": "Solte um campo aqui",
|
||||
"drop_field_or": "Solte o campo ou",
|
||||
"edit_csv_mapping": "Editar mapeamento CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Falha ao carregar registros de feedback",
|
||||
"feedback_date": "Data atual",
|
||||
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
|
||||
"feedback_record_details": "Detalhes do registro de feedback",
|
||||
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
|
||||
"feedback_record_directory": "Diretório de Registros de Feedback",
|
||||
"feedback_record_fields": "Campos do registro de feedback",
|
||||
"feedback_record_mcp": "Registro de Feedback MCP",
|
||||
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
|
||||
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
|
||||
"feedback_records": "Registros de feedback",
|
||||
"feedback_records_refreshed": "Registros de feedback atualizados",
|
||||
"feedback_sources": "Fontes de Feedback",
|
||||
"feedback_sources_directory_access_multiple": "Novos registros dessas fontes serão armazenados em: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Novos registros desta fonte serão armazenados em: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Conecte e gerencie todas as fontes de feedback para este workspace.",
|
||||
"field_group_id": "ID do grupo de campos",
|
||||
"field_group_label": "Etiqueta do grupo de campos",
|
||||
"field_id": "ID do campo",
|
||||
"field_label": "Rótulo do campo",
|
||||
"field_type": "Tipo de campo",
|
||||
"formbricks_surveys": "Pesquisas Formbricks",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Importar respostas históricas",
|
||||
"import_historical_responses_description": "Importe respostas existentes desta pesquisa agora.",
|
||||
"import_rows": "Importar {count} linhas",
|
||||
"import_via_source_name": "Importar via \"{sourceName}\"",
|
||||
"importing_data": "Importando dados...",
|
||||
"importing_historical_data": "Importando dados históricos...",
|
||||
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
|
||||
"load_sample_csv": "Carregar CSV de exemplo",
|
||||
"manage_directories": "Gerenciar diretórios",
|
||||
"manage_feedback_sources": "Gerenciar fontes de feedback",
|
||||
"metadata": "Metadados",
|
||||
"metadata_key": "Chave de metadados",
|
||||
"metadata_read_only_entries": "Valores de metadados somente leitura (sem string)",
|
||||
"metadata_value": "Valor dos metadados",
|
||||
"missing_feedback_source_title": "Faltando alguma fonte de feedback?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas",
|
||||
"select_a_value": "Selecione um valor...",
|
||||
"select_feedback_record_directory": "Selecione um diretório",
|
||||
"select_feedback_record_source_type": "Selecione o tipo de fonte",
|
||||
"select_questions": "Selecionar perguntas",
|
||||
"select_source_type_description": "Selecione o tipo de fonte de feedback que você deseja conectar.",
|
||||
"select_survey": "Selecionar pesquisa",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Envie registros de feedback através da integração MCP.",
|
||||
"source_connect_formbricks_description": "Conectar feedback das suas pesquisas Formbricks",
|
||||
"source_fields": "Campos de origem",
|
||||
"source_id": "ID da fonte",
|
||||
"source_name": "Nome da origem",
|
||||
"source_type": "Tipo de fonte",
|
||||
"source_type_cannot_be_changed": "O tipo de origem não pode ser alterado",
|
||||
"sources": "Origens",
|
||||
"status_error": "Erro",
|
||||
"status_live_sync": "Sincronização ao vivo",
|
||||
"status_paused": "Pausado",
|
||||
"status_ready": "Pronto",
|
||||
"submission_id": "ID de envio",
|
||||
"survey_has_no_questions": "Esta pesquisa não possui perguntas",
|
||||
"topics_and_subtopics": "Tópicos e subtópicos",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Faça upload de um arquivo CSV para importar dados de feedback.",
|
||||
"upload_csv_file": "Fazer upload de arquivo CSV",
|
||||
"user_identifier": "Usuário",
|
||||
"value": "Valor"
|
||||
"value": "Valor",
|
||||
"value_boolean": "Valor (Booleano)",
|
||||
"value_date": "Valor (Data)",
|
||||
"value_number": "Valor (Número)",
|
||||
"value_text": "Valor (Texto)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Não está autenticado para realizar esta ação.",
|
||||
"not_authorized": "Não autorizado",
|
||||
"not_connected": "Não Conectado",
|
||||
"not_set": "Não definido",
|
||||
"note": "Nota",
|
||||
"notifications": "Notificações",
|
||||
"number": "Número",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Veja quais as equipas que podem aceder a este espaço de trabalho."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Adicionar registro de feedback",
|
||||
"add_feedback_record_description": "Crie um registro de feedback manualmente.",
|
||||
"add_feedback_source": "Adicionar fonte de feedback",
|
||||
"add_source": "Adicionar fonte",
|
||||
"allowed_values": "Valores permitidos: {values}",
|
||||
"api_ingestion": "Ingestão de API",
|
||||
"api_ingestion_manage_api_keys": "Gerir chaves de API",
|
||||
"api_ingestion_settings_description": "Envia registos de feedback através da API de gestão.",
|
||||
"auto_generated": "Gerado automaticamente",
|
||||
"change_file": "Alterar ficheiro",
|
||||
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
|
||||
"click_to_upload": "Clique para carregar",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importar dados duas vezes irá criar registos duplicados.",
|
||||
"csv_inconsistent_columns": "A linha {row} tem colunas inconsistentes. Todas as linhas devem ter os mesmos cabeçalhos.",
|
||||
"csv_max_records": "Máximo de {max} registos permitidos.",
|
||||
"custom_source_type": "Tipo de origem personalizado",
|
||||
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
|
||||
"default_connector_name_csv": "Importação CSV",
|
||||
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
|
||||
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
|
||||
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
|
||||
"drop_a_field_here": "Solte um campo aqui",
|
||||
"drop_field_or": "Solte o campo ou",
|
||||
"edit_csv_mapping": "Editar mapeamento CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Falha ao carregar registos de feedback",
|
||||
"feedback_date": "Data atual",
|
||||
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
|
||||
"feedback_record_details": "Detalhes do registro de feedback",
|
||||
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
|
||||
"feedback_record_directory": "Diretório de Registos de Feedback",
|
||||
"feedback_record_fields": "Campos de registo de feedback",
|
||||
"feedback_record_mcp": "MCP de Registo de Feedback",
|
||||
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
|
||||
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
|
||||
"feedback_records": "Registos de feedback",
|
||||
"feedback_records_refreshed": "Registos de feedback atualizados",
|
||||
"feedback_sources": "Fontes de Feedback",
|
||||
"feedback_sources_directory_access_multiple": "Novos registos destas fontes serão armazenados em: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Novos registos desta fonte serão armazenados em: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Liga e gere todas as fontes de feedback para este espaço de trabalho.",
|
||||
"field_group_id": "ID do grupo de campos",
|
||||
"field_group_label": "Etiqueta do grupo de campos",
|
||||
"field_id": "ID do campo",
|
||||
"field_label": "Etiqueta do campo",
|
||||
"field_type": "Tipo de campo",
|
||||
"formbricks_surveys": "Pesquisas Formbricks",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Importar respostas históricas",
|
||||
"import_historical_responses_description": "Importa agora as respostas existentes deste inquérito.",
|
||||
"import_rows": "Importar {count} linhas",
|
||||
"import_via_source_name": "Importar via \"{sourceName}\"",
|
||||
"importing_data": "A importar dados...",
|
||||
"importing_historical_data": "A importar dados históricos...",
|
||||
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
|
||||
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
|
||||
"load_sample_csv": "Carregar CSV de exemplo",
|
||||
"manage_directories": "Gerir diretórios",
|
||||
"manage_feedback_sources": "Gerenciar fontes de feedback",
|
||||
"metadata": "Metadados",
|
||||
"metadata_key": "Chave de metadados",
|
||||
"metadata_read_only_entries": "Valores de metadados somente leitura (sem string)",
|
||||
"metadata_value": "Valor dos metadados",
|
||||
"missing_feedback_source_title": "Falta alguma fonte de feedback?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas",
|
||||
"select_a_value": "Selecione um valor...",
|
||||
"select_feedback_record_directory": "Selecionar um diretório",
|
||||
"select_feedback_record_source_type": "Selecione o tipo de fonte",
|
||||
"select_questions": "Selecionar perguntas",
|
||||
"select_source_type_description": "Selecione o tipo de fonte de feedback que pretende conectar.",
|
||||
"select_survey": "Selecionar inquérito",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Envia registos de feedback através da integração MCP.",
|
||||
"source_connect_formbricks_description": "Conectar feedback dos seus inquéritos Formbricks",
|
||||
"source_fields": "Campos da fonte",
|
||||
"source_id": "ID da fonte",
|
||||
"source_name": "Nome da fonte",
|
||||
"source_type": "Tipo de fonte",
|
||||
"source_type_cannot_be_changed": "O tipo de fonte não pode ser alterado",
|
||||
"sources": "Fontes",
|
||||
"status_error": "Erro",
|
||||
"status_live_sync": "Sincronização em direto",
|
||||
"status_paused": "Em pausa",
|
||||
"status_ready": "Pronto",
|
||||
"submission_id": "ID de envio",
|
||||
"survey_has_no_questions": "Este inquérito não tem perguntas",
|
||||
"topics_and_subtopics": "Tópicos e subtópicos",
|
||||
"unify_feedback": "Unificar feedback",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Carrega um ficheiro CSV para importar dados de feedback.",
|
||||
"upload_csv_file": "Carregar ficheiro CSV",
|
||||
"user_identifier": "Utilizador",
|
||||
"value": "Valor"
|
||||
"value": "Valor",
|
||||
"value_boolean": "Valor (Booleano)",
|
||||
"value_date": "Valor (Data)",
|
||||
"value_number": "Valor (Número)",
|
||||
"value_text": "Valor (Texto)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Nu sunteți autentificat pentru a efectua această acțiune.",
|
||||
"not_authorized": "Neautorizat",
|
||||
"not_connected": "Neconectat",
|
||||
"not_set": "Nu setat",
|
||||
"note": "Notă",
|
||||
"notifications": "Notificări",
|
||||
"number": "Număr",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Vedeți ce echipe pot accesa acest spațiu de lucru."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Adăugați înregistrarea de feedback",
|
||||
"add_feedback_record_description": "Creați manual o înregistrare de feedback.",
|
||||
"add_feedback_source": "Adaugă sursă de feedback",
|
||||
"add_source": "Adaugă sursă",
|
||||
"allowed_values": "Valori permise: {values}",
|
||||
"api_ingestion": "Ingestie API",
|
||||
"api_ingestion_manage_api_keys": "Gestionează cheile API",
|
||||
"api_ingestion_settings_description": "Trimite înregistrări de feedback folosind API-ul de management.",
|
||||
"auto_generated": "Generat automat",
|
||||
"change_file": "Schimbă fișierul",
|
||||
"click_load_sample_csv": "Apasă pe „Încarcă CSV de exemplu” pentru a vedea coloanele",
|
||||
"click_to_upload": "Apasă pentru a încărca",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Importarea datelor de două ori va crea înregistrări duplicate.",
|
||||
"csv_inconsistent_columns": "Rândul {row} are coloane inconsistente. Toate rândurile trebuie să aibă aceleași antete.",
|
||||
"csv_max_records": "Sunt permise maximum {max} înregistrări.",
|
||||
"custom_source_type": "Tip sursă personalizat",
|
||||
"custom_source_type_placeholder": "Introduceți tipul de sursă personalizat",
|
||||
"default_connector_name_csv": "Import CSV",
|
||||
"default_connector_name_formbricks": "Conexiune chestionar Formbricks",
|
||||
"discard_feedback_record_changes_description": "Modificările dvs. se vor pierde dacă închideți acest sertar.",
|
||||
"discard_feedback_record_changes_title": "Renunțați la modificările nesalvate?",
|
||||
"drop_a_field_here": "Trage un câmp aici",
|
||||
"drop_field_or": "Trage câmpul sau",
|
||||
"edit_csv_mapping": "Editează maparea CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback",
|
||||
"feedback_date": "Data curentă",
|
||||
"feedback_record_created_successfully": "Înregistrare de feedback creată cu succes",
|
||||
"feedback_record_details": "Detaliile înregistrării feedback-ului",
|
||||
"feedback_record_details_description": "Examinați și actualizați câmpurile pentru înregistrarea de feedback.",
|
||||
"feedback_record_directory": "Director de înregistrări feedback",
|
||||
"feedback_record_fields": "Câmpuri înregistrare feedback",
|
||||
"feedback_record_mcp": "MCP Înregistrări Feedback",
|
||||
"feedback_record_updated_successfully": "Înregistrarea feedback-ului a fost actualizată cu succes",
|
||||
"feedback_record_value_required": "Este necesară o valoare pentru tipul de câmp selectat",
|
||||
"feedback_records": "Înregistrări de feedback",
|
||||
"feedback_records_refreshed": "Înregistrările de feedback au fost actualizate",
|
||||
"feedback_sources": "Surse de feedback",
|
||||
"feedback_sources_directory_access_multiple": "Înregistrările noi din aceste surse vor fi stocate în: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Înregistrările noi din această sursă vor fi stocate în: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Conectează și gestionează toate sursele de feedback pentru acest spațiu de lucru.",
|
||||
"field_group_id": "ID grup de câmpuri",
|
||||
"field_group_label": "Eticheta grupului de câmpuri",
|
||||
"field_id": "ID-ul câmpului",
|
||||
"field_label": "Etichetă câmp",
|
||||
"field_type": "Tip câmp",
|
||||
"formbricks_surveys": "Chestionare Formbricks",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Importă răspunsuri istorice",
|
||||
"import_historical_responses_description": "Importă acum răspunsurile existente din acest sondaj.",
|
||||
"import_rows": "Importă {count, plural, one {# rând} few {# rânduri} other {# de rânduri}}",
|
||||
"import_via_source_name": "Import prin „{sourceName}”",
|
||||
"importing_data": "Se importă datele...",
|
||||
"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_sample_csv": "Încarcă un CSV de exemplu",
|
||||
"manage_directories": "Gestionează directoarele",
|
||||
"manage_feedback_sources": "Gestionați sursele de feedback",
|
||||
"metadata": "Metadate",
|
||||
"metadata_key": "Cheia de metadate",
|
||||
"metadata_read_only_entries": "Valori de metadate numai pentru citire (fără șir)",
|
||||
"metadata_value": "Valoarea metadatelor",
|
||||
"missing_feedback_source_title": "Lipsește o sursă de feedback?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările",
|
||||
"select_a_value": "Selectează o valoare...",
|
||||
"select_feedback_record_directory": "Selectează un director",
|
||||
"select_feedback_record_source_type": "Selectați tipul sursei",
|
||||
"select_questions": "Selectează întrebări",
|
||||
"select_source_type_description": "Selectează tipul sursei de feedback pe care vrei să o conectezi.",
|
||||
"select_survey": "Selectează chestionar",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Trimite înregistrări de feedback prin integrarea MCP.",
|
||||
"source_connect_formbricks_description": "Conectează feedback din sondajele Formbricks",
|
||||
"source_fields": "Câmpuri sursă",
|
||||
"source_id": "ID sursă",
|
||||
"source_name": "Nume sursă",
|
||||
"source_type": "Tip sursă",
|
||||
"source_type_cannot_be_changed": "Tipul sursei nu poate fi schimbat",
|
||||
"sources": "Surse",
|
||||
"status_error": "Eroare",
|
||||
"status_live_sync": "Sincronizare în timp real",
|
||||
"status_paused": "Pauzat",
|
||||
"status_ready": "Gata",
|
||||
"submission_id": "ID-ul trimiterii",
|
||||
"survey_has_no_questions": "Acest sondaj nu are întrebări",
|
||||
"topics_and_subtopics": "Subiecte și subiecte secundare",
|
||||
"unify_feedback": "Unify Feedback",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Încarcă un fișier CSV pentru a importa date de feedback.",
|
||||
"upload_csv_file": "Încarcă fișier CSV",
|
||||
"user_identifier": "Utilizator",
|
||||
"value": "Valoare"
|
||||
"value": "Valoare",
|
||||
"value_boolean": "Valoare (booleană)",
|
||||
"value_date": "Valoare (data)",
|
||||
"value_number": "Valoare (număr)",
|
||||
"value_text": "Valoare (Text)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "У вас нет прав для выполнения этого действия.",
|
||||
"not_authorized": "Нет доступа",
|
||||
"not_connected": "Нет подключения",
|
||||
"not_set": "Не установлено",
|
||||
"note": "Примечание",
|
||||
"notifications": "Уведомления",
|
||||
"number": "Номер",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Посмотрите, какие команды имеют доступ к этому рабочему пространству."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Добавить запись отзыва",
|
||||
"add_feedback_record_description": "Создайте запись обратной связи вручную.",
|
||||
"add_feedback_source": "Добавить источник отзывов",
|
||||
"add_source": "Добавить источник",
|
||||
"allowed_values": "Допустимые значения: {values}",
|
||||
"api_ingestion": "Импорт через API",
|
||||
"api_ingestion_manage_api_keys": "Управление API-ключами",
|
||||
"api_ingestion_settings_description": "Отправляйте записи обратной связи через Management API.",
|
||||
"auto_generated": "Автоматически генерируется",
|
||||
"change_file": "Изменить файл",
|
||||
"click_load_sample_csv": "Нажмите «Загрузить пример CSV», чтобы увидеть столбцы",
|
||||
"click_to_upload": "Кликните для загрузки",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Импорт уже загруженных данных может создать дубликаты записей.",
|
||||
"csv_inconsistent_columns": "В строке {row} несоответствие столбцов. Во всех строках должны быть одинаковые заголовки.",
|
||||
"csv_max_records": "Допустимо не более {max} записей.",
|
||||
"custom_source_type": "Пользовательский тип источника",
|
||||
"custom_source_type_placeholder": "Введите собственный тип источника",
|
||||
"default_connector_name_csv": "Импорт CSV",
|
||||
"default_connector_name_formbricks": "Подключение опроса Formbricks",
|
||||
"discard_feedback_record_changes_description": "Ваши изменения будут потеряны, если вы закроете этот ящик.",
|
||||
"discard_feedback_record_changes_title": "Отменить несохраненные изменения?",
|
||||
"drop_a_field_here": "Перетащи сюда поле",
|
||||
"drop_field_or": "Перетащи поле или",
|
||||
"edit_csv_mapping": "Редактировать сопоставление CSV",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Не удалось загрузить отзывы",
|
||||
"feedback_date": "Текущая дата",
|
||||
"feedback_record_created_successfully": "Запись отзыва успешно создана",
|
||||
"feedback_record_details": "Детали записи обратной связи",
|
||||
"feedback_record_details_description": "Просмотрите и обновите поля записи отзыва.",
|
||||
"feedback_record_directory": "Каталог записей обратной связи",
|
||||
"feedback_record_fields": "Поля записи отзыва",
|
||||
"feedback_record_mcp": "MCP для записей обратной связи",
|
||||
"feedback_record_updated_successfully": "Запись отзыва успешно обновлена.",
|
||||
"feedback_record_value_required": "Требуется значение для выбранного типа поля.",
|
||||
"feedback_records": "Записи отзывов",
|
||||
"feedback_records_refreshed": "Записи отзывов обновлены",
|
||||
"feedback_sources": "Источники обратной связи",
|
||||
"feedback_sources_directory_access_multiple": "Новые записи из этих источников будут сохранены в: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "Новые записи из этого источника будут сохранены в: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Подключайте источники обратной связи и управляйте ими для этого рабочего пространства.",
|
||||
"field_group_id": "Идентификатор группы полей",
|
||||
"field_group_label": "Метка группы полей",
|
||||
"field_id": "Идентификатор поля",
|
||||
"field_label": "Метка поля",
|
||||
"field_type": "Тип поля",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Импортировать предыдущие ответы",
|
||||
"import_historical_responses_description": "Импортируйте существующие ответы из этого опроса прямо сейчас.",
|
||||
"import_rows": "Импортировать {count, plural, one {# строку} few {# строки} many {# строк} other {# строки}}",
|
||||
"import_via_source_name": "Импорт через «{sourceName}»",
|
||||
"importing_data": "Импорт данных...",
|
||||
"importing_historical_data": "Импорт исторических данных...",
|
||||
"invalid_enum_values": "Недопустимые значения в столбце, сопоставленном с {field}",
|
||||
"invalid_values_found": "Найдено: {values} (строки: {rows}) {extra}",
|
||||
"load_sample_csv": "Загрузить пример CSV",
|
||||
"manage_directories": "Управление директориями",
|
||||
"manage_feedback_sources": "Управление источниками обратной связи",
|
||||
"metadata": "Метаданные",
|
||||
"metadata_key": "Ключ метаданных",
|
||||
"metadata_read_only_entries": "Значения метаданных только для чтения (нестроковые)",
|
||||
"metadata_value": "Значение метаданных",
|
||||
"missing_feedback_source_title": "Не нашли нужный источник обратной связи?",
|
||||
"no_feedback_record_directory_available": "К этому рабочему пространству не назначен каталог записей обратной связи. Сначала создайте или назначьте каталог.",
|
||||
"no_feedback_records": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы",
|
||||
"select_a_value": "Выберите значение...",
|
||||
"select_feedback_record_directory": "Выберите каталог",
|
||||
"select_feedback_record_source_type": "Выберите тип источника",
|
||||
"select_questions": "Выберите вопросы",
|
||||
"select_source_type_description": "Выберите тип источника отзывов, который хотите подключить.",
|
||||
"select_survey": "Выбрать опрос",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Отправляйте записи обратной связи через интеграцию MCP.",
|
||||
"source_connect_formbricks_description": "Подключить отзывы из ваших опросов Formbricks",
|
||||
"source_fields": "Поля источника",
|
||||
"source_id": "Идентификатор источника",
|
||||
"source_name": "Имя источника",
|
||||
"source_type": "Тип источника",
|
||||
"source_type_cannot_be_changed": "Тип источника нельзя изменить",
|
||||
"sources": "Источники",
|
||||
"status_error": "Ошибка",
|
||||
"status_live_sync": "Синхронизация в реальном времени",
|
||||
"status_paused": "Приостановлен",
|
||||
"status_ready": "Готово",
|
||||
"submission_id": "Идентификатор отправки",
|
||||
"survey_has_no_questions": "В этом опросе нет вопросов",
|
||||
"topics_and_subtopics": "Темы и подтемы",
|
||||
"unify_feedback": "Обратная связь Unify",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Загрузи CSV-файл, чтобы импортировать данные отзывов.",
|
||||
"upload_csv_file": "Загрузить CSV-файл",
|
||||
"user_identifier": "Пользователь",
|
||||
"value": "Значение"
|
||||
"value": "Значение",
|
||||
"value_boolean": "Значение (логическое)",
|
||||
"value_date": "Значение (Дата)",
|
||||
"value_number": "Значение (число)",
|
||||
"value_text": "Значение (текст)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Du är inte autentiserad för att utföra denna åtgärd.",
|
||||
"not_authorized": "Ej behörig",
|
||||
"not_connected": "Ej ansluten",
|
||||
"not_set": "Inte inställt",
|
||||
"note": "Anteckning",
|
||||
"notifications": "Aviseringar",
|
||||
"number": "Nummer",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Se vilka team som har tillgång till denna arbetsyta."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Lägg till feedbackpost",
|
||||
"add_feedback_record_description": "Skapa en feedbackpost manuellt.",
|
||||
"add_feedback_source": "Lägg till feedbackkälla",
|
||||
"add_source": "Lägg till källa",
|
||||
"allowed_values": "Tillåtna värden: {values}",
|
||||
"api_ingestion": "API ingestion",
|
||||
"api_ingestion_manage_api_keys": "Manage API keys",
|
||||
"api_ingestion_settings_description": "Send feedback records using the Management API.",
|
||||
"auto_generated": "Automatiskt genererad",
|
||||
"change_file": "Byt fil",
|
||||
"click_load_sample_csv": "Klicka på 'Ladda exempel-CSV' för att se kolumner",
|
||||
"click_to_upload": "Klicka för att ladda upp",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Om du importerar data två gånger kommer det att skapa dubbletter.",
|
||||
"csv_inconsistent_columns": "Rad {row} har inkonsekventa kolumner. Alla rader måste ha samma rubriker.",
|
||||
"csv_max_records": "Maximalt {max} poster tillåtna.",
|
||||
"custom_source_type": "Anpassad källtyp",
|
||||
"custom_source_type_placeholder": "Ange anpassad källtyp",
|
||||
"default_connector_name_csv": "CSV-import",
|
||||
"default_connector_name_formbricks": "Formbricks Survey-anslutning",
|
||||
"discard_feedback_record_changes_description": "Dina ändringar kommer att gå förlorade om du stänger den här lådan.",
|
||||
"discard_feedback_record_changes_title": "Vill du ignorera osparade ändringar?",
|
||||
"drop_a_field_here": "Släpp ett fält här",
|
||||
"drop_field_or": "Släpp fält eller",
|
||||
"edit_csv_mapping": "Redigera CSV-mappning",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter",
|
||||
"feedback_date": "Aktuellt datum",
|
||||
"feedback_record_created_successfully": "Feedbackposten har skapats",
|
||||
"feedback_record_details": "Feedbackpostdetaljer",
|
||||
"feedback_record_details_description": "Granska och uppdatera fält för feedbackposter.",
|
||||
"feedback_record_directory": "Katalog för feedbackposter",
|
||||
"feedback_record_fields": "Fält för feedbackpost",
|
||||
"feedback_record_mcp": "Feedback Record MCP",
|
||||
"feedback_record_updated_successfully": "Feedbackposten har uppdaterats",
|
||||
"feedback_record_value_required": "Ett värde krävs för den valda fälttypen",
|
||||
"feedback_records": "Feedbackposter",
|
||||
"feedback_records_refreshed": "Feedbackposter har uppdaterats",
|
||||
"feedback_sources": "Feedback Sources",
|
||||
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
|
||||
"field_group_id": "Fältgrupp-ID",
|
||||
"field_group_label": "Fältgruppsetikett",
|
||||
"field_id": "Fält-ID",
|
||||
"field_label": "Fältetikett",
|
||||
"field_type": "Fälttyp",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Import historical responses",
|
||||
"import_historical_responses_description": "Import existing responses from this survey now.",
|
||||
"import_rows": "Importera {count} rader",
|
||||
"import_via_source_name": "Importera via \"{sourceName}\"",
|
||||
"importing_data": "Importerar data...",
|
||||
"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_sample_csv": "Ladda exempel-CSV",
|
||||
"manage_directories": "Manage directories",
|
||||
"manage_feedback_sources": "Hantera feedbackkällor",
|
||||
"metadata": "Metadata",
|
||||
"metadata_key": "Metadatanyckel",
|
||||
"metadata_read_only_entries": "Skrivskyddade metadatavärden (icke-sträng)",
|
||||
"metadata_value": "Metadatavärde",
|
||||
"missing_feedback_source_title": "Missing feedback source?",
|
||||
"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.",
|
||||
@@ -3700,6 +3722,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_feedback_record_directory": "Välj en katalog",
|
||||
"select_feedback_record_source_type": "Välj källtyp",
|
||||
"select_questions": "Välj frågor",
|
||||
"select_source_type_description": "Välj vilken typ av feedbackkälla du vill ansluta.",
|
||||
"select_survey": "Välj enkät",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
|
||||
"source_connect_formbricks_description": "Anslut feedback från dina Formbricks-enkäter",
|
||||
"source_fields": "Källfält",
|
||||
"source_id": "Käll-ID",
|
||||
"source_name": "Källnamn",
|
||||
"source_type": "Källtyp",
|
||||
"source_type_cannot_be_changed": "Källtyp kan inte ändras",
|
||||
"sources": "Källor",
|
||||
"status_error": "Fel",
|
||||
"status_live_sync": "Live sync",
|
||||
"status_paused": "Pausad",
|
||||
"status_ready": "Ready",
|
||||
"submission_id": "Inlämnings-ID",
|
||||
"survey_has_no_questions": "Den här enkäten har inga frågor",
|
||||
"topics_and_subtopics": "Ämnen och delämnen",
|
||||
"unify_feedback": "Samla feedback",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Ladda upp en CSV-fil för att importera feedbackdata.",
|
||||
"upload_csv_file": "Ladda upp CSV-fil",
|
||||
"user_identifier": "Användare",
|
||||
"value": "Värde"
|
||||
"value": "Värde",
|
||||
"value_boolean": "Värde (booleskt)",
|
||||
"value_date": "Värde (datum)",
|
||||
"value_number": "Värde (antal)",
|
||||
"value_text": "Värde (text)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "Bu işlemi gerçekleştirmek için yetkiniz yok.",
|
||||
"not_authorized": "Yetkisiz",
|
||||
"not_connected": "Bağlı Değil",
|
||||
"not_set": "Ayarlanmadı",
|
||||
"note": "Not",
|
||||
"notifications": "Bildirimler",
|
||||
"number": "Sayı",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "Bu çalışma alanına hangi takımların erişebildiğini görün."
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "Geri bildirim kaydı ekle",
|
||||
"add_feedback_record_description": "Manuel olarak bir geri bildirim kaydı oluşturun.",
|
||||
"add_feedback_source": "Geri Bildirim Kaynağı Ekle",
|
||||
"add_source": "Kaynak ekle",
|
||||
"allowed_values": "İzin verilen değerler: {values}",
|
||||
"api_ingestion": "API ingestion",
|
||||
"api_ingestion_manage_api_keys": "Manage API keys",
|
||||
"api_ingestion_settings_description": "Send feedback records using the Management API.",
|
||||
"auto_generated": "Otomatik olarak oluşturuldu",
|
||||
"change_file": "Dosyayı değiştir",
|
||||
"click_load_sample_csv": "Sütunları görmek için 'Örnek CSV yükle'ye tıkla",
|
||||
"click_to_upload": "Yüklemek için tıkla",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "Verileri iki kez içe aktarmak yinelenen kayıtlar oluşturacaktır.",
|
||||
"csv_inconsistent_columns": "Satır {row} tutarsız sütunlara sahip. Tüm satırlar aynı başlıklara sahip olmalıdır.",
|
||||
"csv_max_records": "Maksimum {max} kayda izin verilir.",
|
||||
"custom_source_type": "Özel kaynak türü",
|
||||
"custom_source_type_placeholder": "Özel kaynak türünü girin",
|
||||
"default_connector_name_csv": "CSV İçe Aktarma",
|
||||
"default_connector_name_formbricks": "Formbricks Anket Bağlantısı",
|
||||
"discard_feedback_record_changes_description": "Bu çekmeceyi kapatırsanız değişiklikleriniz kaybolacak.",
|
||||
"discard_feedback_record_changes_title": "Kaydedilmemiş değişiklikler silinsin mi?",
|
||||
"drop_a_field_here": "Buraya bir alan bırakın",
|
||||
"drop_field_or": "Alan bırakın veya",
|
||||
"edit_csv_mapping": "CSV eşlemesini düzenle",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "Geri bildirim kayıtları yüklenemedi",
|
||||
"feedback_date": "Geçerli tarih",
|
||||
"feedback_record_created_successfully": "Geri bildirim kaydı başarıyla oluşturuldu",
|
||||
"feedback_record_details": "Geri bildirim kaydı ayrıntıları",
|
||||
"feedback_record_details_description": "Geri bildirim kayıt alanlarını inceleyin ve güncelleyin.",
|
||||
"feedback_record_directory": "Geri Bildirim Kayıt Dizini",
|
||||
"feedback_record_fields": "Geri Bildirim Kayıt Alanları",
|
||||
"feedback_record_mcp": "Feedback Record MCP",
|
||||
"feedback_record_updated_successfully": "Geri bildirim kaydı başarıyla güncellendi",
|
||||
"feedback_record_value_required": "Seçilen alan türü için bir değer gerekli",
|
||||
"feedback_records": "Geri Bildirim Kayıtları",
|
||||
"feedback_records_refreshed": "Geri bildirim kayıtları yenilendi",
|
||||
"feedback_sources": "Feedback Sources",
|
||||
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
|
||||
"field_group_id": "Alan Grubu Kimliği",
|
||||
"field_group_label": "Alan Grubu Etiketi",
|
||||
"field_id": "Alan Kimliği",
|
||||
"field_label": "Alan Etiketi",
|
||||
"field_type": "Alan Türü",
|
||||
"formbricks_surveys": "Formbricks Anketleri",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Import historical responses",
|
||||
"import_historical_responses_description": "Import existing responses from this survey now.",
|
||||
"import_rows": "{count} satır içe aktar",
|
||||
"import_via_source_name": "\"{sourceName}\" yoluyla içe aktar",
|
||||
"importing_data": "Veri içe aktarılıyor...",
|
||||
"importing_historical_data": "Geçmiş veriler içe aktarılıyor...",
|
||||
"invalid_enum_values": "{field} alanına eşlenen sütunda geçersiz değerler",
|
||||
"invalid_values_found": "Bulunan: {values} (satırlar: {rows}) {extra}",
|
||||
"load_sample_csv": "Örnek CSV yükle",
|
||||
"manage_directories": "Manage directories",
|
||||
"manage_feedback_sources": "Geri bildirim kaynaklarını yönetin",
|
||||
"metadata": "Meta veriler",
|
||||
"metadata_key": "Meta veri anahtarı",
|
||||
"metadata_read_only_entries": "Salt okunur meta veri değerleri (dize dışı)",
|
||||
"metadata_value": "Meta veri değeri",
|
||||
"missing_feedback_source_title": "Missing feedback source?",
|
||||
"no_feedback_record_directory_available": "Bu çalışma alanına atanmış bir geri bildirim kayıt dizini yok. Önce bir tane oluştur veya ata.",
|
||||
"no_feedback_records": "Henüz geri bildirim kaydı yok. Bağlayıcıların veri göndermeye başlamasıyla kayıtlar burada görünecek.",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "Sorularını görmek için bir anket seç",
|
||||
"select_a_value": "Bir değer seç...",
|
||||
"select_feedback_record_directory": "Bir dizin seç",
|
||||
"select_feedback_record_source_type": "Kaynak türünü seçin",
|
||||
"select_questions": "Soru seç",
|
||||
"select_source_type_description": "Bağlamak istediğin geri bildirim kaynağının türünü seç.",
|
||||
"select_survey": "Anket Seç",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
|
||||
"source_connect_formbricks_description": "Formbricks anketlerinizdeki geri bildirimleri bağlayın",
|
||||
"source_fields": "Kaynak Alanları",
|
||||
"source_id": "Kaynak kimliği",
|
||||
"source_name": "Kaynak Adı",
|
||||
"source_type": "Kaynak Türü",
|
||||
"source_type_cannot_be_changed": "Kaynak türü değiştirilemez",
|
||||
"sources": "Kaynaklar",
|
||||
"status_error": "Hata",
|
||||
"status_live_sync": "Live sync",
|
||||
"status_paused": "Duraklatıldı",
|
||||
"status_ready": "Ready",
|
||||
"submission_id": "Gönderim Kimliği",
|
||||
"survey_has_no_questions": "Bu ankette soru yok",
|
||||
"topics_and_subtopics": "Konular ve alt konular",
|
||||
"unify_feedback": "Geri Bildirimleri Birleştir",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "Geri bildirim verilerini içe aktarmak için bir CSV dosyası yükle.",
|
||||
"upload_csv_file": "CSV Dosyası Yükle",
|
||||
"user_identifier": "Kullanıcı",
|
||||
"value": "Değer"
|
||||
"value": "Değer",
|
||||
"value_boolean": "Değer (Boolean)",
|
||||
"value_date": "Değer (Tarih)",
|
||||
"value_number": "Değer (Sayı)",
|
||||
"value_text": "Değer (Metin)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "您 未 认证 以 执行 该 操作。",
|
||||
"not_authorized": "未授权",
|
||||
"not_connected": "未连接",
|
||||
"not_set": "未设置",
|
||||
"note": "注释",
|
||||
"notifications": "通知",
|
||||
"number": "数字",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "查看哪些团队可以访问此工作区。"
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "添加反馈记录",
|
||||
"add_feedback_record_description": "手动创建反馈记录。",
|
||||
"add_feedback_source": "添加反馈来源",
|
||||
"add_source": "添加来源",
|
||||
"allowed_values": "允许的值:{values}",
|
||||
"api_ingestion": "API ingestion",
|
||||
"api_ingestion_manage_api_keys": "Manage API keys",
|
||||
"api_ingestion_settings_description": "Send feedback records using the Management API.",
|
||||
"auto_generated": "自动生成",
|
||||
"change_file": "更换文件",
|
||||
"click_load_sample_csv": "点击“加载示例 CSV”查看列",
|
||||
"click_to_upload": "点击上传",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "重复导入数据会产生重复记录。",
|
||||
"csv_inconsistent_columns": "第 {row} 行的列数不一致。所有行必须有相同的表头。",
|
||||
"csv_max_records": "最多允许 {max} 条记录。",
|
||||
"custom_source_type": "自定义源类型",
|
||||
"custom_source_type_placeholder": "输入自定义来源类型",
|
||||
"default_connector_name_csv": "CSV 导入",
|
||||
"default_connector_name_formbricks": "Formbricks 调查连接",
|
||||
"discard_feedback_record_changes_description": "如果关闭此抽屉,您的更改将会丢失。",
|
||||
"discard_feedback_record_changes_title": "放弃未保存的更改?",
|
||||
"drop_a_field_here": "将字段拖到这里",
|
||||
"drop_field_or": "拖放字段或",
|
||||
"edit_csv_mapping": "编辑 CSV 映射",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "枚举",
|
||||
"failed_to_load_feedback_records": "加载反馈记录失败",
|
||||
"feedback_date": "当前日期",
|
||||
"feedback_record_created_successfully": "反馈记录创建成功",
|
||||
"feedback_record_details": "反馈记录详情",
|
||||
"feedback_record_details_description": "查看并更新反馈记录字段。",
|
||||
"feedback_record_directory": "反馈记录目录",
|
||||
"feedback_record_fields": "反馈记录字段",
|
||||
"feedback_record_mcp": "Feedback Record MCP",
|
||||
"feedback_record_updated_successfully": "反馈记录更新成功",
|
||||
"feedback_record_value_required": "所选字段类型需要一个值",
|
||||
"feedback_records": "反馈记录",
|
||||
"feedback_records_refreshed": "反馈记录已刷新",
|
||||
"feedback_sources": "Feedback Sources",
|
||||
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
|
||||
"field_group_id": "字段组 ID",
|
||||
"field_group_label": "字段组标签",
|
||||
"field_id": "字段ID",
|
||||
"field_label": "字段标签",
|
||||
"field_type": "字段类型",
|
||||
"formbricks_surveys": "Formbricks Surveys",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Import historical responses",
|
||||
"import_historical_responses_description": "Import existing responses from this survey now.",
|
||||
"import_rows": "导入{count}行数据",
|
||||
"import_via_source_name": "通过“{sourceName}”导入",
|
||||
"importing_data": "正在导入数据…",
|
||||
"importing_historical_data": "正在导入历史数据…",
|
||||
"invalid_enum_values": "映射到 {field} 的列中存在无效值",
|
||||
"invalid_values_found": "发现:{values}(行:{rows}){extra}",
|
||||
"load_sample_csv": "加载示例 CSV",
|
||||
"manage_directories": "Manage directories",
|
||||
"manage_feedback_sources": "管理反馈来源",
|
||||
"metadata": "元数据",
|
||||
"metadata_key": "元数据键",
|
||||
"metadata_read_only_entries": "只读元数据值(非字符串)",
|
||||
"metadata_value": "元数据值",
|
||||
"missing_feedback_source_title": "Missing feedback source?",
|
||||
"no_feedback_record_directory_available": "此工作区未分配反馈记录目录。请先创建或分配一个。",
|
||||
"no_feedback_records": "暂无反馈记录。当你的连接器开始发送数据后,记录会显示在这里。",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "请选择一个调查以查看其问题",
|
||||
"select_a_value": "选择一个值...",
|
||||
"select_feedback_record_directory": "选择目录",
|
||||
"select_feedback_record_source_type": "选择来源类型",
|
||||
"select_questions": "选择问题",
|
||||
"select_source_type_description": "请选择你想要连接的反馈来源类型。",
|
||||
"select_survey": "选择调查",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
|
||||
"source_connect_formbricks_description": "连接来自你 Formbricks 调查的反馈",
|
||||
"source_fields": "来源字段",
|
||||
"source_id": "源ID",
|
||||
"source_name": "来源名称",
|
||||
"source_type": "来源类型",
|
||||
"source_type_cannot_be_changed": "来源类型无法更改",
|
||||
"sources": "来源",
|
||||
"status_error": "错误",
|
||||
"status_live_sync": "Live sync",
|
||||
"status_paused": "已暂停",
|
||||
"status_ready": "Ready",
|
||||
"submission_id": "提交ID",
|
||||
"survey_has_no_questions": "该调查没有任何问题",
|
||||
"topics_and_subtopics": "主题和子主题",
|
||||
"unify_feedback": "统一反馈",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "上传 CSV 文件以导入反馈数据。",
|
||||
"upload_csv_file": "上传 CSV 文件",
|
||||
"user_identifier": "用户",
|
||||
"value": "值"
|
||||
"value": "值",
|
||||
"value_boolean": "值(布尔值)",
|
||||
"value_date": "值(日期)",
|
||||
"value_number": "值(数量)",
|
||||
"value_text": "值(文本)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "客户努力评分",
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"not_authenticated": "您未經授權執行此操作。",
|
||||
"not_authorized": "未授權",
|
||||
"not_connected": "未連線",
|
||||
"not_set": "未設定",
|
||||
"note": "筆記",
|
||||
"notifications": "通知",
|
||||
"number": "數字",
|
||||
@@ -3618,12 +3619,15 @@
|
||||
"team_settings_description": "查看哪些團隊可以存取此工作區。"
|
||||
},
|
||||
"unify": {
|
||||
"add_feedback_record": "新增回饋記錄",
|
||||
"add_feedback_record_description": "手動建立回饋記錄。",
|
||||
"add_feedback_source": "新增回饋來源",
|
||||
"add_source": "新增來源",
|
||||
"allowed_values": "允許的值:{values}",
|
||||
"api_ingestion": "API ingestion",
|
||||
"api_ingestion_manage_api_keys": "Manage API keys",
|
||||
"api_ingestion_settings_description": "Send feedback records using the Management API.",
|
||||
"auto_generated": "自動生成",
|
||||
"change_file": "更換檔案",
|
||||
"click_load_sample_csv": "點擊「載入範例 CSV」以查看欄位",
|
||||
"click_to_upload": "點擊以上傳",
|
||||
@@ -3648,8 +3652,12 @@
|
||||
"csv_import_duplicate_warning": "匯入已經匯入過的資料,可能會產生重複紀錄。",
|
||||
"csv_inconsistent_columns": "第 {row} 列的欄位數不一致。所有列必須有相同的標題。",
|
||||
"csv_max_records": "最多允許 {max} 筆紀錄。",
|
||||
"custom_source_type": "自訂來源類型",
|
||||
"custom_source_type_placeholder": "輸入自訂來源類型",
|
||||
"default_connector_name_csv": "CSV 匯入",
|
||||
"default_connector_name_formbricks": "Formbricks 問卷連線",
|
||||
"discard_feedback_record_changes_description": "如果關閉此抽屜,您的變更將會遺失。",
|
||||
"discard_feedback_record_changes_title": "放棄未儲存的變更?",
|
||||
"drop_a_field_here": "請將欄位拖曳到這裡",
|
||||
"drop_field_or": "拖曳欄位或",
|
||||
"edit_csv_mapping": "編輯 CSV 對應",
|
||||
@@ -3659,15 +3667,23 @@
|
||||
"enum": "enum",
|
||||
"failed_to_load_feedback_records": "載入回饋紀錄失敗",
|
||||
"feedback_date": "目前日期",
|
||||
"feedback_record_created_successfully": "回饋記錄創建成功",
|
||||
"feedback_record_details": "反饋記錄詳情",
|
||||
"feedback_record_details_description": "查看並更新回饋記錄欄位。",
|
||||
"feedback_record_directory": "意見回饋記錄目錄",
|
||||
"feedback_record_fields": "回饋紀錄欄位",
|
||||
"feedback_record_mcp": "Feedback Record MCP",
|
||||
"feedback_record_updated_successfully": "回饋記錄更新成功",
|
||||
"feedback_record_value_required": "所選欄位類型需要一個值",
|
||||
"feedback_records": "回饋紀錄",
|
||||
"feedback_records_refreshed": "回饋紀錄已更新",
|
||||
"feedback_sources": "Feedback Sources",
|
||||
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
|
||||
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
|
||||
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
|
||||
"field_group_id": "字段組 ID",
|
||||
"field_group_label": "字段組標籤",
|
||||
"field_id": "欄位ID",
|
||||
"field_label": "欄位標籤",
|
||||
"field_type": "欄位類型",
|
||||
"formbricks_surveys": "Formbricks 問卷",
|
||||
@@ -3678,12 +3694,18 @@
|
||||
"import_historical_responses": "Import historical responses",
|
||||
"import_historical_responses_description": "Import existing responses from this survey now.",
|
||||
"import_rows": "匯入 {count} 筆資料",
|
||||
"import_via_source_name": "透過“{sourceName}”導入",
|
||||
"importing_data": "正在匯入資料…",
|
||||
"importing_historical_data": "正在匯入歷史資料…",
|
||||
"invalid_enum_values": "對應到 {field} 欄位的值無效",
|
||||
"invalid_values_found": "發現:{values}(列:{rows}){extra}",
|
||||
"load_sample_csv": "載入範例 CSV",
|
||||
"manage_directories": "Manage directories",
|
||||
"manage_feedback_sources": "管理回饋來源",
|
||||
"metadata": "元數據",
|
||||
"metadata_key": "元資料鍵",
|
||||
"metadata_read_only_entries": "唯讀元資料值(非字串)",
|
||||
"metadata_value": "元資料值",
|
||||
"missing_feedback_source_title": "Missing feedback source?",
|
||||
"no_feedback_record_directory_available": "此工作區尚未指派意見回饋記錄目錄。請先建立或指派一個目錄。",
|
||||
"no_feedback_records": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。",
|
||||
@@ -3700,6 +3722,7 @@
|
||||
"select_a_survey_to_see_questions": "請選擇問卷以查看其問題",
|
||||
"select_a_value": "請選擇一個值...",
|
||||
"select_feedback_record_directory": "選擇目錄",
|
||||
"select_feedback_record_source_type": "選擇來源類型",
|
||||
"select_questions": "選擇問題",
|
||||
"select_source_type_description": "請選擇你想要連接的回饋來源類型。",
|
||||
"select_survey": "選擇問卷",
|
||||
@@ -3714,14 +3737,15 @@
|
||||
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
|
||||
"source_connect_formbricks_description": "連接來自你 Formbricks 問卷的回饋",
|
||||
"source_fields": "來源欄位",
|
||||
"source_id": "來源ID",
|
||||
"source_name": "來源名稱",
|
||||
"source_type": "來源類型",
|
||||
"source_type_cannot_be_changed": "來源類型無法變更",
|
||||
"sources": "來源",
|
||||
"status_error": "錯誤",
|
||||
"status_live_sync": "Live sync",
|
||||
"status_paused": "已暫停",
|
||||
"status_ready": "Ready",
|
||||
"submission_id": "提交ID",
|
||||
"survey_has_no_questions": "此問卷沒有任何題目",
|
||||
"topics_and_subtopics": "主題與子主題",
|
||||
"unify_feedback": "整合回饋",
|
||||
@@ -3730,7 +3754,11 @@
|
||||
"upload_csv_data_description": "上傳 CSV 檔案以匯入回饋資料。",
|
||||
"upload_csv_file": "上傳 CSV 檔案",
|
||||
"user_identifier": "使用者",
|
||||
"value": "值"
|
||||
"value": "值",
|
||||
"value_boolean": "值(布林值)",
|
||||
"value_date": "值(日期)",
|
||||
"value_number": "值(數量)",
|
||||
"value_text": "值(文字)"
|
||||
},
|
||||
"xm-templates": {
|
||||
"ces": "CES",
|
||||
|
||||
@@ -3,7 +3,9 @@ export {
|
||||
createFeedbackRecord,
|
||||
createFeedbackRecordsBatch,
|
||||
listFeedbackRecords,
|
||||
type CreateFeedbackRecordResult,
|
||||
retrieveFeedbackRecord,
|
||||
updateFeedbackRecord,
|
||||
type HubFeedbackRecordResult,
|
||||
type ListFeedbackRecordsResult,
|
||||
} from "./service";
|
||||
export type {
|
||||
@@ -11,4 +13,5 @@ export type {
|
||||
FeedbackRecordData,
|
||||
FeedbackRecordListParams,
|
||||
FeedbackRecordListResponse,
|
||||
FeedbackRecordUpdateParams,
|
||||
} from "./types";
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { createFeedbackRecord, createFeedbackRecordsBatch, listFeedbackRecords } from "./service";
|
||||
import {
|
||||
createFeedbackRecord,
|
||||
createFeedbackRecordsBatch,
|
||||
listFeedbackRecords,
|
||||
retrieveFeedbackRecord,
|
||||
updateFeedbackRecord,
|
||||
} from "./service";
|
||||
import type { FeedbackRecordCreateParams } from "./types";
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
@@ -15,7 +21,7 @@ const { getHubClient } = await import("./hub-client");
|
||||
const sampleInput: FeedbackRecordCreateParams = {
|
||||
field_id: "el-1",
|
||||
field_type: "rating",
|
||||
source_type: "formbricks",
|
||||
source_type: "formbricks_survey",
|
||||
source_id: "survey-1",
|
||||
source_name: "Test Survey",
|
||||
field_label: "Question?",
|
||||
@@ -121,6 +127,62 @@ describe("hub service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("retrieveFeedbackRecord", () => {
|
||||
test("returns error when client is null", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue(null);
|
||||
const result = await retrieveFeedbackRecord("rec-1");
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error?.message).toContain("HUB_API_KEY");
|
||||
});
|
||||
|
||||
test("returns data on success", async () => {
|
||||
const record = { id: "rec-1", field_id: "f1" };
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { retrieve: vi.fn().mockResolvedValue(record) },
|
||||
} as any);
|
||||
const result = await retrieveFeedbackRecord("rec-1");
|
||||
expect(result.data).toEqual(record);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test("returns error on throw", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { retrieve: vi.fn().mockRejectedValue(new Error("Not found")) },
|
||||
} as any);
|
||||
const result = await retrieveFeedbackRecord("rec-1");
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toMatchObject({ message: "Not found" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateFeedbackRecord", () => {
|
||||
test("returns error when client is null", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue(null);
|
||||
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error?.message).toContain("HUB_API_KEY");
|
||||
});
|
||||
|
||||
test("returns data on success", async () => {
|
||||
const updated = { id: "rec-1", value_text: "new" };
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { update: vi.fn().mockResolvedValue(updated) },
|
||||
} as any);
|
||||
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
|
||||
expect(result.data).toEqual(updated);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test("returns error on throw", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue({
|
||||
feedbackRecords: { update: vi.fn().mockRejectedValue(new Error("Forbidden")) },
|
||||
} as any);
|
||||
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toMatchObject({ message: "Forbidden" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFeedbackRecordsBatch", () => {
|
||||
test("returns all errors when getHubClient returns null", async () => {
|
||||
vi.mocked(getHubClient).mockReturnValue(null);
|
||||
|
||||
@@ -7,11 +7,14 @@ import type {
|
||||
FeedbackRecordData,
|
||||
FeedbackRecordListParams,
|
||||
FeedbackRecordListResponse,
|
||||
FeedbackRecordUpdateParams,
|
||||
} from "./types";
|
||||
|
||||
export type CreateFeedbackRecordResult = {
|
||||
type HubError = { status: number; message: string; detail: string };
|
||||
|
||||
export type HubFeedbackRecordResult = {
|
||||
data: FeedbackRecordData | null;
|
||||
error: { status: number; message: string; detail: string } | null;
|
||||
error: HubError | null;
|
||||
};
|
||||
|
||||
const NO_CONFIG_ERROR = {
|
||||
@@ -20,7 +23,7 @@ const NO_CONFIG_ERROR = {
|
||||
detail: "HUB_API_KEY is not set; Hub integration is disabled.",
|
||||
} as const;
|
||||
|
||||
const createResultFromError = (err: unknown): CreateFeedbackRecordResult => {
|
||||
const createResultFromError = (err: unknown): HubFeedbackRecordResult => {
|
||||
const status = err instanceof FormbricksHub.APIError ? err.status : 0;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { data: null, error: { status, message, detail: message } };
|
||||
@@ -32,7 +35,7 @@ const createResultFromError = (err: unknown): CreateFeedbackRecordResult => {
|
||||
*/
|
||||
export const createFeedbackRecord = async (
|
||||
input: FeedbackRecordCreateParams
|
||||
): Promise<CreateFeedbackRecordResult> => {
|
||||
): Promise<HubFeedbackRecordResult> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return { data: null, error: { ...NO_CONFIG_ERROR } };
|
||||
@@ -46,9 +49,48 @@ export const createFeedbackRecord = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a single feedback record from the Hub by id.
|
||||
*/
|
||||
export const retrieveFeedbackRecord = async (id: string): Promise<HubFeedbackRecordResult> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return { data: null, error: { ...NO_CONFIG_ERROR } };
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.feedbackRecords.retrieve(id);
|
||||
return { data, error: null };
|
||||
} catch (err) {
|
||||
logger.warn({ err, id }, "Hub: retrieveFeedbackRecord failed");
|
||||
return createResultFromError(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a single feedback record in the Hub by id.
|
||||
*/
|
||||
export const updateFeedbackRecord = async (
|
||||
id: string,
|
||||
input: FeedbackRecordUpdateParams
|
||||
): Promise<HubFeedbackRecordResult> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return { data: null, error: { ...NO_CONFIG_ERROR } };
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.feedbackRecords.update(id, input);
|
||||
return { data, error: null };
|
||||
} catch (err) {
|
||||
logger.warn({ err, id }, "Hub: updateFeedbackRecord failed");
|
||||
return createResultFromError(err);
|
||||
}
|
||||
};
|
||||
|
||||
export type ListFeedbackRecordsResult = {
|
||||
data: FeedbackRecordListResponse | null;
|
||||
error: { status: number; message: string; detail: string } | null;
|
||||
error: HubError | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,7 +120,7 @@ export const listFeedbackRecords = async (
|
||||
*/
|
||||
export const createFeedbackRecordsBatch = async (
|
||||
inputs: FeedbackRecordCreateParams[]
|
||||
): Promise<{ results: CreateFeedbackRecordResult[] }> => {
|
||||
): Promise<{ results: HubFeedbackRecordResult[] }> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return {
|
||||
@@ -90,7 +132,7 @@ export const createFeedbackRecordsBatch = async (
|
||||
inputs.map(async (input) => {
|
||||
try {
|
||||
const data = await client.feedbackRecords.create(input);
|
||||
return { data, error: null as CreateFeedbackRecordResult["error"] };
|
||||
return { data, error: null as HubFeedbackRecordResult["error"] };
|
||||
} catch (err) {
|
||||
logger.warn({ err, fieldId: input.field_id }, "Hub: createFeedbackRecord failed");
|
||||
return createResultFromError(err);
|
||||
|
||||
@@ -4,3 +4,4 @@ export type FeedbackRecordCreateParams = FormbricksHub.FeedbackRecordCreateParam
|
||||
export type FeedbackRecordData = FormbricksHub.FeedbackRecordData;
|
||||
export type FeedbackRecordListParams = FormbricksHub.FeedbackRecordListParams;
|
||||
export type FeedbackRecordListResponse = FormbricksHub.FeedbackRecordListResponse;
|
||||
export type FeedbackRecordUpdateParams = FormbricksHub.FeedbackRecordUpdateParams;
|
||||
|
||||
Reference in New Issue
Block a user