fix: resolve metadata in hover confusion + other UI tweaks (#6821)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Johannes
2025-11-17 03:51:49 -08:00
committed by GitHub
parent 35d0d8ed54
commit 5741209aa9
38 changed files with 291 additions and 473 deletions

View File

@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -16,7 +15,6 @@ interface AirtableWrapperProps {
airtableArray: TIntegrationItem[];
airtableIntegration?: TIntegrationAirtable;
surveys: TSurvey[];
environment: TEnvironment;
isEnabled: boolean;
webAppUrl: string;
locale: TUserLocale;
@@ -27,7 +25,6 @@ export const AirtableWrapper = ({
airtableArray,
airtableIntegration,
surveys,
environment,
isEnabled,
webAppUrl,
locale,
@@ -48,7 +45,6 @@ export const AirtableWrapper = ({
<ManageIntegration
airtableArray={airtableArray}
environmentId={environmentId}
environment={environment}
airtableIntegration={airtableIntegration}
setIsConnected={setIsConnected}
surveys={surveys}

View File

@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -15,12 +14,11 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
import { IntegrationModalInputs } from "../lib/types";
interface ManageIntegrationProps {
airtableIntegration: TIntegrationAirtable;
environment: TEnvironment;
environmentId: string;
setIsConnected: (data: boolean) => void;
surveys: TSurvey[];
@@ -29,7 +27,7 @@ interface ManageIntegrationProps {
}
export const ManageIntegration = (props: ManageIntegrationProps) => {
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
const { airtableIntegration, environmentId, setIsConnected, surveys, airtableArray } = props;
const { t } = useTranslation();
const tableHeaders = [
@@ -132,12 +130,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
</div>
) : (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.airtable.no_integrations_yet")}
/>
<EmptyState text={t("environments.integrations.airtable.no_integrations_yet")} />
</div>
)}

View File

@@ -51,7 +51,6 @@ const Page = async (props) => {
airtableArray={airtableArray}
environmentId={environment.id}
surveys={surveys}
environment={environment}
webAppUrl={WEBAPP_URL}
locale={locale}
/>

View File

@@ -60,7 +60,6 @@ export const GoogleSheetWrapper = ({
selectedIntegration={selectedIntegration}
/>
<ManageIntegration
environment={environment}
googleSheetIntegration={googleSheetIntegration}
setOpenAddIntegrationModal={setIsModalOpen}
setIsConnected={setIsConnected}

View File

@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
@@ -15,10 +14,9 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
interface ManageIntegrationProps {
environment: TEnvironment;
googleSheetIntegration: TIntegrationGoogleSheets;
setOpenAddIntegrationModal: (v: boolean) => void;
setIsConnected: (v: boolean) => void;
@@ -27,7 +25,6 @@ interface ManageIntegrationProps {
}
export const ManageIntegration = ({
environment,
googleSheetIntegration,
setOpenAddIntegrationModal,
setIsConnected,
@@ -90,12 +87,7 @@ export const ManageIntegration = ({
</div>
{!integrationArray || integrationArray.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.google_sheets.no_integrations_yet")}
/>
<EmptyState text={t("environments.integrations.google_sheets.no_integrations_yet")} />
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">

View File

@@ -4,7 +4,6 @@ import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
import { TUserLocale } from "@formbricks/types/user";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
@@ -12,11 +11,10 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface ManageIntegrationProps {
environment: TEnvironment;
notionIntegration: TIntegrationNotion;
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
setIsConnected: React.Dispatch<React.SetStateAction<boolean>>;
@@ -28,7 +26,6 @@ interface ManageIntegrationProps {
}
export const ManageIntegration = ({
environment,
notionIntegration,
setOpenAddIntegrationModal,
setIsConnected,
@@ -101,12 +98,7 @@ export const ManageIntegration = ({
</div>
{!integrationArray || integrationArray.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.notion.no_databases_found")}
/>
<EmptyState text={t("environments.integrations.notion.no_databases_found")} />
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">

View File

@@ -64,7 +64,6 @@ export const NotionWrapper = ({
selectedIntegration={selectedIntegration}
/>
<ManageIntegration
environment={environment}
notionIntegration={notionIntegration}
setOpenAddIntegrationModal={setIsModalOpen}
setIsConnected={setIsConnected}

View File

@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { Trans, useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
import { TUserLocale } from "@formbricks/types/user";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
@@ -12,10 +11,9 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
interface ManageIntegrationProps {
environment: TEnvironment;
slackIntegration: TIntegrationSlack;
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
setIsConnected: React.Dispatch<React.SetStateAction<boolean>>;
@@ -29,7 +27,6 @@ interface ManageIntegrationProps {
}
export const ManageIntegration = ({
environment,
slackIntegration,
setOpenAddIntegrationModal,
setIsConnected,
@@ -106,12 +103,7 @@ export const ManageIntegration = ({
</div>
{!integrationArray || integrationArray.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.slack.connect_your_first_slack_channel")}
/>
<EmptyState text={t("environments.integrations.slack.connect_your_first_slack_channel")} />
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">

View File

@@ -78,7 +78,6 @@ export const SlackWrapper = ({
selectedIntegration={selectedIntegration}
/>
<ManageIntegration
environment={environment}
slackIntegration={slackIntegration}
setOpenAddIntegrationModal={setIsModalOpen}
setIsConnected={setIsConnected}

View File

@@ -3,9 +3,13 @@
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TI18nString, TSurveyQuestionId, TSurveySummary } from "@formbricks/types/surveys/types";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TSurvey } from "@formbricks/types/surveys/types";
import {
TI18nString,
TSurvey,
TSurveyQuestionId,
TSurveyQuestionTypeEnum,
TSurveySummary,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import {
SelectedFilterValue,
@@ -29,7 +33,7 @@ import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
import { AddressSummary } from "./AddressSummary";
@@ -103,12 +107,7 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
) : summary.length === 0 ? (
<SkeletonLoader type="summary" />
) : responseCount === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
emptyMessage={t("environments.surveys.summary.no_responses_found")}
/>
<EmptyState text={t("environments.surveys.summary.no_responses_found")} />
) : (
summary.map((questionSummary) => {
if (questionSummary.type === TSurveyQuestionTypeEnum.OpenText) {

View File

@@ -811,7 +811,6 @@ checksums:
environments/project/tags/add_tag: 2cfa04ceea966149f2b5d40d9c131141
environments/project/tags/count: 9c5848662eb8024ddf360f7e4001a968
environments/project/tags/delete_tag_confirmation: a9fb98064cd156242899643f3d2ef032
environments/project/tags/empty_message: da71bd7c7b5bf634469d20e010d25503
environments/project/tags/manage_tags: 2761d558b82b6104befbc240ae2379c6
environments/project/tags/manage_tags_description: ce7cc42da3646fba960502d7e4e49cd2
environments/project/tags/merge: 95051c859b8778be51226b43be6f1075
@@ -1600,7 +1599,7 @@ checksums:
environments/surveys/responses/last_name: 2c9a7de7738ca007ba9023c385149c26
environments/surveys/responses/not_completed: df34eab65a6291f2c5e15a0e349c4eba
environments/surveys/responses/os: a4c753bb2c004a58d02faeed6b4da476
environments/surveys/responses/person_attributes: 8f7f8a9040ce8efb3cb54ce33b590866
environments/surveys/responses/person_attributes: 07ae67ae73d7a2a7c67008694a83f0a3
environments/surveys/responses/phone: b9537ee90fc5b0116942e0af29d926cc
environments/surveys/responses/respondent_skipped_questions: d85daf579ade534dc7e639689156fcd5
environments/surveys/responses/response_deleted_successfully: 6cec5427c271800619fee8c812d7db18
@@ -1718,7 +1717,6 @@ checksums:
environments/surveys/summary/filtered_responses_csv: aad66a98be6a09cac8bef9e4db4a75cf
environments/surveys/summary/filtered_responses_excel: 06e57bae9e41979fd7fc4b8bfe3466f9
environments/surveys/summary/generating_qr_code: 5026d4a76f995db458195e5215d9bbd9
environments/surveys/summary/go_to_setup_checklist: d70bd018d651d01c41ae10370e71d0be
environments/surveys/summary/impressions: 7fe38d42d68a64d3fd8436a063751584
environments/surveys/summary/impressions_tooltip: 4d0823cbf360304770c7c5913e33fdc8
environments/surveys/summary/in_app/connection_description: 9710bbf8048a8a5c3b2b56db9d946b73
@@ -1750,7 +1748,6 @@ checksums:
environments/surveys/summary/in_app/title: a2d1b633244d0e0504ec6f8f561c7a6b
environments/surveys/summary/includes_all: b0e3679282417c62d511c258362f860e
environments/surveys/summary/includes_either: 186d6923c1693e80d7b664b8367d4221
environments/surveys/summary/install_widget: 55d403de32e3d0da7513ab199f1d1934
environments/surveys/summary/is_equal_to: f4aab30ef188eb25dcc0e392cf8e86bb
environments/surveys/summary/is_less_than: 6109d595ba21497c59b1c91d7fd09a13
environments/surveys/summary/last_30_days: a738894cfc5e592052f1e16787744568
@@ -1787,7 +1784,6 @@ checksums:
environments/surveys/summary/ttc_tooltip: 9b1cbe32cc81111314bd3b6fd050c2e7
environments/surveys/summary/unknown_question_type: e4152a7457d2b94f48dcc70aaba9922f
environments/surveys/summary/use_personal_links: da2b3e7e1aaf2ea2bd4efed2dda4247c
environments/surveys/summary/waiting_for_response: 0194a84e0850b8e98435632d5331a916
environments/surveys/summary/whats_next: d920145bfa2147014062f6f2d1d451a4
environments/surveys/summary/your_survey_is_public: 3f5cb5949a5f4020a3d4d74fdfc95e83
environments/surveys/summary/youre_not_plugged_in_yet: 9217467742cdcf7edf8d59cc1472ede6

View File

@@ -872,7 +872,6 @@
"add_tag": "Tag hinzufügen",
"count": "zählen",
"delete_tag_confirmation": "Bist Du sicher, dass Du diesen Tag löschen möchtest?",
"empty_message": "Markiere eine Antwort, um deine Liste der Tags hier zu finden.",
"manage_tags": "Tags verwalten",
"manage_tags_description": "Zusammenführen und Antwort-Tags entfernen.",
"merge": "Zusammenführen",
@@ -1691,7 +1690,7 @@
"last_name": "Nachname",
"not_completed": "Nicht abgeschlossen ⏳",
"os": "Betriebssystem",
"person_attributes": "Personenattribute",
"person_attributes": "Personenattribute zum Zeitpunkt der Einreichung",
"phone": "Telefon",
"respondent_skipped_questions": "Der Befragte hat diese Fragen übersprungen.",
"response_deleted_successfully": "Antwort erfolgreich gelöscht.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Gefilterte Antworten (CSV)",
"filtered_responses_excel": "Gefilterte Antworten (Excel)",
"generating_qr_code": "QR-Code wird generiert",
"go_to_setup_checklist": "Gehe zur Einrichtungs-Checkliste 👉",
"impressions": "Eindrücke",
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Beinhaltet alles",
"includes_either": "Beinhaltet entweder",
"install_widget": "Formbricks Widget installieren",
"is_equal_to": "Ist gleich",
"is_less_than": "ist weniger als",
"last_30_days": "Letzte 30 Tage",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Durchschnittliche Zeit zum Beantworten der Frage.",
"unknown_question_type": "Unbekannter Fragetyp",
"use_personal_links": "Nutze persönliche Links",
"waiting_for_response": "Warte auf eine Antwort 🧘‍♂️",
"whats_next": "Was kommt als Nächstes?",
"your_survey_is_public": "Deine Umfrage ist öffentlich",
"youre_not_plugged_in_yet": "Du bist noch nicht verbunden!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Add Tag",
"count": "Count",
"delete_tag_confirmation": "Are you sure you want to delete this tag?",
"empty_message": "Tag a submission to find your list of tags here.",
"manage_tags": "Manage Tags",
"manage_tags_description": "Merge and remove response tags.",
"merge": "Merge",
@@ -1691,7 +1690,7 @@
"last_name": "Last Name",
"not_completed": "Not Completed ⏳",
"os": "OS",
"person_attributes": "Person attributes",
"person_attributes": "Person attributes at time of submission",
"phone": "Phone",
"respondent_skipped_questions": "Respondent skipped these questions.",
"response_deleted_successfully": "Response deleted successfully.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Filtered responses (CSV)",
"filtered_responses_excel": "Filtered responses (Excel)",
"generating_qr_code": "Generating QR code",
"go_to_setup_checklist": "Go to Setup Checklist \uD83D\uDC49",
"impressions": "Impressions",
"impressions_tooltip": "Number of times the survey has been viewed.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Includes all",
"includes_either": "Includes either",
"install_widget": "Install Formbricks Widget",
"is_equal_to": "Is equal to",
"is_less_than": "Is less than",
"last_30_days": "Last 30 days",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Average time to complete the question.",
"unknown_question_type": "Unknown Question Type",
"use_personal_links": "Use personal links",
"waiting_for_response": "Waiting for a response \uD83E\uDDD8",
"whats_next": "What's next?",
"your_survey_is_public": "Your survey is public",
"youre_not_plugged_in_yet": "You're not plugged in yet!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Añadir etiqueta",
"count": "Recuento",
"delete_tag_confirmation": "¿Estás seguro de que quieres eliminar esta etiqueta?",
"empty_message": "Etiqueta un envío para encontrar tu lista de etiquetas aquí.",
"manage_tags": "Gestionar etiquetas",
"manage_tags_description": "Fusionar y eliminar etiquetas de respuesta.",
"merge": "Fusionar",
@@ -1691,7 +1690,7 @@
"last_name": "Apellido",
"not_completed": "No completado ⏳",
"os": "Sistema operativo",
"person_attributes": "Atributos de persona",
"person_attributes": "Atributos de la persona en el momento del envío",
"phone": "Teléfono",
"respondent_skipped_questions": "El encuestado omitió estas preguntas.",
"response_deleted_successfully": "Respuesta eliminada correctamente.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Respuestas filtradas (CSV)",
"filtered_responses_excel": "Respuestas filtradas (Excel)",
"generating_qr_code": "Generando código QR",
"go_to_setup_checklist": "Ir a la lista de configuración 👉",
"impressions": "Impresiones",
"impressions_tooltip": "Número de veces que se ha visto la encuesta.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Incluye todo",
"includes_either": "Incluye cualquiera",
"install_widget": "Instalar widget de Formbricks",
"is_equal_to": "Es igual a",
"is_less_than": "Es menor que",
"last_30_days": "Últimos 30 días",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Tiempo medio para completar la pregunta.",
"unknown_question_type": "Tipo de pregunta desconocido",
"use_personal_links": "Usar enlaces personales",
"waiting_for_response": "Esperando una respuesta 🧘‍♂️",
"whats_next": "¿Qué sigue?",
"your_survey_is_public": "Tu encuesta es pública",
"youre_not_plugged_in_yet": "¡Aún no estás conectado!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Ajouter une étiquette",
"count": "Compter",
"delete_tag_confirmation": "Êtes-vous sûr de vouloir supprimer cette étiquette ?",
"empty_message": "Ajoutez une balise à une réponse pour afficher votre liste de balises.",
"manage_tags": "Gérer les étiquettes",
"manage_tags_description": "Vous pouvez fusionner et supprimer des balises de réponse.",
"merge": "Fusionner",
@@ -1691,7 +1690,7 @@
"last_name": "Nom de famille",
"not_completed": "Non terminé ⏳",
"os": "Système d'exploitation",
"person_attributes": "Attributs de la personne",
"person_attributes": "Attributs de la personne au moment de la soumission",
"phone": "Téléphone",
"respondent_skipped_questions": "Le répondant a sauté ces questions.",
"response_deleted_successfully": "Réponse supprimée avec succès.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Réponses filtrées (CSV)",
"filtered_responses_excel": "Réponses filtrées (Excel)",
"generating_qr_code": "Génération du code QR",
"go_to_setup_checklist": "Allez à la liste de contrôle de configuration 👉",
"impressions": "Impressions",
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Comprend tous",
"includes_either": "Comprend soit",
"install_widget": "Installer le widget Formbricks",
"is_equal_to": "Est égal à",
"is_less_than": "est inférieur à",
"last_30_days": "30 derniers jours",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Temps moyen pour compléter la question.",
"unknown_question_type": "Type de question inconnu",
"use_personal_links": "Utilisez des liens personnels",
"waiting_for_response": "En attente d'une réponse 🧘‍♂️",
"whats_next": "Qu'est-ce qui vient ensuite ?",
"your_survey_is_public": "Votre enquête est publique.",
"youre_not_plugged_in_yet": "Vous n'êtes pas encore branché !"

View File

@@ -872,7 +872,6 @@
"add_tag": "タグを追加",
"count": "件数",
"delete_tag_confirmation": "このタグを削除してもよろしいですか?",
"empty_message": "送信にタグ付けすると、ここにタグ一覧が表示されます。",
"manage_tags": "タグを管理",
"manage_tags_description": "回答タグを統合・削除します。",
"merge": "統合",
@@ -1691,7 +1690,7 @@
"last_name": "姓",
"not_completed": "未完了 ⏳",
"os": "OS",
"person_attributes": "人属性",
"person_attributes": "回答時の個人属性",
"phone": "電話",
"respondent_skipped_questions": "回答者はこれらの質問をスキップしました。",
"response_deleted_successfully": "回答を正常に削除しました。",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "フィルター済み回答 (CSV)",
"filtered_responses_excel": "フィルター済み回答 (Excel)",
"generating_qr_code": "QRコードを生成中",
"go_to_setup_checklist": "セットアップチェックリストへ移動 👉",
"impressions": "表示回数",
"impressions_tooltip": "フォームが表示された回数。",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "すべてを含む",
"includes_either": "どちらかを含む",
"install_widget": "Formbricksウィジェットをインストール",
"is_equal_to": "と等しい",
"is_less_than": "より小さい",
"last_30_days": "過去30日間",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "フォームを完了するまでの平均時間。",
"unknown_question_type": "不明な質問の種類",
"use_personal_links": "個人リンクを使用",
"waiting_for_response": "回答を待っています 🧘‍♂️",
"whats_next": "次は何をしますか?",
"your_survey_is_public": "あなたのフォームは公開されています",
"youre_not_plugged_in_yet": "まだ接続されていません!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Label toevoegen",
"count": "Graaf",
"delete_tag_confirmation": "Weet u zeker dat u deze tag wilt verwijderen?",
"empty_message": "Tag een inzending om hier uw lijst met tags te vinden.",
"manage_tags": "Beheer tags",
"manage_tags_description": "Reactietags samenvoegen en verwijderen.",
"merge": "Samenvoegen",
@@ -1691,7 +1690,7 @@
"last_name": "Achternaam",
"not_completed": "Niet voltooid ⏳",
"os": "Besturingssysteem",
"person_attributes": "Persoonsattributen",
"person_attributes": "Persoonskenmerken op het moment van indiening",
"phone": "Telefoon",
"respondent_skipped_questions": "Respondent heeft deze vragen overgeslagen.",
"response_deleted_successfully": "Reactie is succesvol verwijderd.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Gefilterde reacties (CSV)",
"filtered_responses_excel": "Gefilterde reacties (Excel)",
"generating_qr_code": "QR-code genereren",
"go_to_setup_checklist": "Ga naar Installatiechecklist 👉",
"impressions": "Indrukken",
"impressions_tooltip": "Aantal keren dat de enquête is bekeken.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Inclusief alles",
"includes_either": "Inclusief beide",
"install_widget": "Installeer Formbricks-widget",
"is_equal_to": "Is gelijk aan",
"is_less_than": "Is minder dan",
"last_30_days": "Laatste 30 dagen",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Gemiddelde tijd om de vraag te beantwoorden.",
"unknown_question_type": "Onbekend vraagtype",
"use_personal_links": "Gebruik persoonlijke links",
"waiting_for_response": "Wachten op een reactie 🧘‍♂️",
"whats_next": "Wat is het volgende?",
"your_survey_is_public": "Uw enquête is openbaar",
"youre_not_plugged_in_yet": "Je bent nog niet aangesloten!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Adicionar Tag",
"count": "Contar",
"delete_tag_confirmation": "Tem certeza de que quer deletar essa tag?",
"empty_message": "Marque uma submissão para encontrar sua lista de tags aqui.",
"manage_tags": "Gerenciar Tags",
"manage_tags_description": "Mesclar e remover tags de resposta.",
"merge": "mesclar",
@@ -1691,7 +1690,7 @@
"last_name": "Sobrenome",
"not_completed": "Não Concluído ⏳",
"os": "sistema operacional",
"person_attributes": "Atributos da pessoa",
"person_attributes": "Atributos da pessoa no momento do envio",
"phone": "Celular",
"respondent_skipped_questions": "Respondente pulou essas perguntas.",
"response_deleted_successfully": "Resposta deletada com sucesso.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Respostas filtradas (CSV)",
"filtered_responses_excel": "Respostas filtradas (Excel)",
"generating_qr_code": "Gerando código QR",
"go_to_setup_checklist": "Vai para a Lista de Configuração 👉",
"impressions": "Impressões",
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Inclui tudo",
"includes_either": "Inclui ou",
"install_widget": "Instalar Widget do Formbricks",
"is_equal_to": "É igual a",
"is_less_than": "É menor que",
"last_30_days": "Últimos 30 dias",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Tempo médio para completar a pergunta.",
"unknown_question_type": "Tipo de pergunta desconhecido",
"use_personal_links": "Use links pessoais",
"waiting_for_response": "Aguardando uma resposta 🧘‍♂️",
"whats_next": "E agora?",
"your_survey_is_public": "Sua pesquisa é pública",
"youre_not_plugged_in_yet": "Você ainda não tá conectado!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Adicionar Etiqueta",
"count": "Contagem",
"delete_tag_confirmation": "Tem a certeza de que deseja eliminar esta etiqueta?",
"empty_message": "Crie etiquetas para as suas submissões e veja-as aqui",
"manage_tags": "Gerir Etiquetas",
"manage_tags_description": "Junte e remova etiquetas de resposta",
"merge": "Fundir",
@@ -1691,7 +1690,7 @@
"last_name": "Apelido",
"not_completed": "Não Concluído ⏳",
"os": "SO",
"person_attributes": "Atributos da pessoa",
"person_attributes": "Atributos da pessoa no momento da submissão",
"phone": "Telefone",
"respondent_skipped_questions": "O respondente saltou estas perguntas.",
"response_deleted_successfully": "Resposta eliminada com sucesso.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Respostas filtradas (CSV)",
"filtered_responses_excel": "Respostas filtradas (Excel)",
"generating_qr_code": "A gerar código QR",
"go_to_setup_checklist": "Ir para a Lista de Verificação de Configuração 👉",
"impressions": "Impressões",
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Inclui tudo",
"includes_either": "Inclui qualquer um",
"install_widget": "Instalar Widget Formbricks",
"is_equal_to": "É igual a",
"is_less_than": "É menos que",
"last_30_days": "Últimos 30 dias",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Tempo médio para concluir a pergunta.",
"unknown_question_type": "Tipo de Pergunta Desconhecido",
"use_personal_links": "Utilize links pessoais",
"waiting_for_response": "A aguardar uma resposta 🧘‍♂️",
"whats_next": "O que se segue?",
"your_survey_is_public": "O seu inquérito é público",
"youre_not_plugged_in_yet": "Ainda não está ligado!"

View File

@@ -872,7 +872,6 @@
"add_tag": "Adaugă Etichetă",
"count": "Număr",
"delete_tag_confirmation": "Sigur doriți să ștergeți această etichetă?",
"empty_message": "Marcați o trimitere pentru a găsi lista de etichete aici.",
"manage_tags": "Gestionați etichetele",
"manage_tags_description": "Îmbinați și eliminați etichetele de răspuns.",
"merge": "Îmbinare",
@@ -1691,7 +1690,7 @@
"last_name": "Nume de familie",
"not_completed": "Necompletat ⏳",
"os": "SO",
"person_attributes": "Atribute persoană",
"person_attributes": "Atributele persoanei la momentul trimiterii",
"phone": "Telefon",
"respondent_skipped_questions": "Respondenții au sărit peste aceste întrebări.",
"response_deleted_successfully": "Răspuns șters cu succes.",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "Răspunsuri filtrate (CSV)",
"filtered_responses_excel": "Răspunsuri filtrate (Excel)",
"generating_qr_code": "Se generează codul QR",
"go_to_setup_checklist": "Mergi la lista de verificare a configurării 👉",
"impressions": "Impresii",
"impressions_tooltip": "Număr de ori când sondajul a fost vizualizat.",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "Include tot",
"includes_either": "Include fie",
"install_widget": "Instalați Widgetul Formbricks",
"is_equal_to": "Este egal cu",
"is_less_than": "Este mai puțin de",
"last_30_days": "Ultimele 30 de zile",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "Timp mediu pentru a completa întrebarea.",
"unknown_question_type": "Tip de întrebare necunoscut",
"use_personal_links": "Folosește linkuri personale",
"waiting_for_response": "Așteptând un răspuns 🧘‍♂️",
"whats_next": "Ce urmează?",
"your_survey_is_public": "Sondajul tău este public",
"youre_not_plugged_in_yet": "Nu sunteţi încă conectat!"

View File

@@ -872,7 +872,6 @@
"add_tag": "添加 标签",
"count": "数量",
"delete_tag_confirmation": "您 确定 要 删除 此 标签 吗?",
"empty_message": "标记一个提交以在此处找到您的标签列表。",
"manage_tags": "管理标签",
"manage_tags_description": "合并 和 删除 response 标签。",
"merge": "合并",
@@ -1691,7 +1690,7 @@
"last_name": "姓",
"not_completed": "未完成 ⏳",
"os": "操作系统",
"person_attributes": "人属性",
"person_attributes": "提交时的个人属性",
"phone": "电话",
"respondent_skipped_questions": "受访者跳过 这些问题。",
"response_deleted_successfully": "响应 删除 成功",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "过滤 反馈 CSV",
"filtered_responses_excel": "过滤 反馈 Excel",
"generating_qr_code": "正在生成二维码",
"go_to_setup_checklist": "前往 设置 检查列表 👉",
"impressions": "印象",
"impressions_tooltip": "调查 被 查看 的 次数",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "包括所有 ",
"includes_either": "包含 任意一个",
"install_widget": "安装 Formbricks 小组件",
"is_equal_to": "等于",
"is_less_than": "少于",
"last_30_days": "最近 30 天",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "完成 本 问题 的 平均 时间",
"unknown_question_type": "未知 问题 类型",
"use_personal_links": "使用 个人 链接",
"waiting_for_response": "等待回复 🧘‍♂️",
"whats_next": "接下来 是 什么?",
"your_survey_is_public": "您的 调查 是 公共 的",
"youre_not_plugged_in_yet": "您 还 没 有 连 接!"

View File

@@ -872,7 +872,6 @@
"add_tag": "新增標籤",
"count": "計數",
"delete_tag_confirmation": "您確定要刪除此標籤嗎?",
"empty_message": "標記提交內容,在此處找到您的標籤清單。",
"manage_tags": "管理標籤",
"manage_tags_description": "合併和移除回應標籤。",
"merge": "合併",
@@ -1691,7 +1690,7 @@
"last_name": "姓氏",
"not_completed": "未完成 ⏳",
"os": "作業系統",
"person_attributes": "人屬性",
"person_attributes": "提交時的個人屬性",
"phone": "電話",
"respondent_skipped_questions": "回應者跳過這些問題。",
"response_deleted_successfully": "回應已成功刪除。",
@@ -1827,7 +1826,6 @@
"filtered_responses_csv": "篩選回應 (CSV)",
"filtered_responses_excel": "篩選回應 (Excel)",
"generating_qr_code": "正在生成 QR code",
"go_to_setup_checklist": "前往設定檢查清單 👉",
"impressions": "曝光數",
"impressions_tooltip": "問卷已檢視的次數。",
"in_app": {
@@ -1861,7 +1859,6 @@
},
"includes_all": "包含全部",
"includes_either": "包含其中一個",
"install_widget": "安裝 Formbricks 小工具",
"is_equal_to": "等於",
"is_less_than": "小於",
"last_30_days": "過去 30 天",
@@ -1898,7 +1895,6 @@
"ttc_tooltip": "完成 問題 的 平均 時間。",
"unknown_question_type": "未知的問題類型",
"use_personal_links": "使用 個人 連結",
"waiting_for_response": "正在等待回應 🧘‍♂️",
"whats_next": "下一步是什麼?",
"your_survey_is_public": "您的問卷是公開的",
"youre_not_plugged_in_yet": "您尚未插入任何內容!"

View File

@@ -1,18 +1,18 @@
"use client";
import { SettingsIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { TResponse } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { TUserLocale } from "@formbricks/types/user";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TagError } from "@/modules/projects/settings/types/tag";
import { Button } from "@/modules/ui/components/button";
import { Tag } from "@/modules/ui/components/tag";
import { TagsCombobox } from "@/modules/ui/components/tags-combobox";
import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions";
import { SingleResponseCardMetadata } from "./SingleResponseCardMetadata";
interface ResponseTagsWrapperProps {
tags: {
@@ -24,6 +24,8 @@ interface ResponseTagsWrapperProps {
environmentTags: TTag[];
updateFetchedResponses: () => void;
isReadOnly?: boolean;
response: TResponse;
locale: TUserLocale;
}
export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
@@ -33,9 +35,10 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
environmentTags,
updateFetchedResponses,
isReadOnly,
response,
locale,
}) => {
const { t } = useTranslation();
const router = useRouter();
const [searchValue, setSearchValue] = useState("");
const [open, setOpen] = React.useState(false);
const [tagsState, setTagsState] = useState(tags);
@@ -79,7 +82,6 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
if (errorMessage?.code === TagError.TAG_NAME_ALREADY_EXISTS) {
toast.error(t("environments.surveys.responses.tag_already_exists"), {
duration: 2000,
icon: <SettingsIcon className="h-5 w-5 text-orange-500" />,
});
} else {
toast.error(t("environments.surveys.responses.an_error_occurred_creating_the_tag"));
@@ -131,6 +133,7 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
return (
<div className="flex items-center justify-between gap-4 border-t border-slate-200 px-6 py-3">
<div className="flex flex-wrap items-center gap-2">
<SingleResponseCardMetadata response={response} locale={locale} />
{tagsState?.map((tag) => (
<Tag
key={tag.tagId}
@@ -157,18 +160,6 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
/>
)}
</div>
{!isReadOnly && (
<Button
variant="ghost"
size="sm"
className="flex-shrink-0"
onClick={() => {
router.push(`/environments/${environmentId}/project/tags`);
}}>
<SettingsIcon className="h-4 w-4" />
</Button>
)}
</div>
);
};

View File

@@ -1,10 +1,8 @@
"use client";
import { LanguagesIcon, TrashIcon } from "lucide-react";
import { TrashIcon } from "lucide-react";
import Link from "next/link";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -12,17 +10,12 @@ import { TUser, TUserLocale } from "@formbricks/types/user";
import { timeSince } from "@/lib/time";
import { getContactIdentifier } from "@/lib/utils/contact";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { isSubmissionTimeMoreThan5Minutes } from "../util";
interface TooltipRendererProps {
shouldRender: boolean;
tooltipContent: ReactNode;
children: ReactNode;
}
interface SingleResponseCardHeaderProps {
pageType: "people" | "response";
response: TResponse;
@@ -54,140 +47,40 @@ export const SingleResponseCardHeader = ({
? true
: isSubmissionTimeMoreThan5Minutes(response.updatedAt);
const TooltipRenderer = ({ children, shouldRender, tooltipContent }: TooltipRendererProps) => {
return shouldRender ? (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent avoidCollisions align="start" side="bottom" className="max-w-[75vw]">
{tooltipContent}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<>{children}</>
);
};
const renderTooltip = Boolean(
(response.contactAttributes && Object.keys(response.contactAttributes).length > 0) ||
(response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0)
);
const tooltipContent = (
<>
{response.singleUseId && (
<div>
<p className="py-1 font-bold text-slate-700">
{t("environments.surveys.responses.single_use_id")}:
</p>
<span>{response.singleUseId}</span>
</div>
)}
{response.contactAttributes && Object.keys(response.contactAttributes).length > 0 && (
<div>
<p className="py-1 font-bold text-slate-700">
{t("environments.surveys.responses.person_attributes")}:
</p>
{Object.keys(response.contactAttributes).map((key) => (
<p
key={key}
className="truncate"
title={`${key}: ${response.contactAttributes && response.contactAttributes[key]}`}>
{key}:{" "}
<span className="font-bold">
{response.contactAttributes && response.contactAttributes[key]}
</span>
</p>
))}
</div>
)}
{response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0 && (
<div className="text-slate-600">
{response.contactAttributes && Object.keys(response.contactAttributes).length > 0 && (
<hr className="my-2 border-slate-200" />
)}
<p className="py-1 font-bold text-slate-700">{t("environments.surveys.responses.device_info")}:</p>
{response.meta.userAgent?.browser && (
<p className="truncate" title={`Browser: ${response.meta.userAgent.browser}`}>
{t("environments.surveys.responses.browser")}: {response.meta.userAgent.browser}
</p>
)}
{response.meta.userAgent?.os && (
<p className="truncate" title={`OS: ${response.meta.userAgent.os}`}>
{t("environments.surveys.responses.os")}: {response.meta.userAgent.os}
</p>
)}
{response.meta.userAgent && (
<p
className="truncate"
title={`Device: ${response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}`}>
{t("environments.surveys.responses.device")}:{" "}
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
</p>
)}
{response.meta.url && (
<p className="truncate" title={`URL: ${response.meta.url}`}>
{t("common.url")}: {response.meta.url}
</p>
)}
{response.meta.action && (
<p className="truncate" title={`Action: ${response.meta.action}`}>
{t("common.action")}: {response.meta.action}
</p>
)}
{response.meta.source && (
<p className="truncate" title={`Source: ${response.meta.source}`}>
{t("environments.surveys.responses.source")}: {response.meta.source}
</p>
)}
{response.meta.country && (
<p className="truncate" title={`Country: ${response.meta.country}`}>
{t("environments.surveys.responses.country")}: {response.meta.country}
</p>
)}
</div>
)}
</>
);
const deleteSubmissionToolTip = <>{t("environments.surveys.responses.this_response_is_in_progress")}</>;
return (
<div className="space-y-2 border-b border-slate-200 px-6 pb-4 pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-center space-x-4">
<div className="flex items-center justify-center space-x-2">
{pageType === "response" && (
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
<div className="group">
{response.contact?.id ? (
user ? (
<Link
className="flex items-center space-x-2"
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
{displayIdentifier}
</h3>
{response.contact.userId && <IdBadge id={response.contact.userId} />}
</Link>
) : (
<div className="flex items-center space-x-2">
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
{displayIdentifier}
</h3>
{response.contact.userId && <IdBadge id={response.contact.userId} />}
</div>
)
<>
{response.contact?.id ? (
user ? (
<Link
className="flex items-center space-x-2"
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
{displayIdentifier}
</h3>
</Link>
) : (
<div className="flex items-center">
<PersonAvatar personId="anonymous" />
<h3 className="ml-4 pb-1 font-semibold text-slate-600">{t("common.anonymous")}</h3>
<div className="flex items-center space-x-2">
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
{displayIdentifier}
</h3>
</div>
)}
</div>
</TooltipRenderer>
)
) : (
<div className="flex items-center">
<PersonAvatar personId="anonymous" />
<h3 className="ml-4 pb-1 font-semibold text-slate-600">{t("common.anonymous")}</h3>
</div>
)}
{response.contact?.userId && <IdBadge id={response.contact.userId} />}
</>
)}
{pageType === "people" && (
@@ -202,34 +95,34 @@ export const SingleResponseCardHeader = ({
</Link>
</div>
)}
{response.language && response.language !== "default" && (
<div className="flex space-x-2 rounded-full bg-slate-700 px-2 py-1 text-xs text-white">
<div>{getLanguageLabel(response.language, locale)}</div>
<LanguagesIcon className="h-4 w-4" />
</div>
)}
</div>
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center space-x-2 text-sm">
<time className="text-slate-500" dateTime={timeSince(response.createdAt.toISOString(), locale)}>
{timeSince(response.createdAt.toISOString(), locale)}
</time>
{user &&
!isReadOnly &&
(canResponseBeDeleted ? (
<TrashIcon
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteDialogOpen(true)}
className="h-4 w-4 cursor-pointer text-slate-500 hover:text-red-700"
aria-label="Delete response"
/>
aria-label="Delete response">
<TrashIcon className="h-4 w-4" />
</Button>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TrashIcon
className="h-4 w-4 cursor-not-allowed text-slate-400"
aria-label="Cannot delete response in progress"
/>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled
className="text-slate-400"
aria-label="Cannot delete response in progress">
<TrashIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">{deleteSubmissionToolTip}</TooltipContent>
</Tooltip>

View File

@@ -0,0 +1,163 @@
"use client";
import { LanguagesIcon, LucideIcon, MonitorIcon, SmartphoneIcon, Tag } from "lucide-react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
import { TResponse } from "@formbricks/types/responses";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@/modules/ui/components/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface InfoIconButtonProps {
icon: LucideIcon;
tooltipContent: ReactNode;
ariaLabel: string;
maxWidth?: string;
}
const InfoIconButton = ({
icon: Icon,
tooltipContent,
ariaLabel,
maxWidth = "max-w-[75vw]",
}: InfoIconButtonProps) => {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" aria-label={ariaLabel}>
<Icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent avoidCollisions align="start" side="bottom" className={maxWidth}>
{tooltipContent}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
interface SingleResponseCardMetadataProps {
response: TResponse;
locale: TUserLocale;
}
export const SingleResponseCardMetadata = ({ response, locale }: SingleResponseCardMetadataProps) => {
const { t } = useTranslation();
const hasContactAttributes =
response.contactAttributes && Object.keys(response.contactAttributes).length > 0;
const hasUserAgent = response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0;
const hasLanguage = response.language && response.language !== "default";
if (!hasContactAttributes && !hasUserAgent && !hasLanguage) {
return null;
}
const userAgentDeviceIcon = (() => {
if (!hasUserAgent || !response.meta.userAgent?.device) return MonitorIcon;
const device = response.meta.userAgent.device.toLowerCase();
return device.includes("mobile") || device.includes("phone") ? SmartphoneIcon : MonitorIcon;
})();
const contactAttributesTooltipContent = hasContactAttributes ? (
<div>
{response.singleUseId && (
<div className="mb-2">
<p className="py-1 font-semibold text-slate-700">
{t("environments.surveys.responses.single_use_id")}
</p>
<span>{response.singleUseId}</span>
</div>
)}
<p className="py-1 font-semibold text-slate-700">
{t("environments.surveys.responses.person_attributes")}
</p>
{Object.keys(response.contactAttributes || {}).map((key) => (
<p key={key} className="truncate" title={`${key}: ${response.contactAttributes?.[key]}`}>
{key}: {response.contactAttributes?.[key]}
</p>
))}
</div>
) : null;
const userAgentTooltipContent = hasUserAgent ? (
<div className="text-slate-600">
<p className="py-1 font-semibold text-slate-700">{t("environments.surveys.responses.device_info")}</p>
{response.meta.userAgent?.browser && (
<p className="truncate" title={`Browser: ${response.meta.userAgent.browser}`}>
{t("environments.surveys.responses.browser")}: {response.meta.userAgent.browser}
</p>
)}
{response.meta.userAgent?.os && (
<p className="truncate" title={`OS: ${response.meta.userAgent.os}`}>
{t("environments.surveys.responses.os")}: {response.meta.userAgent.os}
</p>
)}
{response.meta.userAgent && (
<p
className="truncate"
title={`Device: ${response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}`}>
{t("environments.surveys.responses.device")}:{" "}
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
</p>
)}
{response.meta.url && (
<p className="break-all" title={`URL: ${response.meta.url}`}>
{t("common.url")}: {response.meta.url}
</p>
)}
{response.meta.action && (
<p className="truncate" title={`Action: ${response.meta.action}`}>
{t("common.action")}: {response.meta.action}
</p>
)}
{response.meta.source && (
<p className="truncate" title={`Source: ${response.meta.source}`}>
{t("environments.surveys.responses.source")}: {response.meta.source}
</p>
)}
{response.meta.country && (
<p className="truncate" title={`Country: ${response.meta.country}`}>
{t("environments.surveys.responses.country")}: {response.meta.country}
</p>
)}
</div>
) : null;
const languageTooltipContent =
hasLanguage && response.language ? (
<div>
<p className="font-semibold text-slate-700">{t("common.language")}</p>
<p>{getLanguageLabel(response.language, locale)}</p>
</div>
) : null;
return (
<div className="flex items-center space-x-2">
{hasContactAttributes && contactAttributesTooltipContent && (
<InfoIconButton
icon={Tag}
tooltipContent={contactAttributesTooltipContent}
ariaLabel={t("environments.surveys.responses.person_attributes")}
/>
)}
{hasUserAgent && userAgentTooltipContent && (
<InfoIconButton
icon={userAgentDeviceIcon}
tooltipContent={userAgentTooltipContent}
ariaLabel={t("environments.surveys.responses.device_info")}
maxWidth="max-w-md"
/>
)}
{hasLanguage && languageTooltipContent && (
<InfoIconButton
icon={LanguagesIcon}
tooltipContent={languageTooltipContent}
ariaLabel={t("common.language")}
/>
)}
</div>
);
};

View File

@@ -143,6 +143,8 @@ export const SingleResponseCard = ({
environmentTags={environmentTags}
updateFetchedResponses={updateFetchedResponses}
isReadOnly={isReadOnly}
response={response}
locale={locale}
/>
<DeleteDialog

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponseWithQuotas } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -12,7 +13,7 @@ import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
interface ResponseTimelineProps {
surveys: TSurvey[];
@@ -33,6 +34,7 @@ export const ResponseFeed = ({
locale,
projectPermission,
}: ResponseTimelineProps) => {
const { t } = useTranslation();
const [fetchedResponses, setFetchedResponses] = useState(responses);
useEffect(() => {
@@ -50,7 +52,7 @@ export const ResponseFeed = ({
return (
<>
{fetchedResponses.length === 0 ? (
<EmptySpaceFiller type="response" environment={environment} />
<EmptyState text={t("environments.contacts.no_responses_found")} />
) : (
fetchedResponses.map((response) => (
<ResponseSurveyCard

View File

@@ -300,7 +300,7 @@ export const ContactsTable = ({
</Table>
</div>
{data && hasMore && data.length > 0 && (
{data && hasMore && data.length > 0 && isDataLoaded && (
<div className="mt-4 flex justify-center">
<Button onClick={fetchNextPage}>{t("common.load_more")}</Button>
</div>

View File

@@ -33,6 +33,7 @@ export const SegmentTable = async ({
<>
{segments.map((segment) => (
<SegmentTableDataRowContainer
key={segment.id}
currentSegment={segment}
segments={segments}
contactAttributeKeys={contactAttributeKeys}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { WebhookModal } from "@/modules/integrations/webhooks/components/webhook-detail-modal";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
interface WebhookTableProps {
environment: TEnvironment;
@@ -46,12 +46,7 @@ export const WebhookTable = ({
return (
<>
{webhooks.length === 0 ? (
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.webhooks.empty_webhook_message")}
/>
<EmptyState text={t("environments.integrations.webhooks.empty_webhook_message")} />
) : (
<div className="rounded-lg border border-slate-200">
{TableHeading}

View File

@@ -2,13 +2,11 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { TTag, TTagsCount } from "@formbricks/types/tags";
import { SingleTag } from "@/modules/projects/settings/tags/components/single-tag";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { EmptyState } from "@/modules/ui/components/empty-state";
interface EditTagsWrapperProps {
environment: TEnvironment;
environmentTags: TTag[];
environmentTagsCount: TTagsCount;
isReadOnly: boolean;
@@ -16,7 +14,12 @@ interface EditTagsWrapperProps {
export const EditTagsWrapper: React.FC<EditTagsWrapperProps> = (props) => {
const { t } = useTranslation();
const { environment, environmentTags, environmentTagsCount, isReadOnly } = props;
const { environmentTags, environmentTagsCount, isReadOnly } = props;
if (!environmentTags?.length) {
return <EmptyState text={t("environments.project.tags.no_tag_found")} />;
}
return (
<div className="">
<div className="grid grid-cols-4 content-center rounded-lg bg-white text-left text-sm font-semibold text-slate-900">
@@ -27,11 +30,7 @@ export const EditTagsWrapper: React.FC<EditTagsWrapperProps> = (props) => {
)}
</div>
{!environmentTags?.length ? (
<EmptySpaceFiller environment={environment} type="tag" noWidgetRequired />
) : null}
{environmentTags?.map((tag) => (
{environmentTags.map((tag) => (
<SingleTag
key={tag.id}
tagId={tag.id}

View File

@@ -12,7 +12,7 @@ export const TagsPage = async (props) => {
const params = await props.params;
const t = await getTranslate();
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
const { isReadOnly } = await getEnvironmentAuth(params.environmentId);
const [tags, environmentTagsCount] = await Promise.all([
getTagsByEnvironmentId(params.environmentId),
@@ -28,7 +28,6 @@ export const TagsPage = async (props) => {
title={t("environments.project.tags.manage_tags")}
description={t("environments.project.tags.manage_tags_description")}>
<EditTagsWrapper
environment={environment}
environmentTags={tags}
environmentTagsCount={environmentTagsCount}
isReadOnly={isReadOnly}

View File

@@ -4,7 +4,7 @@ pre {
}
pre::-webkit-scrollbar {
width: 4px !important;
width: 7px !important;
border-radius: 99px;
}
@@ -15,6 +15,6 @@ pre::-webkit-scrollbar-track {
pre::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border: 3px solid #cbd5e1;
border: 2px solid #cbd5e1;
border-radius: 99px;
}

View File

@@ -54,7 +54,7 @@
pre::-webkit-scrollbar {
background: transparent;
width: 10px;
width: 7px;
}
pre::-webkit-scrollbar-thumb {

View File

@@ -1,157 +0,0 @@
"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment";
import { Skeleton } from "@/modules/ui/components/skeleton";
type EmptySpaceFillerProps = {
type: "table" | "response" | "event" | "linkResponse" | "tag" | "summary";
environment: TEnvironment;
noWidgetRequired?: boolean;
emptyMessage?: string;
};
export const EmptySpaceFiller = ({
type,
environment,
noWidgetRequired,
emptyMessage,
}: EmptySpaceFillerProps) => {
const { t } = useTranslation();
if (type === "table") {
return (
<div className="shadow-xs group rounded-xl border border-slate-100 bg-white p-4">
<div className="w-full space-y-3">
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
<div className="flex h-16 w-full flex-col items-center justify-center rounded-lg bg-slate-50 text-slate-700 transition-all duration-300 ease-in-out hover:bg-slate-100">
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex w-full items-center justify-center"
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} </strong>
</span>
</Link>
)}
{((environment.appSetupCompleted || noWidgetRequired) && emptyMessage) ||
t("environments.surveys.summary.waiting_for_response")}
</div>
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
</div>
</div>
);
}
if (type === "response") {
return (
<div className="group space-y-4 rounded-lg bg-white p-6">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
<div className="h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="h-12 w-full rounded-full bg-slate-100"></div>
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} </strong>
</span>
</Link>
)}
{(environment.appSetupCompleted || noWidgetRequired) && (
<span className="bg-light-background-primary-500 text-center">
{emptyMessage ?? t("environments.surveys.summary.waiting_for_response")}
</span>
)}
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</div>
);
}
if (type === "tag") {
return (
<div className="group space-y-4 rounded-lg bg-white p-6">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
<div className="h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="h-12 w-full rounded-full bg-slate-100"></div>
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} 👉</strong>
</span>
</Link>
)}
{(environment.appSetupCompleted || noWidgetRequired) && (
<span className="text-center">{t("environments.project.tags.empty_message")}</span>
)}
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</div>
);
}
if (type === "summary") {
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<Skeleton className="group space-y-4 rounded-lg bg-white p-6">
<div className="flex items-center space-x-4">
<div className="h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="flex gap-4">
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
</div>
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</Skeleton>
</div>
);
}
return (
<div className="group space-y-4 rounded-lg bg-white p-6">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
<div className="h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="h-12 w-full rounded-full bg-slate-100"></div>
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
{!environment.appSetupCompleted && !noWidgetRequired && (
<Link
className="flex h-full w-full items-center justify-center"
href={`/environments/${environment.id}/project/app-connection`}>
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
{t("environments.surveys.summary.install_widget")}{" "}
<strong>{t("environments.surveys.summary.go_to_setup_checklist")} 👉</strong>
</span>
</Link>
)}
{(environment.appSetupCompleted || noWidgetRequired) && (
<span className="text-center">{t("environments.surveys.summary.waiting_for_response")}</span>
)}
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</div>
);
};

View File

@@ -0,0 +1,19 @@
"use client";
interface EmptyStateProps {
text: string;
}
export const EmptyState = ({ text }: EmptyStateProps) => {
return (
<div className="shadow-xs rounded-xl border border-slate-100 bg-white p-4">
<div className="w-full space-y-3">
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
<div className="flex h-16 w-full flex-col items-center justify-center rounded-lg bg-slate-50 text-sm text-slate-500">
{text}
</div>
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
</div>
</div>
);
};

View File

@@ -63,9 +63,7 @@ export const TagsCombobox = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button size="sm" aria-expanded={open}>
{t("environments.project.tags.add_tag")}
</Button>
<Button aria-expanded={open}>{t("environments.project.tags.add_tag")}</Button>
</PopoverTrigger>
<PopoverContent className="max-h-60 w-[200px] overflow-y-auto p-0">
<Command

View File

@@ -58,7 +58,8 @@
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;
width: 7px;
height: 7px;
}
*::-webkit-scrollbar-track {
@@ -67,11 +68,11 @@
*::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border: 3px solid #cbd5e1;
border: 1px solid #cbd5e1;
}
.filter-scrollbar::-webkit-scrollbar {
height: 10px;
height: 7px;
}
.filter-scrollbar::-webkit-scrollbar-thumb {