mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 19:14:53 -05:00
Merge branch 'main' of https://github.com/formbricks/formbricks into chore-update-hun-20260203
This commit is contained in:
@@ -316,6 +316,14 @@ export const generateResponseTableColumns = (
|
||||
},
|
||||
};
|
||||
|
||||
const responseIdColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "responseId",
|
||||
header: () => <div className="gap-x-1.5">{t("common.response_id")}</div>,
|
||||
cell: ({ row }) => {
|
||||
return <IdBadge id={row.original.responseId} />;
|
||||
},
|
||||
};
|
||||
|
||||
const quotasColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "quota",
|
||||
header: t("common.quota"),
|
||||
@@ -376,24 +384,24 @@ export const generateResponseTableColumns = (
|
||||
|
||||
const hiddenFieldColumns: ColumnDef<TResponseTableData>[] = survey.hiddenFields.fieldIds
|
||||
? survey.hiddenFields.fieldIds.map((hiddenFieldId) => {
|
||||
return {
|
||||
accessorKey: "HIDDEN_FIELD_" + hiddenFieldId,
|
||||
header: () => (
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="truncate">{hiddenFieldId}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const hiddenFieldResponse = row.original.responseData[hiddenFieldId];
|
||||
if (typeof hiddenFieldResponse === "string") {
|
||||
return <div className="text-slate-900">{hiddenFieldResponse}</div>;
|
||||
}
|
||||
},
|
||||
};
|
||||
})
|
||||
return {
|
||||
accessorKey: "HIDDEN_FIELD_" + hiddenFieldId,
|
||||
header: () => (
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="truncate">{hiddenFieldId}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const hiddenFieldResponse = row.original.responseData[hiddenFieldId];
|
||||
if (typeof hiddenFieldResponse === "string") {
|
||||
return <div className="text-slate-900">{hiddenFieldResponse}</div>;
|
||||
}
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const metadataColumns = getMetadataColumnsData(t);
|
||||
@@ -414,6 +422,7 @@ export const generateResponseTableColumns = (
|
||||
const baseColumns = [
|
||||
personColumn,
|
||||
singleUseIdColumn,
|
||||
responseIdColumn,
|
||||
dateColumn,
|
||||
...(showQuotasColumn ? [quotasColumn] : []),
|
||||
statusColumn,
|
||||
|
||||
@@ -323,6 +323,7 @@ checksums:
|
||||
common/request_trial_license: 560df1240ef621f7c60d3f7d65422ccd
|
||||
common/reset_to_default: 68ee98b46677392f44b505b268053b26
|
||||
common/response: c7a9d88269d8ff117abcbc0d97f88b2c
|
||||
common/response_id: 73375099cc976dc7203b8e27f5f709e0
|
||||
common/responses: 14bb6c69f906d7bbd1359f7ef1bb3c28
|
||||
common/restart: bab6232e89f24e3129f8e48268739d5b
|
||||
common/role: 53743bbb6ca938f5b893552e839d067f
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Testlizenz anfordern",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"response": "Antwort",
|
||||
"response_id": "Antwort-ID",
|
||||
"responses": "Antworten",
|
||||
"restart": "Neustart",
|
||||
"role": "Rolle",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Request trial license",
|
||||
"reset_to_default": "Reset to default",
|
||||
"response": "Response",
|
||||
"response_id": "Response ID",
|
||||
"responses": "Responses",
|
||||
"restart": "Restart",
|
||||
"role": "Role",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Solicitar licencia de prueba",
|
||||
"reset_to_default": "Restablecer a valores predeterminados",
|
||||
"response": "Respuesta",
|
||||
"response_id": "ID de respuesta",
|
||||
"responses": "Respuestas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Rol",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Demander une licence d'essai",
|
||||
"reset_to_default": "Réinitialiser par défaut",
|
||||
"response": "Réponse",
|
||||
"response_id": "ID de réponse",
|
||||
"responses": "Réponses",
|
||||
"restart": "Recommencer",
|
||||
"role": "Rôle",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "トライアルライセンスをリクエスト",
|
||||
"reset_to_default": "デフォルトにリセット",
|
||||
"response": "回答",
|
||||
"response_id": "回答ID",
|
||||
"responses": "回答",
|
||||
"restart": "再開",
|
||||
"role": "役割",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Proeflicentie aanvragen",
|
||||
"reset_to_default": "Resetten naar standaard",
|
||||
"response": "Antwoord",
|
||||
"response_id": "Antwoord-ID",
|
||||
"responses": "Reacties",
|
||||
"restart": "Opnieuw opstarten",
|
||||
"role": "Rol",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Pedir licença de teste",
|
||||
"reset_to_default": "Restaurar para o padrão",
|
||||
"response": "Resposta",
|
||||
"response_id": "ID da resposta",
|
||||
"responses": "Respostas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Rolê",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Solicitar licença de teste",
|
||||
"reset_to_default": "Repor para o padrão",
|
||||
"response": "Resposta",
|
||||
"response_id": "ID de resposta",
|
||||
"responses": "Respostas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Função",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Solicitați o licență de încercare",
|
||||
"reset_to_default": "Revino la implicit",
|
||||
"response": "Răspuns",
|
||||
"response_id": "ID răspuns",
|
||||
"responses": "Răspunsuri",
|
||||
"restart": "Repornește",
|
||||
"role": "Rolul",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Запросить пробную лицензию",
|
||||
"reset_to_default": "Сбросить по умолчанию",
|
||||
"response": "Ответ",
|
||||
"response_id": "ID ответа",
|
||||
"responses": "Ответы",
|
||||
"restart": "Перезапустить",
|
||||
"role": "Роль",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "Begär provlicens",
|
||||
"reset_to_default": "Återställ till standard",
|
||||
"response": "Svar",
|
||||
"response_id": "Svar-ID",
|
||||
"responses": "Svar",
|
||||
"restart": "Starta om",
|
||||
"role": "Roll",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "申请试用许可证",
|
||||
"reset_to_default": "重置为 默认",
|
||||
"response": "响应",
|
||||
"response_id": "响应 ID",
|
||||
"responses": "反馈",
|
||||
"restart": "重新启动",
|
||||
"role": "角色",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"request_trial_license": "請求試用授權",
|
||||
"reset_to_default": "重設為預設值",
|
||||
"response": "回應",
|
||||
"response_id": "回應 ID",
|
||||
"responses": "回應",
|
||||
"restart": "重新開始",
|
||||
"role": "角色",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useTransition } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TI18nString } from "@formbricks/types/i18n";
|
||||
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
@@ -74,6 +74,8 @@ export function LocalizedEditor({
|
||||
[id, isInvalid, localSurvey.languages, value]
|
||||
);
|
||||
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Editor
|
||||
@@ -109,44 +111,45 @@ export function LocalizedEditor({
|
||||
sanitizedContent = v.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
|
||||
}
|
||||
|
||||
// Check if the elements still exists before updating
|
||||
const currentElement = elements[elementIdx];
|
||||
|
||||
// if this is a card, we wanna check if the card exists in the localSurvey
|
||||
if (isCard) {
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
startTransition(() => {
|
||||
// if this is a card, we wanna check if the card exists in the localSurvey
|
||||
if (isCard) {
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
|
||||
// For ending cards, check if the field exists before updating
|
||||
if (isEndingCard) {
|
||||
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
// If the field doesn't exist on the ending card, don't create it
|
||||
if (!ending || ending[id] === undefined) {
|
||||
// For ending cards, check if the field exists before updating
|
||||
if (isEndingCard) {
|
||||
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
// If the field doesn't exist on the ending card, don't create it
|
||||
if (!ending || ending[id] === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For welcome cards, check if it exists
|
||||
if (isWelcomeCard && !localSurvey.welcomeCard) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For welcome cards, check if it exists
|
||||
if (isWelcomeCard && !localSurvey.welcomeCard) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement({ [id]: translatedContent });
|
||||
return;
|
||||
}
|
||||
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement({ [id]: translatedContent });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the field exists on the element (not just if it's not undefined)
|
||||
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement(elementIdx, { [id]: translatedContent });
|
||||
}
|
||||
// Check if the field exists on the element (not just if it's not undefined)
|
||||
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement(elementIdx, { [id]: translatedContent });
|
||||
}
|
||||
});
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
elementId={elementId}
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from "@/modules/survey/editor/lib/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { isEndingCardValid, isWelcomeCardValid, validateSurveyElementsInBatch } from "../lib/validation";
|
||||
import { isEndingCardValid, isWelcomeCardValid, validateElement } from "../lib/validation";
|
||||
|
||||
interface ElementsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -211,35 +211,6 @@ export const ElementsView = ({
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!invalidElements) return;
|
||||
let updatedInvalidElements: string[] = [...invalidElements];
|
||||
|
||||
// Check welcome card
|
||||
if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) {
|
||||
if (!updatedInvalidElements.includes("start")) {
|
||||
updatedInvalidElements = [...updatedInvalidElements, "start"];
|
||||
}
|
||||
} else {
|
||||
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== "start");
|
||||
}
|
||||
|
||||
// Check thank you card
|
||||
localSurvey.endings.forEach((ending) => {
|
||||
if (!isEndingCardValid(ending, surveyLanguages)) {
|
||||
if (!updatedInvalidElements.includes(ending.id)) {
|
||||
updatedInvalidElements = [...updatedInvalidElements, ending.id];
|
||||
}
|
||||
} else {
|
||||
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== ending.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
|
||||
setInvalidElements(updatedInvalidElements);
|
||||
}
|
||||
}, [localSurvey.welcomeCard, localSurvey.endings, surveyLanguages, invalidElements, setInvalidElements]);
|
||||
|
||||
const updateElement = (elementIdx: number, updatedAttributes: any) => {
|
||||
// Get element ID from current elements array (for validation)
|
||||
const element = elements[elementIdx];
|
||||
@@ -250,7 +221,6 @@ export const ElementsView = ({
|
||||
|
||||
// Track side effects that need to happen after state update
|
||||
let newActiveElementId: string | null = null;
|
||||
let invalidElementsUpdate: string[] | null = null;
|
||||
|
||||
// Use functional update to ensure we work with the latest state
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
@@ -296,13 +266,6 @@ export const ElementsView = ({
|
||||
const initialElementId = elementId;
|
||||
updatedSurvey = handleElementLogicChange(updatedSurvey, initialElementId, elementLevelAttributes.id);
|
||||
|
||||
// Track side effects to apply after state update
|
||||
if (invalidElements?.includes(initialElementId)) {
|
||||
invalidElementsUpdate = invalidElements.map((id) =>
|
||||
id === initialElementId ? elementLevelAttributes.id : id
|
||||
);
|
||||
}
|
||||
|
||||
// Track new active element ID
|
||||
newActiveElementId = elementLevelAttributes.id;
|
||||
|
||||
@@ -344,9 +307,6 @@ export const ElementsView = ({
|
||||
});
|
||||
|
||||
// Apply side effects after state update is queued
|
||||
if (invalidElementsUpdate) {
|
||||
setInvalidElements(invalidElementsUpdate);
|
||||
}
|
||||
if (newActiveElementId) {
|
||||
setActiveElementId(newActiveElementId);
|
||||
}
|
||||
@@ -764,23 +724,67 @@ export const ElementsView = ({
|
||||
setLocalSurvey(result.data);
|
||||
};
|
||||
|
||||
//useEffect to validate survey when changes are made to languages
|
||||
useEffect(() => {
|
||||
if (!invalidElements) return;
|
||||
let updatedInvalidElements: string[] = invalidElements;
|
||||
// Validate each element
|
||||
elements.forEach((element) => {
|
||||
updatedInvalidElements = validateSurveyElementsInBatch(
|
||||
element,
|
||||
updatedInvalidElements,
|
||||
surveyLanguages
|
||||
);
|
||||
});
|
||||
// Validate survey when changes are made to languages or elements
|
||||
// using set for O(1) lookup
|
||||
useEffect(
|
||||
() => {
|
||||
if (!invalidElements) return;
|
||||
|
||||
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
|
||||
setInvalidElements(updatedInvalidElements);
|
||||
}
|
||||
}, [elements, surveyLanguages, invalidElements, setInvalidElements]);
|
||||
const currentInvalidSet = new Set(invalidElements);
|
||||
let hasChanges = false;
|
||||
|
||||
// Validate each element
|
||||
elements.forEach((element) => {
|
||||
const isValid = validateElement(element, surveyLanguages);
|
||||
if (isValid) {
|
||||
if (currentInvalidSet.has(element.id)) {
|
||||
currentInvalidSet.delete(element.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (!currentInvalidSet.has(element.id)) {
|
||||
currentInvalidSet.add(element.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check welcome card
|
||||
if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) {
|
||||
if (!currentInvalidSet.has("start")) {
|
||||
currentInvalidSet.add("start");
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (currentInvalidSet.has("start")) {
|
||||
currentInvalidSet.delete("start");
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// Check thank you card
|
||||
localSurvey.endings.forEach((ending) => {
|
||||
if (!isEndingCardValid(ending, surveyLanguages)) {
|
||||
if (!currentInvalidSet.has(ending.id)) {
|
||||
currentInvalidSet.add(ending.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (currentInvalidSet.has(ending.id)) {
|
||||
currentInvalidSet.delete(ending.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
setInvalidElements(Array.from(currentInvalidSet));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
elements,
|
||||
surveyLanguages,
|
||||
invalidElements,
|
||||
setInvalidElements,
|
||||
localSurvey.welcomeCard,
|
||||
localSurvey.endings,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
@@ -791,7 +795,7 @@ export const ElementsView = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeElementId, setActiveElementId]);
|
||||
}, [activeElementId, setActiveElementId, localSurvey, selectedLanguageCode]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
|
||||
@@ -86,6 +86,7 @@ export const SurveyEditor = ({
|
||||
const [activeElementId, setActiveElementId] = useState<string | null>(null);
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(() => structuredClone(survey));
|
||||
const [invalidElements, setInvalidElements] = useState<string[] | null>(null);
|
||||
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [localProject, setLocalProject] = useState<Project>(project);
|
||||
|
||||
@@ -3,11 +3,11 @@ x-environment: &environment
|
||||
######################################################## REQUIRED ########################################################
|
||||
|
||||
# The url of your Formbricks instance used in the admin panel
|
||||
# Set this to your public-facing URL, e.g., https://example.com
|
||||
WEBAPP_URL:
|
||||
# Set this to your public-facing URL, e.g., example http://localhost:3000 or https://example.com
|
||||
WEBAPP_URL:
|
||||
|
||||
# Required for next-auth. Should be the same as WEBAPP_URL
|
||||
NEXTAUTH_URL:
|
||||
NEXTAUTH_URL:
|
||||
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
@@ -15,15 +15,15 @@ x-environment: &environment
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -hex 32` to generate one
|
||||
NEXTAUTH_SECRET:
|
||||
NEXTAUTH_SECRET:
|
||||
|
||||
# Encryption Key is used for 2FA & Single use URLs for Link Surveys
|
||||
# You can use: $(openssl rand -hex 32) to generate one
|
||||
ENCRYPTION_KEY:
|
||||
ENCRYPTION_KEY:
|
||||
|
||||
# API Secret for running cron jobs.
|
||||
# You can use: $(openssl rand -hex 32) to generate a secure one
|
||||
CRON_SECRET:
|
||||
CRON_SECRET:
|
||||
|
||||
# Redis URL for caching, rate limiting, and audit logging
|
||||
# To use external Redis/Valkey: remove the redis service below and update this URL
|
||||
@@ -201,9 +201,13 @@ services:
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
# Postgres DB Super User Password
|
||||
# Replace the below with your own secure password & Make sure the password matches the password field in DATABASE_URL above
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
|
||||
# Redis/Valkey service for caching, rate limiting, and audit logging
|
||||
# Remove this service if you want to use an external Redis/Valkey instance
|
||||
@@ -215,13 +219,21 @@ services:
|
||||
- redis:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "valkey-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
formbricks:
|
||||
restart: always
|
||||
image: ghcr.io/formbricks/formbricks:latest
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user