mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 19:29:40 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68d0af027a | |||
| eec02496ca | |||
| 7412b32526 | |||
| 193346a70d | |||
| a1d4754b04 | |||
| f4b918a4b6 | |||
| fb9a0b197a | |||
| 95b6c16dd1 | |||
| cfdf09650f | |||
| 4c94fc25ae | |||
| ccf501d925 | |||
| 04dfbe0777 | |||
| cbf255ab0d | |||
| 942366956c | |||
| a6ee796cef | |||
| a535529bd3 |
@@ -51,6 +51,22 @@ export const POST = async (request: Request) => {
|
||||
throw new ResourceNotFoundError("Organization", "Organization not found");
|
||||
}
|
||||
|
||||
// Fetch survey for webhook payload
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) {
|
||||
logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`);
|
||||
|
||||
return responses.notFoundResponse("Survey", surveyId, true);
|
||||
}
|
||||
|
||||
if (survey.environmentId !== environmentId) {
|
||||
logger.error(
|
||||
{ url: request.url, surveyId, environmentId, surveyEnvironmentId: survey.environmentId },
|
||||
`Survey ${surveyId} does not belong to environment ${environmentId}`
|
||||
);
|
||||
return responses.badRequestResponse("Survey not found in this environment");
|
||||
}
|
||||
|
||||
// Fetch webhooks
|
||||
const getWebhooksForPipeline = async (environmentId: string, event: PipelineTriggers, surveyId: string) => {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
@@ -81,7 +97,16 @@ export const POST = async (request: Request) => {
|
||||
body: JSON.stringify({
|
||||
webhookId: webhook.id,
|
||||
event,
|
||||
data: response,
|
||||
data: {
|
||||
...response,
|
||||
survey: {
|
||||
title: survey.name,
|
||||
type: survey.type,
|
||||
status: survey.status,
|
||||
createdAt: survey.createdAt,
|
||||
updatedAt: survey.updatedAt,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch((error) => {
|
||||
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
|
||||
@@ -89,18 +114,12 @@ export const POST = async (request: Request) => {
|
||||
);
|
||||
|
||||
if (event === "responseFinished") {
|
||||
// Fetch integrations, survey, and responseCount in parallel
|
||||
const [integrations, survey, responseCount] = await Promise.all([
|
||||
// Fetch integrations and responseCount in parallel
|
||||
const [integrations, responseCount] = await Promise.all([
|
||||
getIntegrations(environmentId),
|
||||
getSurvey(surveyId),
|
||||
getResponseCountBySurveyId(surveyId),
|
||||
]);
|
||||
|
||||
if (!survey) {
|
||||
logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`);
|
||||
return new Response("Survey not found", { status: 404 });
|
||||
}
|
||||
|
||||
if (integrations.length > 0) {
|
||||
await handleIntegrations(integrations, inputValidation.data, survey);
|
||||
}
|
||||
|
||||
+7
-4
@@ -395,6 +395,7 @@ checksums:
|
||||
common/updated: 8aa8ff2dc2977ca4b269e80a513100b4
|
||||
common/updated_at: 8fdb85248e591254973403755dcc3724
|
||||
common/upload: 4a6c84aa16db0f4e5697f49b45257bc7
|
||||
common/upload_failed: d4dd7b6ee4c1572e4136659f74d9632b
|
||||
common/upload_input_description: 64f59bc339568d52b8464b82546b70ea
|
||||
common/url: ca97457614226960d41dd18c3c29c86b
|
||||
common/user: 61073457a5c3901084b557d065f876be
|
||||
@@ -1170,8 +1171,7 @@ checksums:
|
||||
environments/surveys/edit/automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds: 1be3819ffa1db67385357ae933d69a7b
|
||||
environments/surveys/edit/automatically_mark_the_survey_as_complete_after: c6ede2a5515a4ca72b36aec2583f43aa
|
||||
environments/surveys/edit/back_button_label: 25af945e77336724b5276de291cc92d9
|
||||
environments/surveys/edit/background_styling: 4e1e6fd2ec767bbff8767f6c0f68a731
|
||||
environments/surveys/edit/block_deleted: c682259eb138ad84f8b4441abfd9b572
|
||||
environments/surveys/edit/background_styling: eb4a06cf54a7271b493fab625d930570
|
||||
environments/surveys/edit/block_duplicated: dc9e9fab2b1cd91f6c265324b34c6376
|
||||
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
||||
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
||||
@@ -1187,7 +1187,7 @@ checksums:
|
||||
environments/surveys/edit/card_arrangement_for_survey_type_derived: c06b9aaebcc11bc16e57a445b62361fc
|
||||
environments/surveys/edit/card_background_color: acd5d023e1d1a4471b053dce504c7a83
|
||||
environments/surveys/edit/card_border_color: 8d7c7f4cbd99f154ce892dfa258eb504
|
||||
environments/surveys/edit/card_styling: 01e88d58219539fb831e79f0bb3ce88e
|
||||
environments/surveys/edit/card_styling: 47137a7e809b060ca94418202a8fd3c5
|
||||
environments/surveys/edit/casual: 6534fe68718fade470a9031f7390409e
|
||||
environments/surveys/edit/caution_edit_duplicate: ee93bccb34fcd707e1ef4735f1c2fc31
|
||||
environments/surveys/edit/caution_edit_published_survey: faf7fc57c776f2a9104d143e20044486
|
||||
@@ -1243,6 +1243,7 @@ checksums:
|
||||
environments/surveys/edit/css_selector: 615e9f1b74622df29de28a5b5614c6fe
|
||||
environments/surveys/edit/cta_button_label: ec070ffba38eae24751bb3a4c1e14c81
|
||||
environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429
|
||||
environments/surveys/edit/customize_survey_logo: 7f7e26026c88a727228f2d7a00d914e2
|
||||
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
|
||||
environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a
|
||||
environments/surveys/edit/days_before_showing_this_survey_again: 354fb28c5ff076f022d82a20c749ee46
|
||||
@@ -1343,9 +1344,9 @@ checksums:
|
||||
environments/surveys/edit/hide_back_button_description: caaa30cf43c5611577933a1c9f44b9ee
|
||||
environments/surveys/edit/hide_block_settings: c24c3d3892c251792e297cdc036d2fde
|
||||
environments/surveys/edit/hide_logo: eef4de2e3fffe8cbe32bff4f6f7250d8
|
||||
environments/surveys/edit/hide_logo_from_survey: 9d44321539cc2b397376a35bb8b3d1cd
|
||||
environments/surveys/edit/hide_progress_bar: 7eefe7db6a051105bded521d94204933
|
||||
environments/surveys/edit/hide_question_settings: 99127cd016db2f7fc80333b36473c0ef
|
||||
environments/surveys/edit/hide_the_logo_in_this_specific_survey: 29d4c6c714886e57bc29ad292d0f5a00
|
||||
environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62
|
||||
environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e
|
||||
environments/surveys/edit/if_you_need_more_please: a7d208c283caf6b93800b809fca80768
|
||||
@@ -1392,6 +1393,7 @@ checksums:
|
||||
environments/surveys/edit/load_segment: 5341d3de37ff10f7526152e38e25e3c5
|
||||
environments/surveys/edit/logic_error_warning: 542fbb918ffdb29e6f9a4a6196ffb558
|
||||
environments/surveys/edit/logic_error_warning_text: f2afad8852a95ed169a39959efbf592c
|
||||
environments/surveys/edit/logo_settings: 9f54ca6684e989cc38bf177425b6d366
|
||||
environments/surveys/edit/long_answer: 3a97f8d2e90aba6e679917a0c5670c53
|
||||
environments/surveys/edit/long_answer_toggle_description: 86bcdfeb74d9825c2f2d5a215e92d111
|
||||
environments/surveys/edit/lower_label: 45985bca022d4370bd6e013af75d5160
|
||||
@@ -1424,6 +1426,7 @@ checksums:
|
||||
environments/surveys/edit/overwrite_global_waiting_time: 7bc23bd502b6bd048356b67acd956d9d
|
||||
environments/surveys/edit/overwrite_global_waiting_time_description: 795cf6e93d4c01d2e43aa0ebab601c6e
|
||||
environments/surveys/edit/overwrite_placement: d7278be243e52c5091974e0fc4a7c342
|
||||
environments/surveys/edit/overwrite_survey_logo: a89cb566dfcc1559446abd8b830c84ed
|
||||
environments/surveys/edit/overwrite_the_global_placement_of_the_survey: 874075712254b1ce92e099d89f675a48
|
||||
environments/surveys/edit/pick_a_background_from_our_library_or_upload_your_own: b83bcbdc8131fc9524d272ff5dede754
|
||||
environments/surveys/edit/picture_idx: 55e053ad1ade5d17c582406706036028
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Aktualisiert",
|
||||
"updated_at": "Aktualisiert am",
|
||||
"upload": "Hochladen",
|
||||
"upload_failed": "Upload fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"upload_input_description": "Klicke oder ziehe, um Dateien hochzuladen.",
|
||||
"url": "URL",
|
||||
"user": "Benutzer",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Umfrage automatisch als abgeschlossen markieren nach",
|
||||
"back_button_label": "Zurück\"- Button ",
|
||||
"background_styling": "Hintergründe",
|
||||
"background_styling": "Hintergrundgestaltung",
|
||||
"block_duplicated": "Block dupliziert.",
|
||||
"bold": "Fett",
|
||||
"brand_color": "Markenfarbe",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Kartenanordnung für {surveyTypeDerived} Umfragen",
|
||||
"card_background_color": "Hintergrundfarbe der Karte",
|
||||
"card_border_color": "Farbe des Kartenrandes",
|
||||
"card_styling": "Kartenstil",
|
||||
"card_styling": "Kartengestaltung",
|
||||
"casual": "Lässig",
|
||||
"caution_edit_duplicate": "Duplizieren & bearbeiten",
|
||||
"caution_edit_published_survey": "Eine veröffentlichte Umfrage bearbeiten?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS-Selektor",
|
||||
"cta_button_label": "\"CTA\"-Schaltflächen-Beschriftung",
|
||||
"custom_hostname": "Benutzerdefinierter Hostname",
|
||||
"customize_survey_logo": "Umfragelogo anpassen",
|
||||
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
|
||||
"date_format": "Datumsformat",
|
||||
"days_before_showing_this_survey_again": "Tage nachdem eine beliebige Umfrage angezeigt wurde, bevor diese Umfrage erscheinen kann.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Den Zurück-Button in der Umfrage nicht anzeigen",
|
||||
"hide_block_settings": "Block-Einstellungen ausblenden",
|
||||
"hide_logo": "Logo verstecken",
|
||||
"hide_logo_from_survey": "Logo in dieser Umfrage ausblenden",
|
||||
"hide_progress_bar": "Fortschrittsbalken ausblenden",
|
||||
"hide_question_settings": "Frageeinstellungen ausblenden",
|
||||
"hide_the_logo_in_this_specific_survey": "Logo in dieser speziellen Umfrage verstecken",
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
|
||||
"if_you_need_more_please": "Wenn Du mehr brauchst, bitte",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Segment laden",
|
||||
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
|
||||
"logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage",
|
||||
"logo_settings": "Logo-Einstellungen",
|
||||
"long_answer": "Lange Antwort",
|
||||
"long_answer_toggle_description": "Ermöglichen Sie den Befragten, längere Antworten über mehrere Zeilen zu schreiben.",
|
||||
"lower_label": "Unteres Label",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Benutzerdefinierte Wartezeit festlegen",
|
||||
"overwrite_global_waiting_time_description": "Die Projektkonfiguration nur für diese Umfrage überschreiben.",
|
||||
"overwrite_placement": "Platzierung überschreiben",
|
||||
"overwrite_survey_logo": "Benutzerdefiniertes Umfragelogo festlegen",
|
||||
"overwrite_the_global_placement_of_the_survey": "Platzierung für diese Umfrage überschreiben",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Wähle einen Hintergrund aus oder lade deinen eigenen hoch.",
|
||||
"picture_idx": "Bild {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Updated",
|
||||
"updated_at": "Updated at",
|
||||
"upload": "Upload",
|
||||
"upload_failed": "Upload failed. Please try again.",
|
||||
"upload_input_description": "Click or drag to upload files.",
|
||||
"url": "URL",
|
||||
"user": "User",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Automatically mark the survey as complete after",
|
||||
"back_button_label": "\"Back\" Button Label",
|
||||
"background_styling": "Background Styling",
|
||||
"background_styling": "Background styling",
|
||||
"block_duplicated": "Block duplicated.",
|
||||
"bold": "Bold",
|
||||
"brand_color": "Brand color",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Card Arrangement for {surveyTypeDerived} Surveys",
|
||||
"card_background_color": "Card background color",
|
||||
"card_border_color": "Card border color",
|
||||
"card_styling": "Card Styling",
|
||||
"card_styling": "Card styling",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicate & edit",
|
||||
"caution_edit_published_survey": "Edit a published survey?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS Selector",
|
||||
"cta_button_label": "\"CTA\" button label",
|
||||
"custom_hostname": "Custom hostname",
|
||||
"customize_survey_logo": "Customize the survey logo",
|
||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||
"date_format": "Date format",
|
||||
"days_before_showing_this_survey_again": "days after any survey is shown before this survey can appear.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Do not display the back button in the survey",
|
||||
"hide_block_settings": "Hide Block settings",
|
||||
"hide_logo": "Hide logo",
|
||||
"hide_logo_from_survey": "Hide logo from this survey",
|
||||
"hide_progress_bar": "Hide progress bar",
|
||||
"hide_question_settings": "Hide Question settings",
|
||||
"hide_the_logo_in_this_specific_survey": "Hide the logo in this specific survey",
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys",
|
||||
"if_you_need_more_please": "If you need more, please",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Load segment",
|
||||
"logic_error_warning": "Changing will cause logic errors",
|
||||
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
||||
"logo_settings": "Logo settings",
|
||||
"long_answer": "Long answer",
|
||||
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
||||
"lower_label": "Lower Label",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Set custom waiting time",
|
||||
"overwrite_global_waiting_time_description": "Override the project configuration for this survey only.",
|
||||
"overwrite_placement": "Overwrite placement",
|
||||
"overwrite_survey_logo": "Set custom survey logo",
|
||||
"overwrite_the_global_placement_of_the_survey": "Overwrite the global placement of the survey",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Pick a background from our library or upload your own.",
|
||||
"picture_idx": "Picture {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Actualizado",
|
||||
"updated_at": "Actualizado el",
|
||||
"upload": "Subir",
|
||||
"upload_failed": "La subida ha fallado. Por favor, inténtalo de nuevo.",
|
||||
"upload_input_description": "Haz clic o arrastra para subir archivos.",
|
||||
"url": "URL",
|
||||
"user": "Usuario",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Cerrar automáticamente la encuesta si el usuario no responde después de cierto número de segundos.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automáticamente la encuesta como completa después de",
|
||||
"back_button_label": "Etiqueta del botón \"Atrás\"",
|
||||
"background_styling": "Estilo de fondo",
|
||||
"background_styling": "Estilo del fondo",
|
||||
"block_duplicated": "Bloque duplicado.",
|
||||
"bold": "Negrita",
|
||||
"brand_color": "Color de marca",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Disposición de tarjetas para encuestas de tipo {surveyTypeDerived}",
|
||||
"card_background_color": "Color de fondo de la tarjeta",
|
||||
"card_border_color": "Color del borde de la tarjeta",
|
||||
"card_styling": "Estilo de tarjeta",
|
||||
"card_styling": "Estilo de la tarjeta",
|
||||
"casual": "Informal",
|
||||
"caution_edit_duplicate": "Duplicar y editar",
|
||||
"caution_edit_published_survey": "¿Editar una encuesta publicada?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "Selector CSS",
|
||||
"cta_button_label": "Etiqueta del botón \"CTA\"",
|
||||
"custom_hostname": "Nombre de host personalizado",
|
||||
"customize_survey_logo": "Personalizar el logotipo de la encuesta",
|
||||
"darken_or_lighten_background_of_your_choice": "Oscurece o aclara el fondo de tu elección.",
|
||||
"date_format": "Formato de fecha",
|
||||
"days_before_showing_this_survey_again": "días después de que se muestre cualquier encuesta antes de que esta encuesta pueda aparecer.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "No mostrar el botón de retroceso en la encuesta",
|
||||
"hide_block_settings": "Ocultar ajustes del bloque",
|
||||
"hide_logo": "Ocultar logotipo",
|
||||
"hide_logo_from_survey": "Ocultar logotipo de esta encuesta",
|
||||
"hide_progress_bar": "Ocultar barra de progreso",
|
||||
"hide_question_settings": "Ocultar ajustes de la pregunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Ocultar el logotipo en esta encuesta específica",
|
||||
"hostname": "Nombre de host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "¿Cuánto estilo quieres darle a tus tarjetas en las encuestas de tipo {surveyTypeDerived}?",
|
||||
"if_you_need_more_please": "Si necesitas más, por favor",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Cargar segmento",
|
||||
"logic_error_warning": "El cambio causará errores lógicos",
|
||||
"logic_error_warning_text": "Cambiar el tipo de pregunta eliminará las condiciones lógicas de esta pregunta",
|
||||
"logo_settings": "Ajustes del logotipo",
|
||||
"long_answer": "Respuesta larga",
|
||||
"long_answer_toggle_description": "Permitir a los encuestados escribir respuestas más largas y de varias líneas.",
|
||||
"lower_label": "Etiqueta inferior",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Establecer tiempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Anular la configuración del proyecto solo para esta encuesta.",
|
||||
"overwrite_placement": "Sobrescribir ubicación",
|
||||
"overwrite_survey_logo": "Establecer logotipo personalizado para la encuesta",
|
||||
"overwrite_the_global_placement_of_the_survey": "Sobrescribir la ubicación global de la encuesta",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Elige un fondo de nuestra biblioteca o sube el tuyo propio.",
|
||||
"picture_idx": "Imagen {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Mise à jour",
|
||||
"updated_at": "Mis à jour à",
|
||||
"upload": "Télécharger",
|
||||
"upload_failed": "Échec du téléchargement. Veuillez réessayer.",
|
||||
"upload_input_description": "Cliquez ou faites glisser pour charger un fichier.",
|
||||
"url": "URL",
|
||||
"user": "Utilisateur",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après",
|
||||
"back_button_label": "Label du bouton \"Retour''",
|
||||
"background_styling": "Style de fond",
|
||||
"background_styling": "Style d'arrière-plan",
|
||||
"block_duplicated": "Bloc dupliqué.",
|
||||
"bold": "Gras",
|
||||
"brand_color": "Couleur de marque",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "Sélecteur CSS",
|
||||
"cta_button_label": "Libellé du bouton « CTA »",
|
||||
"custom_hostname": "Nom d'hôte personnalisé",
|
||||
"customize_survey_logo": "Personnaliser le logo de l'enquête",
|
||||
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
|
||||
"date_format": "Format de date",
|
||||
"days_before_showing_this_survey_again": "jours après qu'une enquête soit affichée avant que cette enquête puisse apparaître.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Ne pas afficher le bouton retour dans l'enquête",
|
||||
"hide_block_settings": "Masquer les paramètres du bloc",
|
||||
"hide_logo": "Cacher le logo",
|
||||
"hide_logo_from_survey": "Masquer le logo de cette enquête",
|
||||
"hide_progress_bar": "Cacher la barre de progression",
|
||||
"hide_question_settings": "Masquer les paramètres de la question",
|
||||
"hide_the_logo_in_this_specific_survey": "Cacher le logo dans cette enquête spécifique",
|
||||
"hostname": "Nom d'hôte",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Si vous en avez besoin de plus, s'il vous plaît",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Segment de chargement",
|
||||
"logic_error_warning": "Changer causera des erreurs logiques",
|
||||
"logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.",
|
||||
"logo_settings": "Paramètres du logo",
|
||||
"long_answer": "Longue réponse",
|
||||
"long_answer_toggle_description": "Permettre aux répondants d'écrire des réponses plus longues et sur plusieurs lignes.",
|
||||
"lower_label": "Étiquette inférieure",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Définir un temps d'attente personnalisé",
|
||||
"overwrite_global_waiting_time_description": "Remplacer la configuration du projet pour cette enquête uniquement.",
|
||||
"overwrite_placement": "Surcharge de placement",
|
||||
"overwrite_survey_logo": "Définir un logo d'enquête personnalisé",
|
||||
"overwrite_the_global_placement_of_the_survey": "Surcharger le placement global de l'enquête",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Choisissez un arrière-plan dans notre bibliothèque ou téléchargez le vôtre.",
|
||||
"picture_idx": "Image {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "更新済み",
|
||||
"updated_at": "更新日時",
|
||||
"upload": "アップロード",
|
||||
"upload_failed": "アップロードに失敗しました。もう一度お試しください。",
|
||||
"upload_input_description": "クリックまたはドラッグしてファイルをアップロードしてください。",
|
||||
"url": "URL",
|
||||
"user": "ユーザー",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "ユーザーが一定秒数応答しない場合、フォームを自動的に閉じます。",
|
||||
"automatically_mark_the_survey_as_complete_after": "フォームを自動的に完了としてマークする",
|
||||
"back_button_label": "「戻る」ボタンのラベル",
|
||||
"background_styling": "背景のスタイル",
|
||||
"background_styling": "背景のスタイル設定",
|
||||
"block_duplicated": "ブロックが複製されました。",
|
||||
"bold": "太字",
|
||||
"brand_color": "ブランドカラー",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} フォームのカード配置",
|
||||
"card_background_color": "カードの背景色",
|
||||
"card_border_color": "カードの枠線の色",
|
||||
"card_styling": "カードのスタイル",
|
||||
"card_styling": "カードのスタイル設定",
|
||||
"casual": "カジュアル",
|
||||
"caution_edit_duplicate": "複製して編集",
|
||||
"caution_edit_published_survey": "公開済みのフォームを編集しますか?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSSセレクター",
|
||||
"cta_button_label": "\"CTA\"ボタンのラベル",
|
||||
"custom_hostname": "カスタムホスト名",
|
||||
"customize_survey_logo": "アンケートのロゴをカスタマイズする",
|
||||
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
|
||||
"date_format": "日付形式",
|
||||
"days_before_showing_this_survey_again": "任意のフォームが表示された後、このフォームが再表示されるまでの日数。",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "フォームに「戻る」ボタンを表示しない",
|
||||
"hide_block_settings": "ブロック設定を非表示",
|
||||
"hide_logo": "ロゴを非表示",
|
||||
"hide_logo_from_survey": "このアンケートからロゴを非表示にする",
|
||||
"hide_progress_bar": "プログレスバーを非表示",
|
||||
"hide_question_settings": "質問設定を非表示",
|
||||
"hide_the_logo_in_this_specific_survey": "この特定のフォームでロゴを非表示にする",
|
||||
"hostname": "ホスト名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
|
||||
"if_you_need_more_please": "さらに必要な場合は、",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "セグメントを読み込み",
|
||||
"logic_error_warning": "変更するとロジックエラーが発生します",
|
||||
"logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます",
|
||||
"logo_settings": "ロゴ設定",
|
||||
"long_answer": "長文回答",
|
||||
"long_answer_toggle_description": "回答者が長文の複数行の回答を書けるようにします。",
|
||||
"lower_label": "下限ラベル",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "カスタム待機時間を設定する",
|
||||
"overwrite_global_waiting_time_description": "このフォームのみプロジェクト設定を上書きします。",
|
||||
"overwrite_placement": "配置を上書き",
|
||||
"overwrite_survey_logo": "カスタムアンケートロゴを設定する",
|
||||
"overwrite_the_global_placement_of_the_survey": "フォームのグローバルな配置を上書き",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "ライブラリから背景を選択するか、独自にアップロードしてください。",
|
||||
"picture_idx": "写真 {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Bijgewerkt",
|
||||
"updated_at": "Bijgewerkt op",
|
||||
"upload": "Uploaden",
|
||||
"upload_failed": "Upload mislukt. Probeer het opnieuw.",
|
||||
"upload_input_description": "Klik of sleep om bestanden te uploaden.",
|
||||
"url": "URL",
|
||||
"user": "Gebruiker",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Sluit de enquête automatisch af als de gebruiker na een bepaald aantal seconden niet reageert.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Markeer de enquête daarna automatisch als voltooid",
|
||||
"back_button_label": "Knoplabel 'Terug'",
|
||||
"background_styling": "Achtergrondstyling",
|
||||
"background_styling": "Achtergrondstijl",
|
||||
"block_duplicated": "Blok gedupliceerd.",
|
||||
"bold": "Vetgedrukt",
|
||||
"brand_color": "Merk kleur",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Kaartarrangement voor {surveyTypeDerived} enquêtes",
|
||||
"card_background_color": "Achtergrondkleur van de kaart",
|
||||
"card_border_color": "Randkleur kaart",
|
||||
"card_styling": "Kaartstyling",
|
||||
"card_styling": "Kaartstijl",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Dupliceren en bewerken",
|
||||
"caution_edit_published_survey": "Een gepubliceerde enquête bewerken?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS-kiezer",
|
||||
"cta_button_label": "\"CTA\" knoplabel",
|
||||
"custom_hostname": "Aangepaste hostnaam",
|
||||
"customize_survey_logo": "Pas het enquêtelogo aan",
|
||||
"darken_or_lighten_background_of_your_choice": "Maak de achtergrond naar keuze donkerder of lichter.",
|
||||
"date_format": "Datumformaat",
|
||||
"days_before_showing_this_survey_again": "dagen nadat een enquête is getoond voordat deze enquête kan verschijnen.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Geef de terugknop niet weer in de enquête",
|
||||
"hide_block_settings": "Blokinstellingen verbergen",
|
||||
"hide_logo": "Logo verbergen",
|
||||
"hide_logo_from_survey": "Verberg logo van deze enquête",
|
||||
"hide_progress_bar": "Voortgangsbalk verbergen",
|
||||
"hide_question_settings": "Vraaginstellingen verbergen",
|
||||
"hide_the_logo_in_this_specific_survey": "Verberg het logo in deze specifieke enquête",
|
||||
"hostname": "Hostnaam",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hoe funky wil je je kaarten hebben in {surveyTypeDerived} Enquêtes",
|
||||
"if_you_need_more_please": "Als u meer nodig heeft, alstublieft",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Laadsegment",
|
||||
"logic_error_warning": "Wijzigen zal logische fouten veroorzaken",
|
||||
"logic_error_warning_text": "Als u het vraagtype wijzigt, worden de logische voorwaarden van deze vraag verwijderd",
|
||||
"logo_settings": "Logo-instellingen",
|
||||
"long_answer": "Lang antwoord",
|
||||
"long_answer_toggle_description": "Sta respondenten toe om langere antwoorden met meerdere regels te schrijven.",
|
||||
"lower_label": "Lager etiket",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Stel aangepaste wachttijd in",
|
||||
"overwrite_global_waiting_time_description": "Overschrijf de projectconfiguratie alleen voor deze enquête.",
|
||||
"overwrite_placement": "Plaatsing overschrijven",
|
||||
"overwrite_survey_logo": "Stel aangepast enquêtelogo in",
|
||||
"overwrite_the_global_placement_of_the_survey": "Overschrijf de globale plaatsing van de enquête",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Kies een achtergrond uit onze bibliotheek of upload je eigen achtergrond.",
|
||||
"picture_idx": "Afbeelding {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "atualizado",
|
||||
"updated_at": "Atualizado em",
|
||||
"upload": "Enviar",
|
||||
"upload_failed": "Falha no upload. Tente novamente.",
|
||||
"upload_input_description": "Clique ou arraste para fazer o upload de arquivos.",
|
||||
"url": "URL",
|
||||
"user": "Usuário",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente a pesquisa como concluída após",
|
||||
"back_button_label": "Voltar",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"background_styling": "Estilo do plano de fundo",
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Pesquisas {surveyTypeDerived}",
|
||||
"card_background_color": "Cor de fundo do cartão",
|
||||
"card_border_color": "Cor da borda do cartão",
|
||||
"card_styling": "Estilização de Cartão",
|
||||
"card_styling": "Estilo do cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
"caution_edit_published_survey": "Editar uma pesquisa publicada?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "Seletor CSS",
|
||||
"cta_button_label": "Rótulo do botão \"CTA\"",
|
||||
"custom_hostname": "Hostname personalizado",
|
||||
"customize_survey_logo": "Personalizar o logo da pesquisa",
|
||||
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
|
||||
"date_format": "Formato de data",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer pesquisa ser mostrada antes que esta pesquisa possa aparecer.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Não exibir o botão de voltar na pesquisa",
|
||||
"hide_block_settings": "Ocultar configurações do bloco",
|
||||
"hide_logo": "Esconder logo",
|
||||
"hide_logo_from_survey": "Esconder logo desta pesquisa",
|
||||
"hide_progress_bar": "Esconder barra de progresso",
|
||||
"hide_question_settings": "Ocultar configurações da pergunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Esconder o logo nessa pesquisa específica",
|
||||
"hostname": "nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Se você precisar de mais, por favor",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "segmento de carga",
|
||||
"logic_error_warning": "Mudar vai causar erros de lógica",
|
||||
"logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta",
|
||||
"logo_settings": "Configurações do logo",
|
||||
"long_answer": "resposta longa",
|
||||
"long_answer_toggle_description": "Permitir que os respondentes escrevam respostas mais longas e com várias linhas.",
|
||||
"lower_label": "Etiqueta Inferior",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para esta pesquisa.",
|
||||
"overwrite_placement": "Substituir posicionamento",
|
||||
"overwrite_survey_logo": "Definir logo personalizado para a pesquisa",
|
||||
"overwrite_the_global_placement_of_the_survey": "Substituir a posição global da pesquisa",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou faça upload do seu próprio.",
|
||||
"picture_idx": "Imagem {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Atualizado",
|
||||
"updated_at": "Atualizado em",
|
||||
"upload": "Carregar",
|
||||
"upload_failed": "Falha no carregamento. Por favor, tente novamente.",
|
||||
"upload_input_description": "Clique ou arraste para carregar ficheiros.",
|
||||
"url": "URL",
|
||||
"user": "Utilizador",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente o inquérito como concluído após",
|
||||
"back_button_label": "Rótulo do botão \"Voltar\"",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"background_styling": "Estilo de fundo",
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Inquéritos {surveyTypeDerived}",
|
||||
"card_background_color": "Cor de fundo do cartão",
|
||||
"card_border_color": "Cor da borda do cartão",
|
||||
"card_styling": "Estilo do cartão",
|
||||
"card_styling": "Estilo de cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
"caution_edit_published_survey": "Editar um inquérito publicado?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "Seletor CSS",
|
||||
"cta_button_label": "Etiqueta do botão \"CTA\"",
|
||||
"custom_hostname": "Nome do host personalizado",
|
||||
"customize_survey_logo": "Personalizar o logótipo do inquérito",
|
||||
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
|
||||
"date_format": "Formato da data",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer inquérito ser mostrado antes que este inquérito possa aparecer.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Não mostrar o botão de retroceder no inquérito",
|
||||
"hide_block_settings": "Ocultar definições do bloco",
|
||||
"hide_logo": "Esconder logótipo",
|
||||
"hide_logo_from_survey": "Ocultar logótipo deste inquérito",
|
||||
"hide_progress_bar": "Ocultar barra de progresso",
|
||||
"hide_question_settings": "Ocultar definições da pergunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Ocultar o logótipo neste inquérito específico",
|
||||
"hostname": "Nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Se precisar de mais, por favor",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Carregar segmento",
|
||||
"logic_error_warning": "A alteração causará erros de lógica",
|
||||
"logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta",
|
||||
"logo_settings": "Definições do logótipo",
|
||||
"long_answer": "Resposta longa",
|
||||
"long_answer_toggle_description": "Permitir que os inquiridos escrevam respostas mais longas e com várias linhas.",
|
||||
"lower_label": "Etiqueta Inferior",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para este inquérito.",
|
||||
"overwrite_placement": "Substituir colocação",
|
||||
"overwrite_survey_logo": "Definir logótipo de inquérito personalizado",
|
||||
"overwrite_the_global_placement_of_the_survey": "Substituir a colocação global do inquérito",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou carregue o seu próprio.",
|
||||
"picture_idx": "Imagem {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Actualizat",
|
||||
"updated_at": "Actualizat la",
|
||||
"upload": "Încărcați",
|
||||
"upload_failed": "Încărcarea a eșuat. Vă rugăm să încercați din nou.",
|
||||
"upload_input_description": "Faceți clic sau trageți pentru a încărca fișiere.",
|
||||
"url": "URL",
|
||||
"user": "Utilizator",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "Selector CSS",
|
||||
"cta_button_label": "Eticheta butonului \"CTA\"",
|
||||
"custom_hostname": "Gazdă personalizată",
|
||||
"customize_survey_logo": "Personalizează logo-ul chestionarului",
|
||||
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
|
||||
"date_format": "Format dată",
|
||||
"days_before_showing_this_survey_again": "zile după afișarea oricărui sondaj înainte ca acest sondaj să poată apărea din nou.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Nu afișa butonul Înapoi în sondaj",
|
||||
"hide_block_settings": "Ascunde setările blocului",
|
||||
"hide_logo": "Ascunde logo",
|
||||
"hide_logo_from_survey": "Ascunde logo-ul din acest chestionar",
|
||||
"hide_progress_bar": "Ascunde bara de progres",
|
||||
"hide_question_settings": "Ascunde setările întrebării",
|
||||
"hide_the_logo_in_this_specific_survey": "Ascunde logo-ul în acest chestionar specific",
|
||||
"hostname": "Nume gazdă",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Dacă aveți nevoie de mai multe, vă rugăm să",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Încarcă segment",
|
||||
"logic_error_warning": "Schimbarea va provoca erori de logică",
|
||||
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",
|
||||
"logo_settings": "Setări logo",
|
||||
"long_answer": "Răspuns lung",
|
||||
"long_answer_toggle_description": "Permite respondenților să scrie răspunsuri mai lungi, pe mai multe rânduri.",
|
||||
"lower_label": "Etichetă inferioară",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Setează un timp de așteptare personalizat",
|
||||
"overwrite_global_waiting_time_description": "Suprascrie configurația proiectului doar pentru acest sondaj.",
|
||||
"overwrite_placement": "Suprascriere amplasare",
|
||||
"overwrite_survey_logo": "Setează un logo personalizat pentru chestionar",
|
||||
"overwrite_the_global_placement_of_the_survey": "Suprascrie amplasarea globală a sondajului",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Alege un fundal din biblioteca noastră sau încarcă unul propriu.",
|
||||
"picture_idx": "Poză {idx}",
|
||||
|
||||
+37
-33
@@ -39,17 +39,17 @@
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Skapa ett konto",
|
||||
"email_does_not_match": "Hoppsan! Fel e-post \uD83E\uDD26",
|
||||
"email_does_not_match": "Hoppsan! Fel e-post 🤦",
|
||||
"email_does_not_match_description": "E-postadressen i inbjudan matchar inte din.",
|
||||
"go_to_app": "Gå till appen",
|
||||
"happy_to_have_you": "Kul att ha dig här \uD83E\uDD17",
|
||||
"happy_to_have_you": "Kul att ha dig här 🤗",
|
||||
"happy_to_have_you_description": "Vänligen skapa ett konto eller logga in.",
|
||||
"invite_expired": "Inbjudan har gått ut \uD83D\uDE25",
|
||||
"invite_expired": "Inbjudan har gått ut 😥",
|
||||
"invite_expired_description": "Inbjudningar är giltiga i 7 dagar. Vänligen begär en ny inbjudan.",
|
||||
"invite_not_found": "Inbjudan hittades inte \uD83D\uDE25",
|
||||
"invite_not_found": "Inbjudan hittades inte 😥",
|
||||
"invite_not_found_description": "Inbjudningskoden kan inte hittas eller har redan använts.",
|
||||
"login": "Logga in",
|
||||
"welcome_to_organization": "Du är med \uD83C\uDF89",
|
||||
"welcome_to_organization": "Du är med 🎉",
|
||||
"welcome_to_organization_description": "Välkommen till organisationen."
|
||||
},
|
||||
"last_used": "Senast använd",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"verification-requested": {
|
||||
"invalid_email_address": "Ogiltig e-postadress",
|
||||
"invalid_token": "Ogiltig token \u2639\uFE0F",
|
||||
"invalid_token": "Ogiltig token ☹️",
|
||||
"new_email_verification_success": "Om adressen är giltig har ett verifieringsmeddelande skickats.",
|
||||
"no_email_provided": "Ingen e-post angiven",
|
||||
"please_confirm_your_email_address": "Vänligen bekräfta din e-postadress",
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "Uppdaterad",
|
||||
"updated_at": "Uppdaterad",
|
||||
"upload": "Ladda upp",
|
||||
"upload_failed": "Uppladdning misslyckades. Vänligen försök igen.",
|
||||
"upload_input_description": "Klicka eller dra för att ladda upp filer.",
|
||||
"url": "URL",
|
||||
"user": "Användare",
|
||||
@@ -486,20 +487,20 @@
|
||||
"privacy_policy": "Integritetspolicy",
|
||||
"reject": "Avvisa",
|
||||
"render_email_response_value_file_upload_response_link_not_included": "Länk till uppladdad fil ingår inte av dataskyddsskäl",
|
||||
"response_finished_email_subject": "Ett svar för {surveyName} har slutförts \u2705",
|
||||
"response_finished_email_subject_with_email": "{personEmail} har precis slutfört din {surveyName}-enkät \u2705",
|
||||
"response_finished_email_subject": "Ett svar för {surveyName} har slutförts ✅",
|
||||
"response_finished_email_subject_with_email": "{personEmail} har precis slutfört din {surveyName}-enkät ✅",
|
||||
"schedule_your_meeting": "Boka ditt möte",
|
||||
"select_a_date": "Välj ett datum",
|
||||
"survey_response_finished_email_congrats": "Grattis, du har fått ett nytt svar på din enkät! Någon har precis slutfört din enkät: {surveyName}",
|
||||
"survey_response_finished_email_dont_want_notifications": "Vill du inte få dessa aviseringar?",
|
||||
"survey_response_finished_email_hey": "Hej \uD83D\uDC4B",
|
||||
"survey_response_finished_email_hey": "Hej 👋",
|
||||
"survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Stäng av aviseringar för alla nyskapade formulär",
|
||||
"survey_response_finished_email_turn_off_notifications_for_this_form": "Stäng av aviseringar för detta formulär",
|
||||
"survey_response_finished_email_view_more_responses": "Visa {responseCount} fler svar",
|
||||
"survey_response_finished_email_view_survey_summary": "Visa enkätsammanfattning",
|
||||
"verification_email_click_on_this_link": "Du kan också klicka på denna länk:",
|
||||
"verification_email_heading": "Nästan där!",
|
||||
"verification_email_hey": "Hej \uD83D\uDC4B",
|
||||
"verification_email_hey": "Hej 👋",
|
||||
"verification_email_if_expired_request_new_token": "Om den har gått ut, vänligen begär en ny token här:",
|
||||
"verification_email_link_valid_for_24_hours": "Länken är giltig i 24 timmar.",
|
||||
"verification_email_request_new_verification": "Begär ny verifiering",
|
||||
@@ -648,7 +649,7 @@
|
||||
"link_airtable_table": "Länka Airtable-tabell",
|
||||
"link_new_table": "Länka ny tabell",
|
||||
"no_bases_found": "Inga Airtable-baser hittades",
|
||||
"no_integrations_yet": "Dina Airtable-integrationer visas här så snart du lägger till dem. \u23F2\uFE0F",
|
||||
"no_integrations_yet": "Dina Airtable-integrationer visas här så snart du lägger till dem. ⏲️",
|
||||
"please_create_a_base": "Vänligen skapa en bas på Airtable",
|
||||
"please_select_a_base": "Vänligen välj en bas",
|
||||
"please_select_a_table": "Vänligen välj en tabell",
|
||||
@@ -674,7 +675,7 @@
|
||||
"google_sheets_integration_description": "Synka svar direkt med Google Kalkylark.",
|
||||
"link_google_sheet": "Länka Google Kalkylark",
|
||||
"link_new_sheet": "Länka nytt kalkylark",
|
||||
"no_integrations_yet": "Dina Google Kalkylark-integrationer visas här så snart du lägger till dem. \u23F2\uFE0F",
|
||||
"no_integrations_yet": "Dina Google Kalkylark-integrationer visas här så snart du lägger till dem. ⏲️",
|
||||
"spreadsheet_url": "Kalkylblads-URL"
|
||||
},
|
||||
"include_created_at": "Inkludera Skapad vid",
|
||||
@@ -698,7 +699,7 @@
|
||||
"link_new_database": "Länka ny databas",
|
||||
"link_notion_database": "Länka Notion-databas",
|
||||
"map_formbricks_fields_to_notion_property": "Mappa Formbricks-fält till Notion-egenskap",
|
||||
"no_databases_found": "Dina Notion-integrationer visas här så snart du lägger till dem. \u23F2\uFE0F",
|
||||
"no_databases_found": "Dina Notion-integrationer visas här så snart du lägger till dem. ⏲️",
|
||||
"notion_integration": "Notion-integration",
|
||||
"notion_integration_description": "Skicka svar direkt till Notion.",
|
||||
"notion_integration_is_not_configured": "Notion-integration är inte konfigurerad i din Formbricks-instans.",
|
||||
@@ -745,7 +746,7 @@
|
||||
"all_current_and_new_surveys": "Alla nuvarande och nya enkäter",
|
||||
"created_by_third_party": "Skapad av tredje part",
|
||||
"discord_webhook_not_supported": "Discord-webhooks stöds för närvarande inte.",
|
||||
"empty_webhook_message": "Dina webhooks visas här så snart du lägger till dem. \u23F2\uFE0F",
|
||||
"empty_webhook_message": "Dina webhooks visas här så snart du lägger till dem. ⏲️",
|
||||
"endpoint_pinged": "Ja! Vi kan nå webhooken!",
|
||||
"endpoint_pinged_error": "Kunde inte nå webhooken!",
|
||||
"please_check_console": "Vänligen kontrollera konsolen för mer information",
|
||||
@@ -800,7 +801,7 @@
|
||||
"formbricks_sdk_not_connected_description": "Lägg till Formbricks SDK på din webbplats eller i din app för att ansluta den med Formbricks",
|
||||
"how_to_setup": "Hur man konfigurerar",
|
||||
"how_to_setup_description": "Följ dessa steg för att konfigurera Formbricks-widgeten i din app.",
|
||||
"receiving_data": "Tar emot data \uD83D\uDC83\uD83D\uDD7A",
|
||||
"receiving_data": "Tar emot data 💃🕺",
|
||||
"recheck": "Kontrollera igen",
|
||||
"sdk_connection_details": "SDK-anslutningsdetaljer",
|
||||
"sdk_connection_details_description": "Ditt unika miljö-ID och SDK-anslutnings-URL för att integrera Formbricks med din applikation.",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS-selektor",
|
||||
"cta_button_label": "\"CTA\"-knappetikett",
|
||||
"custom_hostname": "Anpassat värdnamn",
|
||||
"customize_survey_logo": "Anpassa undersökningens logotyp",
|
||||
"darken_or_lighten_background_of_your_choice": "Gör bakgrunden mörkare eller ljusare efter eget val.",
|
||||
"date_format": "Datumformat",
|
||||
"days_before_showing_this_survey_again": "dagar efter att någon enkät visats innan denna enkät kan visas.",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "Visa inte tillbakaknappen i enkäten",
|
||||
"hide_block_settings": "Dölj blockinställningar",
|
||||
"hide_logo": "Dölj logotyp",
|
||||
"hide_logo_from_survey": "Dölj logotypen från denna undersökning",
|
||||
"hide_progress_bar": "Dölj framstegsindikator",
|
||||
"hide_question_settings": "Dölj frågeinställningar",
|
||||
"hide_the_logo_in_this_specific_survey": "Dölj logotypen i denna specifika enkät",
|
||||
"hostname": "Värdnamn",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hur coola vill du att dina kort ska vara i {surveyTypeDerived}-enkäter",
|
||||
"if_you_need_more_please": "Om du behöver fler, vänligen",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "Ladda segment",
|
||||
"logic_error_warning": "Ändring kommer att orsaka logikfel",
|
||||
"logic_error_warning_text": "Att ändra frågetypen kommer att ta bort logikvillkoren från denna fråga",
|
||||
"logo_settings": "Inställningar för logotyp",
|
||||
"long_answer": "Långt svar",
|
||||
"long_answer_toggle_description": "Tillåt respondenter att skriva längre svar på flera rader.",
|
||||
"lower_label": "Lägre etikett",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "Ställ in anpassad väntetid",
|
||||
"overwrite_global_waiting_time_description": "Åsidosätt projektkonfigurationen endast för denna enkät.",
|
||||
"overwrite_placement": "Åsidosätt placering",
|
||||
"overwrite_survey_logo": "Ange anpassad logotyp för undersökningen",
|
||||
"overwrite_the_global_placement_of_the_survey": "Åsidosätt den globala placeringen av enkäten",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Välj en bakgrund från vårt bibliotek eller ladda upp din egen.",
|
||||
"picture_idx": "Bild {idx}",
|
||||
@@ -1630,7 +1634,7 @@
|
||||
"survey_display_settings": "Visningsinställningar för enkät",
|
||||
"survey_placement": "Enkätplacering",
|
||||
"survey_trigger": "Enkätutlösare",
|
||||
"switch_multi_lanugage_on_to_get_started": "Aktivera flerspråk för att komma igång \uD83D\uDC49",
|
||||
"switch_multi_lanugage_on_to_get_started": "Aktivera flerspråk för att komma igång 👉",
|
||||
"target_block_not_found": "Målblock hittades inte",
|
||||
"targeted": "Riktad",
|
||||
"ten_points": "10 poäng",
|
||||
@@ -1705,7 +1709,7 @@
|
||||
"bulk_delete_response_quotas": "Svaren är del av kvoter för denna enkät. Hur vill du hantera kvoterna?",
|
||||
"city": "Stad",
|
||||
"company": "Företag",
|
||||
"completed": "Slutförd \u2705",
|
||||
"completed": "Slutförd ✅",
|
||||
"country": "Land",
|
||||
"decrement_quotas": "Minska alla gränser för kvoter inklusive detta svar",
|
||||
"delete_response_confirmation": "Detta kommer att ta bort enkätsvaret, inklusive alla svar, taggar, bifogade dokument och svarsmetadata.",
|
||||
@@ -1717,7 +1721,7 @@
|
||||
"first_name": "Förnamn",
|
||||
"how_to_identify_users": "Hur man identifierar användare",
|
||||
"last_name": "Efternamn",
|
||||
"not_completed": "Inte slutförd \u23F3",
|
||||
"not_completed": "Inte slutförd ⏳",
|
||||
"os": "OS",
|
||||
"person_attributes": "Personattribut vid tidpunkten för inskickning",
|
||||
"phone": "Telefon",
|
||||
@@ -2023,7 +2027,7 @@
|
||||
"setup": {
|
||||
"intro": {
|
||||
"get_started": "Kom igång",
|
||||
"made_with_love_in_kiel": "Gjort med \uD83E\uDD0D i Tyskland",
|
||||
"made_with_love_in_kiel": "Gjort med 🤍 i Tyskland",
|
||||
"paragraph_1": "Formbricks är en Experience Management Suite byggd av den <b>snabbast växande öppenkällkods enkätplattformen</b> i världen.",
|
||||
"paragraph_2": "Kör riktade enkäter på webbplatser, i appar eller var som helst online. Samla värdefulla insikter för att <b>skapa oemotståndliga upplevelser</b> för kunder, användare och anställda.",
|
||||
"paragraph_3": "Vi är engagerade i högsta grad av dataintegritet. Självhosta för att behålla <b>full kontroll över dina data</b>.",
|
||||
@@ -2173,7 +2177,7 @@
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vi skulle gärna behålla dig som kund. Gärna erbjuder vi 30% rabatt för nästa år.</span></p>",
|
||||
"churn_survey_question_4_headline": "Vilka funktioner saknar du?",
|
||||
"churn_survey_question_5_button_label": "Skicka e-post till VD",
|
||||
"churn_survey_question_5_headline": "Så tråkigt att höra \uD83D\uDE14 Prata direkt med vår VD!",
|
||||
"churn_survey_question_5_headline": "Så tråkigt att höra 😔 Prata direkt med vår VD!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vi strävar efter att ge bästa möjliga kundservice. Vänligen maila vår VD så hanterar hon personligen ditt ärende.</span></p>",
|
||||
"collect_feedback_description": "Samla omfattande feedback om din produkt eller tjänst.",
|
||||
"collect_feedback_name": "Samla feedback",
|
||||
@@ -2301,8 +2305,8 @@
|
||||
"default_welcome_card_html": "Tack för att du ger din feedback - låt oss köra igång!",
|
||||
"docs_feedback_description": "Mät hur tydlig varje sida i din utvecklardokumentation är.",
|
||||
"docs_feedback_name": "Dokumentationsfeedback",
|
||||
"docs_feedback_question_1_choice_1": "Ja \uD83D\uDC4D",
|
||||
"docs_feedback_question_1_choice_2": "Nej \uD83D\uDC4E",
|
||||
"docs_feedback_question_1_choice_1": "Ja 👍",
|
||||
"docs_feedback_question_1_choice_2": "Nej 👎",
|
||||
"docs_feedback_question_1_headline": "Var denna sida hjälpsam?",
|
||||
"docs_feedback_question_2_headline": "Vänligen utveckla:",
|
||||
"docs_feedback_question_3_headline": "Sid-URL",
|
||||
@@ -2367,7 +2371,7 @@
|
||||
"evaluate_a_product_idea_name": "Utvärdera en produktidé",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Kör på!",
|
||||
"evaluate_a_product_idea_question_1_headline": "Vi älskar hur du använder $[projectName]! Vi skulle gärna vilja höra dina tankar om en funktionsidé. Har du en minut?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vi respekterar din tid och höll det kort \uD83E\uDD38</span></p>",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vi respekterar din tid och höll det kort 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Tack! Hur svårt eller enkelt är det för dig att [PROBLEMOMRÅDE] idag?",
|
||||
"evaluate_a_product_idea_question_2_lower_label": "Mycket svårt",
|
||||
"evaluate_a_product_idea_question_2_upper_label": "Mycket enkelt",
|
||||
@@ -2416,8 +2420,8 @@
|
||||
"feature_chaser_question_2_headline": "Vilken aspekt är viktigast?",
|
||||
"feedback_box_description": "Ge dina användare möjlighet att smidigt dela vad de har på hjärtat.",
|
||||
"feedback_box_name": "Feedbackruta",
|
||||
"feedback_box_question_1_choice_1": "Buggrapport \uD83D\uDC1E",
|
||||
"feedback_box_question_1_choice_2": "Funktionsförfrågan \uD83D\uDCA1",
|
||||
"feedback_box_question_1_choice_1": "Buggrapport 🐞",
|
||||
"feedback_box_question_1_choice_2": "Funktionsförfrågan 💡",
|
||||
"feedback_box_question_1_headline": "Vad har du på hjärtat?",
|
||||
"feedback_box_question_1_subheader": "Tack för att du delar. Vi återkommer så snart som möjligt.",
|
||||
"feedback_box_question_2_headline": "Vad är trasigt?",
|
||||
@@ -2432,7 +2436,7 @@
|
||||
"file_upload": "Filuppladdning",
|
||||
"file_upload_description": "Låt respondenter ladda upp dokument, bilder eller andra filer",
|
||||
"finish": "Slutför",
|
||||
"follow_ups_modal_action_body": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Hej \uD83D\uDC4B</span><br><br><span style=\"white-space: pre-wrap;\">Tack för att du tog dig tid att svara, vi återkommer snart.</span><br><br><span style=\"white-space: pre-wrap;\">Ha en fin dag!</span></p>",
|
||||
"follow_ups_modal_action_body": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Hej 👋</span><br><br><span style=\"white-space: pre-wrap;\">Tack för att du tog dig tid att svara, vi återkommer snart.</span><br><br><span style=\"white-space: pre-wrap;\">Ha en fin dag!</span></p>",
|
||||
"free_text": "Fritext",
|
||||
"free_text_description": "Samla öppna återkopplingar",
|
||||
"free_text_placeholder": "Skriv ditt svar här...",
|
||||
@@ -2470,7 +2474,7 @@
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Skriv ditt svar här...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Registrera dig",
|
||||
"identify_sign_up_barriers_question_9_headline": "Tack! Här är din kod: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Tack så mycket för att du tog dig tid att dela feedback \uD83D\uDE4F</span></p>",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Tack så mycket för att du tog dig tid att dela feedback 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Ta reda på hur mycket tid din produkt sparar för användaren. Använd det för merförsäljning.",
|
||||
"identify_upsell_opportunities_name": "Identifiera merförsäljningsmöjligheter",
|
||||
"identify_upsell_opportunities_question_1_choice_1": "Mindre än 1 timme",
|
||||
@@ -2505,7 +2509,7 @@
|
||||
"improve_newsletter_content_question_2_headline": "Vad skulle ha gjort denna veckas nyhetsbrev mer hjälpsamt?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Skriv ditt svar här...",
|
||||
"improve_newsletter_content_question_3_button_label": "Hjälper gärna till!",
|
||||
"improve_newsletter_content_question_3_headline": "Tack! \u2764\uFE0F Sprid kärleken till EN vän.",
|
||||
"improve_newsletter_content_question_3_headline": "Tack! ❤️ Sprid kärleken till EN vän.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vem tänker som du? Du skulle göra oss en enorm tjänst om du delade denna veckas avsnitt med din tankevän!</span></p>",
|
||||
"improve_trial_conversion_description": "Ta reda på varför folk avslutade sin testperiod. Dessa insikter hjälper dig förbättra din tratt.",
|
||||
"improve_trial_conversion_name": "Förbättra testkonvertering",
|
||||
@@ -2538,7 +2542,7 @@
|
||||
"interview_prompt_description": "Bjud in en specifik undergrupp av dina användare att boka en intervju med ditt produktteam.",
|
||||
"interview_prompt_name": "Intervjuförfrågan",
|
||||
"interview_prompt_question_1_button_label": "Boka tid",
|
||||
"interview_prompt_question_1_headline": "Har du 15 min att prata med oss? \uD83D\uDE4F",
|
||||
"interview_prompt_question_1_headline": "Har du 15 min att prata med oss? 🙏",
|
||||
"interview_prompt_question_1_html": "Du är en av våra poweranvändare. Vi skulle gärna vilja intervjua dig kort!",
|
||||
"long_term_retention_check_in_description": "Mät långsiktig användarnöjdhet, lojalitet och förbättringsområden för att behålla lojala användare.",
|
||||
"long_term_retention_check_in_name": "Långsiktig retentionsuppföljning",
|
||||
@@ -2798,7 +2802,7 @@
|
||||
"review_prompt_question_1_lower_label": "Inte bra",
|
||||
"review_prompt_question_1_upper_label": "Mycket nöjd",
|
||||
"review_prompt_question_2_button_label": "Skriv recension",
|
||||
"review_prompt_question_2_headline": "Kul att höra \uD83D\uDE4F Vänligen skriv en recension åt oss!",
|
||||
"review_prompt_question_2_headline": "Kul att höra 🙏 Vänligen skriv en recension åt oss!",
|
||||
"review_prompt_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Detta hjälper oss väldigt mycket.</span></p>",
|
||||
"review_prompt_question_3_button_label": "Skicka",
|
||||
"review_prompt_question_3_headline": "Tråkigt att höra! Vad är EN sak vi kan göra bättre?",
|
||||
@@ -2841,7 +2845,7 @@
|
||||
"smileys_survey_question_1_lower_label": "Inte bra",
|
||||
"smileys_survey_question_1_upper_label": "Mycket nöjd",
|
||||
"smileys_survey_question_2_button_label": "Skriv recension",
|
||||
"smileys_survey_question_2_headline": "Kul att höra \uD83D\uDE4F Vänligen skriv en recension åt oss!",
|
||||
"smileys_survey_question_2_headline": "Kul att höra 🙏 Vänligen skriv en recension åt oss!",
|
||||
"smileys_survey_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Detta hjälper oss väldigt mycket.</span></p>",
|
||||
"smileys_survey_question_3_button_label": "Skicka",
|
||||
"smileys_survey_question_3_headline": "Tråkigt att höra! Vad är EN sak vi kan göra bättre?",
|
||||
@@ -2852,7 +2856,7 @@
|
||||
"star_rating_survey_question_1_lower_label": "Extremt missnöjd",
|
||||
"star_rating_survey_question_1_upper_label": "Extremt nöjd",
|
||||
"star_rating_survey_question_2_button_label": "Skriv recension",
|
||||
"star_rating_survey_question_2_headline": "Kul att höra \uD83D\uDE4F Vänligen skriv en recension åt oss!",
|
||||
"star_rating_survey_question_2_headline": "Kul att höra 🙏 Vänligen skriv en recension åt oss!",
|
||||
"star_rating_survey_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Detta hjälper oss väldigt mycket.</span></p>",
|
||||
"star_rating_survey_question_3_button_label": "Skicka",
|
||||
"star_rating_survey_question_3_headline": "Tråkigt att höra! Vad är EN sak vi kan göra bättre?",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "已更新",
|
||||
"updated_at": "更新 于",
|
||||
"upload": "上传",
|
||||
"upload_failed": "上传失败,请重试。",
|
||||
"upload_input_description": "点击 或 拖动 上传 文件",
|
||||
"url": "URL",
|
||||
"user": "用户",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "用户未在一定秒数内应答时 自动关闭 问卷",
|
||||
"automatically_mark_the_survey_as_complete_after": "自动 标记 调查 为 完成 在",
|
||||
"back_button_label": "\"返回\" 按钮标签",
|
||||
"background_styling": "背景 样式",
|
||||
"background_styling": "背景样式",
|
||||
"block_duplicated": "区块已复制。",
|
||||
"bold": "粗体",
|
||||
"brand_color": "品牌 颜色",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} 调查 的 卡片 布局",
|
||||
"card_background_color": "卡片 的 背景 颜色",
|
||||
"card_border_color": "卡片 的 边框 颜色",
|
||||
"card_styling": "卡 样式",
|
||||
"card_styling": "卡片样式",
|
||||
"casual": "休闲",
|
||||
"caution_edit_duplicate": "复制 并 编辑",
|
||||
"caution_edit_published_survey": "编辑 已 发布 的 survey?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS 选择器",
|
||||
"cta_button_label": "“CTA”按钮标签",
|
||||
"custom_hostname": "自 定 义 主 机 名",
|
||||
"customize_survey_logo": "自定义调查 logo",
|
||||
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
|
||||
"date_format": "日期格式",
|
||||
"days_before_showing_this_survey_again": "在显示此调查之前,需等待的天数。",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "不 显示 调查 中 的 返回 按钮",
|
||||
"hide_block_settings": "隐藏区块设置",
|
||||
"hide_logo": "隐藏 徽标",
|
||||
"hide_logo_from_survey": "隐藏此调查中的 logo",
|
||||
"hide_progress_bar": "隐藏 进度 条",
|
||||
"hide_question_settings": "隐藏问题设置",
|
||||
"hide_the_logo_in_this_specific_survey": "隐藏此特定调查中的 logo",
|
||||
"hostname": "主 机 名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
|
||||
"if_you_need_more_please": "如果你需要更多,请",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "载入 段落",
|
||||
"logic_error_warning": "更改 将 导致 逻辑 错误",
|
||||
"logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件",
|
||||
"logo_settings": "Logo 设置",
|
||||
"long_answer": "长答案",
|
||||
"long_answer_toggle_description": "允许受访者填写较长的多行答案。",
|
||||
"lower_label": "下限标签",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "设置自定义等待时间",
|
||||
"overwrite_global_waiting_time_description": "仅为此调查覆盖项目配置。",
|
||||
"overwrite_placement": "覆盖 放置",
|
||||
"overwrite_survey_logo": "设置自定义调查 logo",
|
||||
"overwrite_the_global_placement_of_the_survey": "覆盖 全局 调查 放置",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "从我们的库中选择一种 背景 或 上传您自己的。",
|
||||
"picture_idx": "图片 {idx}",
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"updated": "已更新",
|
||||
"updated_at": "更新時間",
|
||||
"upload": "上傳",
|
||||
"upload_failed": "上傳失敗。請再試一次。",
|
||||
"upload_input_description": "點擊或拖曳以上傳檔案。",
|
||||
"url": "網址",
|
||||
"user": "使用者",
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。",
|
||||
"automatically_mark_the_survey_as_complete_after": "在指定時間後自動將問卷標記為完成",
|
||||
"back_button_label": "「返回」按鈕標籤",
|
||||
"background_styling": "背景樣式設定",
|
||||
"background_styling": "背景樣式",
|
||||
"block_duplicated": "區塊已複製。",
|
||||
"bold": "粗體",
|
||||
"brand_color": "品牌顏色",
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"card_arrangement_for_survey_type_derived": "'{'surveyTypeDerived'}' 問卷的卡片排列",
|
||||
"card_background_color": "卡片背景顏色",
|
||||
"card_border_color": "卡片邊框顏色",
|
||||
"card_styling": "卡片樣式設定",
|
||||
"card_styling": "卡片樣式",
|
||||
"casual": "隨意",
|
||||
"caution_edit_duplicate": "複製 & 編輯",
|
||||
"caution_edit_published_survey": "編輯已發佈的調查?",
|
||||
@@ -1327,6 +1328,7 @@
|
||||
"css_selector": "CSS 選取器",
|
||||
"cta_button_label": "「CTA」按鈕標籤",
|
||||
"custom_hostname": "自訂主機名稱",
|
||||
"customize_survey_logo": "自訂問卷標誌",
|
||||
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
|
||||
"date_format": "日期格式",
|
||||
"days_before_showing_this_survey_again": "在顯示此問卷之前,需等待其他問卷顯示後的天數。",
|
||||
@@ -1427,9 +1429,9 @@
|
||||
"hide_back_button_description": "不要在問卷中顯示返回按鈕",
|
||||
"hide_block_settings": "隱藏區塊設定",
|
||||
"hide_logo": "隱藏標誌",
|
||||
"hide_logo_from_survey": "隱藏此問卷的標誌",
|
||||
"hide_progress_bar": "隱藏進度列",
|
||||
"hide_question_settings": "隱藏問題設定",
|
||||
"hide_the_logo_in_this_specific_survey": "在此特定問卷中隱藏標誌",
|
||||
"hostname": "主機名稱",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
|
||||
"if_you_need_more_please": "如果您需要更多,請",
|
||||
@@ -1476,6 +1478,7 @@
|
||||
"load_segment": "載入區隔",
|
||||
"logic_error_warning": "變更將導致邏輯錯誤",
|
||||
"logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件",
|
||||
"logo_settings": "標誌設定",
|
||||
"long_answer": "長回答",
|
||||
"long_answer_toggle_description": "允許受訪者撰寫較長的多行回答。",
|
||||
"lower_label": "下標籤",
|
||||
@@ -1508,6 +1511,7 @@
|
||||
"overwrite_global_waiting_time": "設定自訂等待時間",
|
||||
"overwrite_global_waiting_time_description": "僅覆蓋此問卷的專案設定。",
|
||||
"overwrite_placement": "覆寫位置",
|
||||
"overwrite_survey_logo": "設定自訂問卷標誌",
|
||||
"overwrite_the_global_placement_of_the_survey": "覆寫問卷的整體位置",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "從我們的媒體庫中選取背景或上傳您自己的背景。",
|
||||
"picture_idx": "圖片 '{'idx'}'",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdo
|
||||
{enabledLanguages.map((surveyLanguage) => (
|
||||
<button
|
||||
key={surveyLanguage.language.code}
|
||||
className="w-full rounded-md p-2 text-start hover:cursor-pointer hover:bg-slate-700"
|
||||
className="w-full truncate rounded-md p-2 text-start hover:cursor-pointer hover:bg-slate-700"
|
||||
onClick={() => {
|
||||
setLanguage(surveyLanguage.language.code);
|
||||
setShowLanguageSelect(false);
|
||||
|
||||
@@ -48,29 +48,33 @@ export function LanguageIndicator({
|
||||
<button
|
||||
aria-expanded={showLanguageDropdown}
|
||||
aria-haspopup="true"
|
||||
className="relative z-20 flex items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
|
||||
className="relative z-20 flex max-w-[120px] items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
|
||||
onClick={toggleDropdown}
|
||||
tabIndex={-1}
|
||||
type="button">
|
||||
{languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed.language.code, locale) : ""}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
<span className="max-w-full truncate">
|
||||
{languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed.language.code, locale) : ""}
|
||||
</span>
|
||||
<ChevronDown className="ml-1 h-4 w-4 flex-shrink-0" />
|
||||
</button>
|
||||
{showLanguageDropdown ? (
|
||||
<div
|
||||
className="absolute right-0 z-30 mt-1 max-h-64 space-y-2 overflow-auto rounded-md bg-slate-900 p-1 text-xs text-white"
|
||||
className="absolute right-0 z-30 mt-1 max-h-64 w-48 space-y-2 overflow-auto rounded-md bg-slate-900 p-1 text-xs text-white"
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map(
|
||||
(language) =>
|
||||
language.language.code !== languageToBeDisplayed?.language.code &&
|
||||
language.enabled && (
|
||||
<button
|
||||
className="block w-full rounded-sm p-1 text-left hover:bg-slate-700"
|
||||
className="flex w-full rounded-sm p-1 text-left hover:bg-slate-700"
|
||||
key={language.language.id}
|
||||
onClick={() => {
|
||||
changeLanguage(language);
|
||||
}}
|
||||
type="button">
|
||||
{getLanguageLabel(language.language.code, locale)}
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{getLanguageLabel(language.language.code, locale)}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -154,7 +154,6 @@ export const ThemeStyling = ({
|
||||
open={cardStylingOpen}
|
||||
setOpen={setCardStylingOpen}
|
||||
isSettingsPage
|
||||
project={project}
|
||||
surveyType={previewSurveyType}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
@@ -133,13 +133,16 @@ export const BlockCard = ({
|
||||
// A button label is invalid if it exists but doesn't have valid text for all enabled languages
|
||||
const surveyLanguages = localSurvey.languages ?? [];
|
||||
const hasInvalidButtonLabel =
|
||||
block.buttonLabel !== undefined && !isLabelValidForAllLanguages(block.buttonLabel, surveyLanguages);
|
||||
block.buttonLabel !== undefined &&
|
||||
block.buttonLabel["default"]?.trim() !== "" &&
|
||||
!isLabelValidForAllLanguages(block.buttonLabel, surveyLanguages);
|
||||
|
||||
// Check if back button label is invalid
|
||||
// Back button label should exist for all blocks except the first one
|
||||
const hasInvalidBackButtonLabel =
|
||||
blockIdx > 0 &&
|
||||
block.backButtonLabel !== undefined &&
|
||||
block.backButtonLabel["default"]?.trim() !== "" &&
|
||||
!isLabelValidForAllLanguages(block.backButtonLabel, surveyLanguages);
|
||||
|
||||
// Block should be highlighted if it has invalid elements OR invalid button labels
|
||||
@@ -291,28 +294,28 @@ export const BlockCard = ({
|
||||
open={!isBlockCollapsed}
|
||||
onOpenChange={() => setIsBlockCollapsed(!isBlockCollapsed)}
|
||||
className={cn(isBlockCollapsed ? "h-full" : "")}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="block h-full w-full cursor-pointer hover:bg-slate-100">
|
||||
<div className="flex h-full items-center justify-between px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700">{block.name}</h4>
|
||||
<p className="text-xs text-slate-500">
|
||||
{blockElementsCount} {blockElementsCountText}
|
||||
</p>
|
||||
<Collapsible.CollapsibleTrigger asChild>
|
||||
<div className="block h-full w-full cursor-pointer hover:bg-slate-100">
|
||||
<div className="flex h-full items-center justify-between px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700">{block.name}</h4>
|
||||
<p className="text-xs text-slate-500">
|
||||
{blockElementsCount} {blockElementsCountText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BlockMenu
|
||||
isFirstBlock={blockIdx === 0}
|
||||
isLastBlock={blockIdx === totalBlocks - 1}
|
||||
isOnlyBlock={totalBlocks === 1}
|
||||
onDuplicate={() => duplicateBlock(block.id)}
|
||||
onDelete={() => deleteBlock(block.id)}
|
||||
onMoveUp={() => moveBlock(block.id, "up")}
|
||||
onMoveDown={() => moveBlock(block.id, "down")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BlockMenu
|
||||
isFirstBlock={blockIdx === 0}
|
||||
isLastBlock={blockIdx === totalBlocks - 1}
|
||||
isOnlyBlock={totalBlocks === 1}
|
||||
onDuplicate={() => duplicateBlock(block.id)}
|
||||
onDelete={() => deleteBlock(block.id)}
|
||||
onMoveUp={() => moveBlock(block.id, "up")}
|
||||
onMoveDown={() => moveBlock(block.id, "down")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
@@ -513,8 +513,8 @@ export const ElementsView = ({
|
||||
id: newBlockId,
|
||||
name: getBlockName(index ?? prevSurvey.blocks.length),
|
||||
elements: [{ ...updatedElement, isDraft: true }],
|
||||
buttonLabel: createI18nString(t("templates.next"), []),
|
||||
backButtonLabel: createI18nString(t("templates.back"), []),
|
||||
buttonLabel: createI18nString("", []),
|
||||
backButtonLabel: createI18nString("", []),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { ChangeEvent, useRef, useState } from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { handleFileUpload } from "@/modules/storage/file-upload";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
|
||||
type LogoSettingsCardProps = {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
environmentId: string;
|
||||
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
|
||||
disabled?: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
};
|
||||
|
||||
export const LogoSettingsCard = ({
|
||||
open,
|
||||
setOpen,
|
||||
environmentId,
|
||||
form,
|
||||
disabled = false,
|
||||
isStorageConfigured,
|
||||
}: LogoSettingsCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [parent] = useAutoAnimate();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const logoUrl = form.watch("logo")?.url;
|
||||
const logoBgColor = form.watch("logo")?.bgColor;
|
||||
const isBgColorEnabled = !!logoBgColor;
|
||||
const isLogoHidden = form.watch("isLogoHidden");
|
||||
|
||||
const setLogoUrl = (url: string | undefined) => {
|
||||
const currentLogo = form.getValues("logo");
|
||||
form.setValue("logo", url ? { ...currentLogo, url } : undefined);
|
||||
};
|
||||
|
||||
const setLogoBgColor = (bgColor: string | undefined) => {
|
||||
const currentLogo = form.getValues("logo");
|
||||
form.setValue("logo", {
|
||||
...currentLogo,
|
||||
url: logoUrl,
|
||||
bgColor,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileInputChange = async (files: string[]) => {
|
||||
if (files.length > 0) {
|
||||
setLogoUrl(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHiddenFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const uploadResult = await handleFileUpload(file, environmentId);
|
||||
if (uploadResult.error) {
|
||||
toast.error(t("common.upload_failed"));
|
||||
return;
|
||||
}
|
||||
setLogoUrl(uploadResult.url);
|
||||
} catch {
|
||||
toast.error(t("common.upload_failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// Reset the input so the same file can be selected again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLogo = () => {
|
||||
form.setValue("logo", undefined);
|
||||
};
|
||||
|
||||
const toggleBackgroundColor = (enabled: boolean) => {
|
||||
setLogoBgColor(enabled ? logoBgColor || "#f8f8f8" : undefined);
|
||||
};
|
||||
|
||||
const handleBgColorChange = (color: string) => {
|
||||
setLogoBgColor(color);
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={(openState) => {
|
||||
if (disabled) return;
|
||||
setOpen(openState);
|
||||
}}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full cursor-pointer rounded-lg hover:bg-slate-50",
|
||||
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
|
||||
)}>
|
||||
<div className="inline-flex w-full px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-base font-semibold text-slate-800">
|
||||
{t("environments.surveys.edit.logo_settings")}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{t("environments.surveys.edit.customize_survey_logo")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isLogoHidden"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch checked={!!field.value} onCheckedChange={field.onChange} disabled={disabled} />
|
||||
</FormControl>
|
||||
<div>
|
||||
<FormLabel className="text-base font-semibold text-slate-900">
|
||||
{t("environments.surveys.edit.hide_logo")}
|
||||
</FormLabel>
|
||||
<FormDescription className="text-sm text-slate-800">
|
||||
{t("environments.surveys.edit.hide_logo_from_survey")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isLogoHidden && (
|
||||
<div className="space-y-4">
|
||||
<div className="font-medium text-slate-800">
|
||||
{t("environments.surveys.edit.overwrite_survey_logo")}
|
||||
</div>
|
||||
|
||||
{/* Hidden file input for replacing logo */}
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg, image/png, image/webp, image/heic"
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
onChange={handleHiddenFileChange}
|
||||
/>
|
||||
|
||||
{logoUrl ? (
|
||||
<>
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt="Survey Logo"
|
||||
width={256}
|
||||
height={56}
|
||||
style={{ backgroundColor: logoBgColor || undefined }}
|
||||
className="h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={disabled || isLoading}>
|
||||
{t("environments.project.look.replace_logo")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleRemoveLogo}
|
||||
disabled={disabled}>
|
||||
{t("environments.project.look.remove_logo")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AdvancedOptionToggle
|
||||
isChecked={isBgColorEnabled}
|
||||
onToggle={toggleBackgroundColor}
|
||||
htmlId="surveyLogoBgColor"
|
||||
title={t("environments.project.look.add_background_color")}
|
||||
description={t("environments.project.look.add_background_color_description")}
|
||||
childBorder
|
||||
customContainerClass="p-0"
|
||||
childrenContainerClass="overflow-visible"
|
||||
disabled={disabled}>
|
||||
{isBgColorEnabled && (
|
||||
<div className="px-2">
|
||||
<ColorPicker
|
||||
color={logoBgColor || "#f8f8f8"}
|
||||
onChange={handleBgColorChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AdvancedOptionToggle>
|
||||
</>
|
||||
) : (
|
||||
<FileInput
|
||||
id="survey-logo-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={handleFileInputChange}
|
||||
disabled={disabled}
|
||||
maxSizeInMB={5}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { defaultStyling } from "@/lib/styling/constants";
|
||||
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
|
||||
import { LogoSettingsCard } from "@/modules/survey/editor/components/logo-settings-card";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -64,6 +65,7 @@ export const StylingView = ({
|
||||
const setOverwriteThemeStyling = (value: boolean) => form.setValue("overwriteThemeStyling", value);
|
||||
|
||||
const [formStylingOpen, setFormStylingOpen] = useState(false);
|
||||
const [logoSettingsOpen, setLogoSettingsOpen] = useState(false);
|
||||
const [cardStylingOpen, setCardStylingOpen] = useState(false);
|
||||
const [stylingOpen, setStylingOpen] = useState(false);
|
||||
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
|
||||
@@ -88,6 +90,7 @@ export const StylingView = ({
|
||||
useEffect(() => {
|
||||
if (!overwriteThemeStyling) {
|
||||
setFormStylingOpen(false);
|
||||
setLogoSettingsOpen(false);
|
||||
setCardStylingOpen(false);
|
||||
setStylingOpen(false);
|
||||
}
|
||||
@@ -198,21 +201,31 @@ export const StylingView = ({
|
||||
setOpen={setCardStylingOpen}
|
||||
surveyType={localSurvey.type}
|
||||
disabled={!overwriteThemeStyling}
|
||||
project={project}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" && (
|
||||
<BackgroundStylingCard
|
||||
open={stylingOpen}
|
||||
setOpen={setStylingOpen}
|
||||
environmentId={environmentId}
|
||||
colors={colors}
|
||||
disabled={!overwriteThemeStyling}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
<>
|
||||
<BackgroundStylingCard
|
||||
open={stylingOpen}
|
||||
setOpen={setStylingOpen}
|
||||
environmentId={environmentId}
|
||||
colors={colors}
|
||||
disabled={!overwriteThemeStyling}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
|
||||
<LogoSettingsCard
|
||||
open={logoSettingsOpen}
|
||||
setOpen={setLogoSettingsOpen}
|
||||
disabled={!overwriteThemeStyling}
|
||||
environmentId={environmentId}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isCxMode && (
|
||||
|
||||
@@ -79,7 +79,7 @@ export const LinkSurveyWrapper = ({
|
||||
styling={styling}
|
||||
onBackgroundLoaded={handleBackgroundLoaded}>
|
||||
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
|
||||
{!styling.isLogoHidden && project.logo?.url && <ClientLogo projectLogo={project.logo} />}
|
||||
{!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && <ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} />}
|
||||
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
||||
{isPreview && (
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Project } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -11,7 +10,6 @@ import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { CardArrangementTabs } from "@/modules/ui/components/card-arrangement-tabs";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
@@ -24,7 +22,6 @@ type CardStylingSettingsProps = {
|
||||
isSettingsPage?: boolean;
|
||||
surveyType?: TSurveyType;
|
||||
disabled?: boolean;
|
||||
project: Project;
|
||||
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
|
||||
};
|
||||
|
||||
@@ -33,14 +30,12 @@ export const CardStylingSettings = ({
|
||||
surveyType,
|
||||
disabled,
|
||||
open,
|
||||
project,
|
||||
setOpen,
|
||||
form,
|
||||
}: CardStylingSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isAppSurvey = surveyType === "app";
|
||||
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
|
||||
const isLogoVisible = !!project.logo?.url;
|
||||
|
||||
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "straight";
|
||||
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "straight";
|
||||
@@ -222,35 +217,6 @@ export const CardStylingSettings = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && !isSettingsPage && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isLogoHidden"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex w-full items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
id="isLogoHidden"
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
/>
|
||||
</FormControl>
|
||||
<div>
|
||||
<FormLabel>
|
||||
{t("environments.surveys.edit.hide_logo")}
|
||||
<Badge type="gray" size="normal" text={t("common.link_surveys")} />
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.hide_the_logo_in_this_specific_survey")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!surveyType || isAppSurvey) && (
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
|
||||
@@ -5,20 +5,24 @@ import { ArrowUpRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TLogo } from "@formbricks/types/styling";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
interface ClientLogoProps {
|
||||
environmentId?: string;
|
||||
projectLogo: Project["logo"] | null;
|
||||
surveyLogo?: TLogo | null;
|
||||
previewSurvey?: boolean;
|
||||
}
|
||||
|
||||
export const ClientLogo = ({ environmentId, projectLogo, previewSurvey = false }: ClientLogoProps) => {
|
||||
export const ClientLogo = ({ environmentId, projectLogo, surveyLogo, previewSurvey = false }: ClientLogoProps) => {
|
||||
const { t } = useTranslation();
|
||||
const logoToUse = surveyLogo?.url ? surveyLogo : projectLogo;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(previewSurvey ? "" : "left-3 top-3 md:left-7 md:top-7", "group absolute z-0 rounded-lg")}
|
||||
style={{ backgroundColor: projectLogo?.bgColor }}>
|
||||
style={{ backgroundColor: logoToUse?.bgColor }}>
|
||||
{previewSurvey && environmentId && (
|
||||
<Link
|
||||
href={`/environments/${environmentId}/project/look`}
|
||||
@@ -30,9 +34,9 @@ export const ClientLogo = ({ environmentId, projectLogo, previewSurvey = false }
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
{projectLogo?.url ? (
|
||||
{logoToUse?.url ? (
|
||||
<Image
|
||||
src={projectLogo?.url}
|
||||
src={logoToUse?.url}
|
||||
className={cn(
|
||||
previewSurvey ? "max-h-12" : "max-h-16 md:max-h-20",
|
||||
"w-auto max-w-40 object-contain p-1 md:max-w-56"
|
||||
|
||||
@@ -117,7 +117,7 @@ export const ConfirmationModal = ({
|
||||
<CircleAlert className="h-4 w-4 text-slate-500" />
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<DialogTitle className="w-full text-left">{title}</DialogTitle>
|
||||
<DialogTitle className="w-full truncate text-left">{title}</DialogTitle>
|
||||
<DialogDescription className="w-full text-left">
|
||||
<span className="mt-2 whitespace-pre-wrap">
|
||||
{description ?? t("environments.project.general.this_action_cannot_be_undone")}
|
||||
|
||||
@@ -263,7 +263,12 @@ export const PreviewSurvey = ({
|
||||
<div className="flex h-full w-full flex-col justify-center px-1">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
||||
<ClientLogo
|
||||
environmentId={environment.id}
|
||||
projectLogo={project.logo}
|
||||
surveyLogo={styling.logo}
|
||||
previewSurvey
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="z-10 w-full rounded-lg border border-transparent">
|
||||
@@ -363,7 +368,12 @@ export const PreviewSurvey = ({
|
||||
isEditorView>
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
||||
<ClientLogo
|
||||
environmentId={environment.id}
|
||||
projectLogo={project.logo}
|
||||
surveyLogo={styling.logo}
|
||||
previewSurvey
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="z-0 w-full max-w-4xl rounded-lg border-transparent">
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"test:coverage": "dotenv -e ../../.env -- vitest run --coverage",
|
||||
"generate-api-specs": "./scripts/openapi/generate.sh",
|
||||
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
|
||||
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints"
|
||||
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints",
|
||||
"i18n:generate": "npx lingo.dev@latest i18n"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.879.0",
|
||||
@@ -101,11 +102,11 @@
|
||||
"lucide-react": "0.507.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "3.0.1",
|
||||
"next": "15.5.6",
|
||||
"next": "15.5.7",
|
||||
"next-auth": "4.24.12",
|
||||
"next-safe-action": "7.10.8",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.9",
|
||||
"nodemailer": "7.0.11",
|
||||
"otplib": "12.0.1",
|
||||
"papaparse": "5.5.2",
|
||||
"prismjs": "1.30.0",
|
||||
|
||||
@@ -62,7 +62,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)
|
||||
.fill("This is my Open Text answer");
|
||||
.fill("Open Text answer");
|
||||
await page.locator("#questionCard-0").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Single Select Question
|
||||
@@ -116,7 +116,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.locator("path").nth(3).click();
|
||||
await page.getByRole("radio", { name: "Rate 3 out of" }).check();
|
||||
await page.locator("#questionCard-3").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// NPS Question
|
||||
@@ -212,11 +212,9 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
|
||||
await page
|
||||
.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1).fill("Address");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("city");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
|
||||
@@ -232,7 +230,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
for (let i = 0; i < surveys.createAndSubmit.ranking.choices.length; i++) {
|
||||
await page.getByText(surveys.createAndSubmit.ranking.choices[i]).click();
|
||||
}
|
||||
await page.locator("#questionCard-12").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-12").getByRole("button", { name: "Finish" }).click();
|
||||
// loading spinner -> wait for it to disappear
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
});
|
||||
@@ -785,7 +783,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.openTextQuestion.placeholder)
|
||||
.fill("This is my Open Text answer");
|
||||
.fill("Open Text answer");
|
||||
await page.locator("#questionCard-0").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Single Select Question
|
||||
@@ -858,10 +856,9 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createWithLogicAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByRole("group", { name: "Choices" }).locator("path").nth(3).click();
|
||||
await page.getByRole("radio", { name: "Rate 4 out of" }).check();
|
||||
await page.locator("#questionCard-4").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// NPS Question
|
||||
@@ -972,14 +969,12 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
.fill("Address");
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
.fill("This is my city");
|
||||
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city).fill("city");
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-13").getByRole("button", { name: "Finish" }).click();
|
||||
|
||||
// loading spinner -> wait for it to disappear
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
@@ -997,13 +992,26 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
const updatedUrl = currentUrl.replace("summary?share=true", "responses");
|
||||
|
||||
await page.goto(updatedUrl);
|
||||
await page.waitForSelector("#response-table");
|
||||
await page.waitForSelector("table#response-table");
|
||||
|
||||
await expect(page.getByRole("cell", { name: "score" })).toBeVisible();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
await expect(page.getByRole("cell", { name: "32", exact: true })).toBeVisible();
|
||||
|
||||
await page.pause();
|
||||
|
||||
// Look for any cell containing "32" or a score-related value
|
||||
const scoreCell = page.getByRole("cell").filter({ hasText: /^32/ });
|
||||
await expect(scoreCell).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Look for the secret message in the table
|
||||
const secretCell = page.getByRole("cell").filter({ hasText: /This is a secret message for e2e tests/ });
|
||||
await expect(secretCell).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -656,7 +656,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
||||
await page.locator("#action-2-operator").click();
|
||||
await page.getByRole("option", { name: "Assign =" }).click();
|
||||
await page.locator("#action-2-value-input").click();
|
||||
await page.locator("#action-2-value-input").fill("1");
|
||||
await page.locator("#action-2-value-input").fill("This ");
|
||||
// Close Block 1 settings before moving to Block 2
|
||||
await page
|
||||
.locator("div")
|
||||
|
||||
@@ -106,51 +106,51 @@ export const surveys = {
|
||||
createAndSubmit: {
|
||||
welcomeCard: {
|
||||
headline: "Welcome to My Testing Survey Welcome Card!",
|
||||
description: "This is the description of my Welcome Card!",
|
||||
description: "the description of my Welcome Card!",
|
||||
},
|
||||
openTextQuestion: {
|
||||
question: "This is my Open Text Question",
|
||||
description: "This is my Open Text Description",
|
||||
placeholder: "This is my Placeholder",
|
||||
question: "Open Text Question",
|
||||
description: "Open Text Description",
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
singleSelectQuestion: {
|
||||
question: "This is my Single Select Question",
|
||||
description: "This is my Single Select Description",
|
||||
question: "Single Select Question",
|
||||
description: "Single Select Description",
|
||||
options: ["Option 1", "Option 2"],
|
||||
},
|
||||
multiSelectQuestion: {
|
||||
question: "This is my Multi Select Question",
|
||||
description: "This is Multi Select Description",
|
||||
question: "Multi Select Question",
|
||||
description: "Multi Select Description",
|
||||
options: ["Option 1", "Option 2", "Option 3"],
|
||||
},
|
||||
ratingQuestion: {
|
||||
question: "This is my Rating Question",
|
||||
description: "This is Rating Description",
|
||||
question: "Rating Question",
|
||||
description: "Rating Description",
|
||||
lowLabel: "My Lower Label",
|
||||
highLabel: "My Upper Label",
|
||||
},
|
||||
npsQuestion: {
|
||||
question: "This is my NPS Question",
|
||||
question: "NPS Question",
|
||||
lowLabel: "My Lower Label",
|
||||
highLabel: "My Upper Label",
|
||||
},
|
||||
ctaQuestion: {
|
||||
question: "This is my CTA Question",
|
||||
question: "CTA Question",
|
||||
buttonLabel: "My Button Label",
|
||||
},
|
||||
consentQuestion: {
|
||||
question: "This is my Consent Question",
|
||||
question: "Consent Question",
|
||||
checkboxLabel: "My Checkbox Label",
|
||||
},
|
||||
pictureSelectQuestion: {
|
||||
question: "This is my Picture Select Question",
|
||||
description: "This is Picture Select Description",
|
||||
question: "Picture Select Question",
|
||||
description: "Picture Select Description",
|
||||
},
|
||||
dateQuestion: {
|
||||
question: "This is my Date Question",
|
||||
question: "Date Question",
|
||||
},
|
||||
fileUploadQuestion: {
|
||||
question: "This is my File Upload Question",
|
||||
question: "File Upload Question",
|
||||
},
|
||||
matrix: {
|
||||
question: "How much do you love these flowers?",
|
||||
@@ -178,57 +178,57 @@ export const surveys = {
|
||||
createWithLogicAndSubmit: {
|
||||
welcomeCard: {
|
||||
headline: "Welcome to My Testing Survey Welcome Card!",
|
||||
description: "This is the description of my Welcome Card!",
|
||||
description: "the description of my Welcome Card!",
|
||||
},
|
||||
openTextQuestion: {
|
||||
question: "This is my Open Text Question",
|
||||
description: "This is my Open Text Description",
|
||||
placeholder: "This is my Placeholder",
|
||||
question: "Open Text Question",
|
||||
description: "Open Text Description",
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
singleSelectQuestion: {
|
||||
question: "This is my Single Select Question",
|
||||
description: "This is my Single Select Description",
|
||||
question: "Single Select Question",
|
||||
description: "Single Select Description",
|
||||
options: ["Option 1", "Option 2"],
|
||||
},
|
||||
multiSelectQuestion: {
|
||||
question: "This is my Multi Select Question",
|
||||
description: "This is Multi Select Description",
|
||||
question: "Multi Select Question",
|
||||
description: "Multi Select Description",
|
||||
options: ["Option 1", "Option 2", "Option 3"],
|
||||
},
|
||||
ratingQuestion: {
|
||||
question: "This is my Rating Question",
|
||||
description: "This is Rating Description",
|
||||
question: "Rating Question",
|
||||
description: "Rating Description",
|
||||
lowLabel: "My Lower Label",
|
||||
highLabel: "My Upper Label",
|
||||
},
|
||||
npsQuestion: {
|
||||
question: "This is my NPS Question",
|
||||
question: "NPS Question",
|
||||
lowLabel: "My Lower Label",
|
||||
highLabel: "My Upper Label",
|
||||
},
|
||||
ctaQuestion: {
|
||||
question: "This is my CTA Question",
|
||||
question: "CTA Question",
|
||||
buttonLabel: "My Button Label",
|
||||
},
|
||||
consentQuestion: {
|
||||
question: "This is my Consent Question",
|
||||
question: "Consent Question",
|
||||
checkboxLabel: "My Checkbox Label",
|
||||
},
|
||||
pictureSelectQuestion: {
|
||||
question: "This is my Picture Select Question",
|
||||
description: "This is Picture Select Description",
|
||||
question: "Picture Select Question",
|
||||
description: "Picture Select Description",
|
||||
},
|
||||
fileUploadQuestion: {
|
||||
question: "This is my File Upload Question",
|
||||
question: "File Upload Question",
|
||||
},
|
||||
date: {
|
||||
question: "This is my Date Question",
|
||||
question: "Date Question",
|
||||
},
|
||||
cal: {
|
||||
question: "This is my cal Question",
|
||||
question: "cal Question",
|
||||
},
|
||||
matrix: {
|
||||
question: "This is my Matrix Question",
|
||||
question: "Matrix Question",
|
||||
description: "0: Not at all, 3: Love it",
|
||||
rows: ["Roses", "Trees", "Ocean"],
|
||||
columns: ["0", "1", "2", "3"],
|
||||
@@ -242,7 +242,7 @@ export const surveys = {
|
||||
},
|
||||
},
|
||||
ranking: {
|
||||
question: "This is my Ranking Question",
|
||||
question: "Ranking Question",
|
||||
choices: ["Work", "Money", "Travel", "Family", "Friends"],
|
||||
},
|
||||
endingCard: {
|
||||
@@ -342,12 +342,12 @@ export const actions = {
|
||||
noCode: {
|
||||
click: {
|
||||
name: "Create Click Action (CSS Selector)",
|
||||
description: "This is my Create Action (click, CSS Selector)",
|
||||
description: "Create Action (click, CSS Selector)",
|
||||
selector: ".my-custom-class",
|
||||
},
|
||||
pageView: {
|
||||
name: "Create Page view Action (specific Page URL)",
|
||||
description: "This is my Create Action (Page view)",
|
||||
description: "Create Action (Page view)",
|
||||
matcher: {
|
||||
label: "Contains",
|
||||
value: "custom-url",
|
||||
@@ -355,16 +355,16 @@ export const actions = {
|
||||
},
|
||||
exitIntent: {
|
||||
name: "Create Exit Intent Action",
|
||||
description: "This is my Create Action (Exit Intent)",
|
||||
description: "Create Action (Exit Intent)",
|
||||
},
|
||||
fiftyPercentScroll: {
|
||||
name: "Create 50% Scroll Action",
|
||||
description: "This is my Create Action (50% Scroll)",
|
||||
description: "Create Action (50% Scroll)",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
name: "Create Action (Code)",
|
||||
description: "This is my Create Action (Code)",
|
||||
description: "Create Action (Code)",
|
||||
key: "Create Action (Code)",
|
||||
},
|
||||
},
|
||||
@@ -372,12 +372,12 @@ export const actions = {
|
||||
noCode: {
|
||||
click: {
|
||||
name: "Edit Click Action (CSS Selector)",
|
||||
description: "This is my Edit Action (click, CSS Selector)",
|
||||
description: "Edit Action (click, CSS Selector)",
|
||||
selector: ".my-custom-class-edited",
|
||||
},
|
||||
pageView: {
|
||||
name: "Edit Page view Action (specific Page URL)",
|
||||
description: "This is my Edit Action (Page view)",
|
||||
description: "Edit Action (Page view)",
|
||||
matcher: {
|
||||
label: "Starts with",
|
||||
value: "custom-url0-edited",
|
||||
@@ -386,26 +386,26 @@ export const actions = {
|
||||
},
|
||||
exitIntent: {
|
||||
name: "Edit Exit Intent Action",
|
||||
description: "This is my Edit Action (Exit Intent)",
|
||||
description: "Edit Action (Exit Intent)",
|
||||
},
|
||||
fiftyPercentScroll: {
|
||||
name: "Edit 50% Scroll Action",
|
||||
description: "This is my Edit Action (50% Scroll)",
|
||||
description: "Edit Action (50% Scroll)",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
description: "This is my Edit Action (Code)",
|
||||
description: "Edit Action (Code)",
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
noCode: {
|
||||
name: "Delete click Action (CSS Selector)",
|
||||
description: "This is my Delete Action (CSS Selector)",
|
||||
description: "Delete Action (CSS Selector)",
|
||||
selector: ".my-custom-class-deleted",
|
||||
},
|
||||
code: {
|
||||
name: "Delete Action (Code)",
|
||||
description: "This is my Delete Action (Code)",
|
||||
description: "Delete Action (Code)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"self-hosting/configuration/smtp",
|
||||
"self-hosting/configuration/file-uploads",
|
||||
"self-hosting/configuration/domain-configuration",
|
||||
"self-hosting/configuration/custom-subpath",
|
||||
{
|
||||
"group": "Auth & SSO",
|
||||
"icon": "lock",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Custom Subpath"
|
||||
description: "Serve Formbricks from a custom URL prefix when you cannot expose it on the root domain."
|
||||
icon: "link"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Custom subpath deployments are currently under internal review. If you need early access, please reach out via
|
||||
[GitHub Discussions](https://github.com/formbricks/formbricks/discussions).
|
||||
</Note>
|
||||
|
||||
### When to use a custom subpath
|
||||
|
||||
Use a custom subpath (also called a Next.js base path) when your reverse proxy reserves the root domain for another
|
||||
service, but you still want Formbricks to live under the same hostname—for example `https://example.com/feedback`.
|
||||
Support for a build-time `BASE_PATH` variable is available in the Formbricks web app so that all internal routes,
|
||||
assets, and sign-in redirects honor the prefix.
|
||||
|
||||
### Requirements and limitations
|
||||
|
||||
- `BASE_PATH` must be present during `pnpm build`; changing it afterward requires a rebuild.
|
||||
- Official Formbricks Docker images do **not** accept this flag for technical reasons, so you must build your own image.
|
||||
- All public URLs (`WEBAPP_URL`, `NEXTAUTH_URL`, webhook targets, OAuth callbacks, etc.) need the same prefix.
|
||||
- Your proxy must rewrite `/custom-path/*` to the Formbricks container while keeping the prefix visible to clients.
|
||||
|
||||
### Configure environment variables
|
||||
|
||||
Add the following variables to the environment you use for builds (local, CI, or Docker build args):
|
||||
|
||||
```bash
|
||||
BASE_PATH="/custom-path"
|
||||
WEBAPP_URL="https://yourdomain.com/custom-path"
|
||||
NEXTAUTH_URL="https://yourdomain.com/custom-path/api/auth"
|
||||
```
|
||||
|
||||
If you use email links, webhooks, or third-party OAuth providers, ensure every URL you register includes the prefix.
|
||||
|
||||
### Build a Docker image with a custom subpath
|
||||
|
||||
<Steps>
|
||||
<Step title="Clone Formbricks and prepare secrets">
|
||||
Make sure you have the repository checked out and create temporary files (or use <code>--secret</code>) for the
|
||||
required build-time secrets such as <code>DATABASE_URL</code>, <code>ENCRYPTION_KEY</code>, <code>REDIS_URL</code>,
|
||||
and optional telemetry tokens.
|
||||
</Step>
|
||||
<Step title="Pass BASE_PATH as a build argument">
|
||||
Use the Formbricks web Dockerfile and supply the custom subpath via <code>--build-arg</code>. Example:
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--progress=plain \
|
||||
--no-cache \
|
||||
--build-arg BASE_PATH=/custom-path \
|
||||
--secret id=database_url,src=<(printf "postgresql://user:password@localhost:5432/formbricks?schema=public") \
|
||||
--secret id=encryption_key,src=<(printf "your-32-character-encryption-key-here") \
|
||||
--secret id=redis_url,src=<(printf "redis://localhost:6379") \
|
||||
--secret id=sentry_auth_token,src=<(printf "") \
|
||||
-f apps/web/Dockerfile \
|
||||
-t formbricks-web \
|
||||
.
|
||||
```
|
||||
|
||||
During the build logs you should see <code>BASE PATH /custom-path</code>, confirming that Next.js picked up the
|
||||
prefix.
|
||||
</Step>
|
||||
<Step title="Run the container behind your proxy">
|
||||
Start the resulting image with the same runtime environment variables you normally use (database credentials,
|
||||
mailing provider, etc.). Point your reverse proxy so that <code>/custom-path</code> requests forward to
|
||||
<code>http://formbricks-web:3000/custom-path</code> without stripping the prefix.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Verify the deployment
|
||||
|
||||
1. Open `https://yourdomain.com/custom-path` and complete the onboarding flow.
|
||||
2. Create a survey and preview it—embedded scripts now load assets relative to the subpath.
|
||||
3. Sign out and confirm the login page still includes `/custom-path`.
|
||||
|
||||
### Troubleshooting checklist
|
||||
|
||||
- Confirm your build pipeline actually passes `BASE_PATH` (and, if needed, `WEBAPP_URL`/`NEXTAUTH_URL`) into the build
|
||||
stage—check CI logs for the `BASE PATH /your-prefix` line and make sure custom Dockerfiles or wrappers forward
|
||||
`--build-arg BASE_PATH=...` correctly.
|
||||
- If you cannot log in, double-check that `NEXTAUTH_URL` includes the prefix and uses the full route to the API as stated above. NextAuth rejects callbacks that do not
|
||||
match exactly.
|
||||
- Re-run the Docker build when changing `BASE_PATH`; simply editing the container environment is not sufficient.
|
||||
- Inspect your proxy configuration to ensure it does not rewrite paths internally (e.g., `strip_prefix` needs to stay
|
||||
disabled).
|
||||
- When in doubt, rebuild locally with `--progress=plain` and verify that the `BASE PATH` line reflects your prefix.
|
||||
|
||||
@@ -81,6 +81,13 @@ Example of Response Created webhook payload:
|
||||
}
|
||||
},
|
||||
"singleUseId": null,
|
||||
"survey": {
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"type": "link",
|
||||
"status": "inProgress",
|
||||
"createdAt": "2025-07-20T10:30:00.000Z",
|
||||
"updatedAt": "2025-07-24T07:45:00.000Z"
|
||||
},
|
||||
"surveyId": "surveyId",
|
||||
"tags": [],
|
||||
"ttc": {
|
||||
@@ -125,6 +132,13 @@ Example of Response Updated webhook payload:
|
||||
}
|
||||
},
|
||||
"singleUseId": null,
|
||||
"survey": {
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"type": "link",
|
||||
"status": "inProgress",
|
||||
"createdAt": "2025-07-20T10:30:00.000Z",
|
||||
"updatedAt": "2025-07-24T07:45:00.000Z"
|
||||
},
|
||||
"surveyId": "surveyId",
|
||||
"tags": [],
|
||||
"ttc": {
|
||||
@@ -170,6 +184,13 @@ Example of Response Finished webhook payload:
|
||||
}
|
||||
},
|
||||
"singleUseId": null,
|
||||
"survey": {
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"type": "link",
|
||||
"status": "inProgress",
|
||||
"createdAt": "2025-07-20T10:30:00.000Z",
|
||||
"updatedAt": "2025-07-24T07:45:00.000Z"
|
||||
},
|
||||
"surveyId": "surveyId",
|
||||
"tags": [],
|
||||
"ttc": {
|
||||
|
||||
+5
-4
@@ -34,15 +34,16 @@
|
||||
"prepare": "husky install",
|
||||
"storybook": "turbo run storybook",
|
||||
"fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate",
|
||||
"i18n:generate": " pnpm --filter @formbricks/surveys i18n:generate",
|
||||
"generate-translations": "cd apps/web && npx lingo.dev@latest i18n",
|
||||
"i18n:surveys:generate": "pnpm --filter @formbricks/surveys i18n:generate",
|
||||
"i18n:web:generate": "pnpm --filter @formbricks/web i18n:generate",
|
||||
"generate-translations": "pnpm i18n:web:generate && pnpm i18n:surveys:generate",
|
||||
"scan-translations": "pnpm --filter @formbricks/i18n-utils scan-translations",
|
||||
"i18n": "pnpm generate-translations && pnpm scan-translations",
|
||||
"i18n:validate": "pnpm scan-translations"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
"react": "19.1.2",
|
||||
"react-dom": "19.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/microsoft-playwright-testing": "1.0.0-beta.7",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { SurveyStatus, SurveyType } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
// eslint-disable-next-line import/no-relative-packages -- Need to import from parent package
|
||||
import { ZLogo } from "../../types/styling";
|
||||
import { ZSurveyBlocks } from "../../types/surveys/blocks";
|
||||
import {
|
||||
ZSurveyEnding,
|
||||
@@ -172,6 +174,7 @@ const ZSurveyBase = z.object({
|
||||
background: ZSurveyStylingBackground.nullish(),
|
||||
hideProgressBar: z.boolean().nullish(),
|
||||
isLogoHidden: z.boolean().nullish(),
|
||||
logo: ZLogo.nullish(),
|
||||
})
|
||||
.nullable()
|
||||
.openapi({
|
||||
|
||||
@@ -43,8 +43,6 @@ export function AddressElement({
|
||||
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
|
||||
}, [value]);
|
||||
|
||||
const isCurrent = element.id === currentElementId;
|
||||
|
||||
const fields = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -166,7 +164,7 @@ export function AddressElement({
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
aria-label={field.label}
|
||||
dir={!safeValue[index] ? dir : "auto"}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,6 @@ export function ConsentElement({
|
||||
}: Readonly<ConsentElementProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const isCurrent = element.id === currentElementId;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
|
||||
@@ -66,7 +65,7 @@ export function ConsentElement({
|
||||
/>
|
||||
<label
|
||||
ref={consentRef}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
id={`${element.id}-label`}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
|
||||
@@ -37,7 +37,6 @@ export function ContactInfoElement({
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const safeValue = useMemo(() => {
|
||||
return Array.isArray(value) ? value : ["", "", "", "", ""];
|
||||
}, [value]);
|
||||
@@ -149,7 +148,7 @@ export function ContactInfoElement({
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
aria-label={field.label}
|
||||
dir={!safeValue[index] ? dir : "auto"}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function CTAElement({
|
||||
<button
|
||||
dir="auto"
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
onClick={handleExternalButtonClick}
|
||||
className="fb-text-heading focus:fb-ring-focus fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
{getLocalizedValue(element.ctaButtonLabel, languageCode)}
|
||||
|
||||
@@ -86,7 +86,6 @@ export function DateElement({
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
||||
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
|
||||
@@ -161,7 +160,7 @@ export function DateElement({
|
||||
onClick={() => {
|
||||
setDatePickerOpen(true);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") setDatePickerOpen(true);
|
||||
|
||||
@@ -31,7 +31,6 @@ export function MatrixElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const rowShuffleIdx = useMemo(() => {
|
||||
if (element.shuffleOption !== "none") {
|
||||
return getShuffledRowIndices(element.rows.length, element.shuffleOption);
|
||||
@@ -127,7 +126,7 @@ export function MatrixElement({
|
||||
{element.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={`column-${columnIndex.toString()}`}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === element.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
||||
onClick={() => {
|
||||
handleSelect(
|
||||
|
||||
@@ -57,7 +57,6 @@ export function MultipleChoiceMultiElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
if (element.shuffleOption) {
|
||||
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
||||
@@ -212,9 +211,9 @@ export function MultipleChoiceMultiElement({
|
||||
return (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleKeyDown(choice.id)}
|
||||
onKeyDown={handleKeyDown(choice.id)} // NOSONAR - needed for keyboard navigation through options
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
@@ -261,14 +260,16 @@ export function MultipleChoiceMultiElement({
|
||||
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleKeyDown(otherOption.id)}>
|
||||
// Disable keyboard navigation when 'other' option is selected to allow space key in input field
|
||||
onKeyDown={otherSelected ? undefined : handleKeyDown(otherOption.id)} // NOSONAR - needed for keyboard navigation through options
|
||||
>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
dir={dir}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
name={element.id}
|
||||
value={otherLabel}
|
||||
@@ -289,7 +290,7 @@ export function MultipleChoiceMultiElement({
|
||||
id={`${otherOption.id}-specify`}
|
||||
maxLength={250}
|
||||
name={element.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
value={otherValue}
|
||||
pattern=".*\S+.*"
|
||||
onChange={(e) => setOtherValue(e.currentTarget.value)}
|
||||
@@ -314,9 +315,10 @@ export function MultipleChoiceMultiElement({
|
||||
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleKeyDown(noneOption.id)}>
|
||||
onKeyDown={handleKeyDown(noneOption.id)} // NOSONAR - needed for keyboard navigation through options
|
||||
>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -36,7 +36,6 @@ export function MultipleChoiceSingleElement({
|
||||
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
||||
const choicesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
if (element.shuffleOption) {
|
||||
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
||||
@@ -158,9 +157,9 @@ export function MultipleChoiceSingleElement({
|
||||
return (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleKeyDown(choice.id)}
|
||||
onKeyDown={handleKeyDown(choice.id)} // NOSONAR - needed for keyboard navigation through options
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
@@ -197,7 +196,11 @@ export function MultipleChoiceSingleElement({
|
||||
: "Please specify";
|
||||
|
||||
return (
|
||||
<label tabIndex={isCurrent ? 0 : -1} className={labelClassName} onKeyDown={handleOtherKeyDown}>
|
||||
<label
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleOtherKeyDown} // NOSONAR - needed for keyboard navigation through options
|
||||
>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
@@ -246,7 +249,7 @@ export function MultipleChoiceSingleElement({
|
||||
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={labelClassName}
|
||||
onKeyDown={handleKeyDown(noneOption.id)}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
|
||||
@@ -33,7 +33,6 @@ export function NPSElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const isCurrent = element.id === currentElementId;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
|
||||
const handleClick = (number: number) => {
|
||||
@@ -74,7 +73,7 @@ export function NPSElement({
|
||||
return (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
|
||||
@@ -169,7 +169,7 @@ export function OpenTextElement({
|
||||
<input
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
name={element.id}
|
||||
id={element.id}
|
||||
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
||||
@@ -195,7 +195,7 @@ export function OpenTextElement({
|
||||
rows={3}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
name={element.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
aria-label="textarea"
|
||||
id={element.id}
|
||||
placeholder={getLocalizedValue(element.placeholder, languageCode, true)}
|
||||
|
||||
@@ -148,7 +148,7 @@ export function PictureSelectionElement({
|
||||
<div className="fb-relative" key={choice.id}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={() => handleChange(choice.id)}
|
||||
className={getButtonClassName(choice.id)}>
|
||||
|
||||
@@ -159,7 +159,7 @@ export function RankingElement({
|
||||
)}>
|
||||
<button
|
||||
autoFocus={idx === 0 && autoFocusEnabled}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -46,7 +46,6 @@ export function RatingElement({
|
||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const isCurrent = element.id === currentElementId;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
|
||||
const handleSelect = (number: number) => {
|
||||
@@ -55,23 +54,6 @@ export function RatingElement({
|
||||
setTtc(updatedTtcObj);
|
||||
};
|
||||
|
||||
function HiddenRadioInput({ number, id }: { number: number; id?: string }) {
|
||||
return (
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-invisible fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleSelect(number);
|
||||
}}
|
||||
required={element.required}
|
||||
checked={value === number}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setHoveredNumber(0);
|
||||
}, [element.id, setHoveredNumber]);
|
||||
@@ -97,14 +79,6 @@ export function RatingElement({
|
||||
setTtc(updatedTtcObj);
|
||||
};
|
||||
|
||||
const handleKeyDown = (number: number) => (e: KeyboardEvent) => {
|
||||
const isActivationKey = e.key === " " || e.key === "Enter";
|
||||
if (isActivationKey) {
|
||||
e.preventDefault();
|
||||
handleSelect(number);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseOver = (number: number) => () => {
|
||||
setHoveredNumber(number);
|
||||
};
|
||||
@@ -160,10 +134,21 @@ export function RatingElement({
|
||||
);
|
||||
};
|
||||
|
||||
const getRatingInputId = (number: number) => `${element.id}-${number}`;
|
||||
|
||||
const handleKeyDown = (number: number) => (e: KeyboardEvent) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
const inputId = getRatingInputId(number);
|
||||
document.getElementById(inputId)?.click();
|
||||
document.getElementById(inputId)?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const renderNumberScale = (number: number, totalLength: number) => {
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
onKeyDown={handleKeyDown(number)}
|
||||
className={getNumberLabelClassName(number, totalLength)}>
|
||||
{element.isColorCodingEnabled && (
|
||||
@@ -171,7 +156,19 @@ export function RatingElement({
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(element.range, number)}`}
|
||||
/>
|
||||
)}
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<input
|
||||
type="radio"
|
||||
id={getRatingInputId(number)}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleSelect(number);
|
||||
}}
|
||||
required={element.required}
|
||||
checked={value === number}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
{number}
|
||||
</label>
|
||||
);
|
||||
@@ -180,12 +177,25 @@ export function RatingElement({
|
||||
const renderStarScale = (number: number) => {
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={`Rate ${number} out of ${element.range}`}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
onKeyDown={handleKeyDown(number)}
|
||||
className={getStarLabelClassName(number)}
|
||||
onFocus={handleFocus(number)}
|
||||
onBlur={handleBlur}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<input
|
||||
type="radio"
|
||||
id={getRatingInputId(number)}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleSelect(number);
|
||||
}}
|
||||
required={element.required}
|
||||
checked={value === number}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
@@ -201,12 +211,25 @@ export function RatingElement({
|
||||
const renderSmileyScale = (number: number, idx: number) => {
|
||||
return (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={`Rate ${number} out of ${element.range}`}
|
||||
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
|
||||
className={getSmileyLabelClassName(number)}
|
||||
onKeyDown={handleKeyDown(number)}
|
||||
onFocus={handleFocus(number)}
|
||||
onBlur={handleBlur}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<input
|
||||
type="radio"
|
||||
id={getRatingInputId(number)}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleSelect(number);
|
||||
}}
|
||||
required={element.required}
|
||||
checked={value === number}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
@@ -254,7 +277,7 @@ export function RatingElement({
|
||||
renderRatingOption(number, i, a.length)
|
||||
)}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-gap-8">
|
||||
<div className="fb-text-subheading fb-mt-8 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-gap-8">
|
||||
<p className="fb-max-w-[50%]" dir="auto">
|
||||
{getLocalizedValue(element.lowerLabel, languageCode)}
|
||||
</p>
|
||||
|
||||
@@ -99,7 +99,7 @@ export function LanguageSwitch({
|
||||
{showLanguageDropdown ? (
|
||||
<div
|
||||
className={cn(
|
||||
"fb-bg-input-bg fb-text-heading fb-absolute fb-top-10 fb-max-h-64 fb-space-y-2 fb-overflow-auto fb-rounded-md fb-p-2 fb-text-xs",
|
||||
"fb-bg-input-bg fb-text-heading fb-absolute fb-top-10 fb-max-h-64 fb-space-y-2 fb-overflow-auto fb-rounded-md fb-p-2 fb-text-xs fb-border-border fb-border",
|
||||
dir === "rtl" ? "fb-left-8" : "fb-right-8"
|
||||
)}
|
||||
ref={languageDropdownRef}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { ZColor, ZPlacement } from "./common";
|
||||
import { ZEnvironment } from "./environment";
|
||||
import { ZBaseStyling } from "./styling";
|
||||
import { ZBaseStyling, ZLogo } from "./styling";
|
||||
|
||||
export const ZProjectStyling = ZBaseStyling.extend({
|
||||
allowStyleOverwrite: z.boolean(),
|
||||
@@ -46,11 +46,6 @@ export const ZLanguageUpdate = z.object({
|
||||
});
|
||||
export type TLanguageUpdate = z.infer<typeof ZLanguageUpdate>;
|
||||
|
||||
export const ZLogo = z.object({
|
||||
url: z.string().optional(),
|
||||
bgColor: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TLogo = z.infer<typeof ZLogo>;
|
||||
|
||||
export const ZProject = z.object({
|
||||
|
||||
@@ -15,6 +15,12 @@ export const ZCardArrangement = z.object({
|
||||
appSurveys: ZCardArrangementOptions,
|
||||
});
|
||||
|
||||
export const ZLogo = z.object({
|
||||
url: z.string().optional(),
|
||||
bgColor: z.string().optional(),
|
||||
});
|
||||
export type TLogo = z.infer<typeof ZLogo>;
|
||||
|
||||
export const ZSurveyStylingBackground = z
|
||||
.object({
|
||||
bg: z.string().nullish(),
|
||||
@@ -48,6 +54,7 @@ export const ZBaseStyling = z.object({
|
||||
background: ZSurveyStylingBackground.nullish(),
|
||||
hideProgressBar: z.boolean().nullish(),
|
||||
isLogoHidden: z.boolean().nullish(),
|
||||
logo: ZLogo.nullish(),
|
||||
});
|
||||
|
||||
export type TBaseStyling = z.infer<typeof ZBaseStyling>;
|
||||
|
||||
@@ -1355,7 +1355,10 @@ export const ZSurvey = z
|
||||
}
|
||||
}
|
||||
|
||||
//only validate back button label for blocks other than the first one and if back button is not hidden
|
||||
if (
|
||||
!isBackButtonHidden &&
|
||||
blockIndex > 0 &&
|
||||
block.backButtonLabel?.[defaultLanguageCode] &&
|
||||
block.backButtonLabel[defaultLanguageCode].trim() !== ""
|
||||
) {
|
||||
|
||||
Generated
+607
-591
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user