Compare commits

...

2 Commits

Author SHA1 Message Date
Javi Aguilar b656e94f07 fix(a11y): add feedback source dialog cannot scroll on short screens 2026-05-13 16:32:23 +02:00
Dhruwang Jariwala 5f5860cb23 feat(unify): add delete option for feedback records (ENG-938) (#7991) 2026-05-13 15:56:55 +04:00
24 changed files with 491 additions and 31 deletions
+7
View File
@@ -3521,6 +3521,9 @@ checksums:
workspace/unify/custom_source_type_placeholder: f139e3e5d70dbf426d7c6b5ab2b198cc
workspace/unify/default_connector_name_csv: ef4060fef24c4fec064987b9d2a9fa4b
workspace/unify/default_connector_name_formbricks: e7afdf7cc1cd7bcf75e7b5d64903a110
workspace/unify/delete_feedback_record: 86d7262c000cfb1f91ea373036cd3616
workspace/unify/delete_feedback_record_confirmation: dd2b12e75cb52a73f92c997c347a8f36
workspace/unify/delete_feedback_records_confirmation: cd8a4ba828963fb6dab5c96606565079
workspace/unify/discard_feedback_record_changes_description: 48ccde99858dcbeb4d679749d0f51941
workspace/unify/discard_feedback_record_changes_title: 52df2800f7b0e8a1d04c47113e019a3e
workspace/unify/drop_a_field_here: 884f3025e618e0a5dcbcb5567335d1bb
@@ -3536,10 +3539,12 @@ checksums:
workspace/unify/error_connector_name_required: b2d5f79f6126e23128d7bef0c1736ff2
workspace/unify/error_connector_questions_required: d7d8e388959ab83a9195ba63bebf6516
workspace/unify/error_connector_survey_required: 1f49086dfb874307aae1136e88c3d514
workspace/unify/failed_to_delete_feedback_records: 6096404d164fda196734675885e278c3
workspace/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8
workspace/unify/feedback_date: ddba5d3270d4a6394d29721025a04400
workspace/unify/feedback_directory: 156fa9957e1ee5eadf1f44226a2365e4
workspace/unify/feedback_record_created_successfully: 0ff30472085f1313a5ad53837c83e7c1
workspace/unify/feedback_record_deleted_successfully: ea58f53841cc48dc974f1589f4718da6
workspace/unify/feedback_record_details: 823f3353db049a9d263ef31405054cda
workspace/unify/feedback_record_details_description: 0b6f908154161241ce6bdeb4a2acaecd
workspace/unify/feedback_record_fields: 88c0f13afeb88fe751f85e79b0f73064
@@ -3547,6 +3552,8 @@ checksums:
workspace/unify/feedback_record_updated_successfully: cb40ef4b924e21fa627ebe6809d1d826
workspace/unify/feedback_record_value_required: b54d4d86f82071a93dc979e8eb359cf0
workspace/unify/feedback_records: e24cf48bb6985910f4ffe5e00512d388
workspace/unify/feedback_records_deleted_successfully: 176c27a362d593a62355904c50bb0e9e
workspace/unify/feedback_records_partially_deleted: dff8cd8482e8053ce4186e6b42d0aee8
workspace/unify/feedback_records_refreshed: 4b27a8e2a8dbe8afa945d9f874aa7ef1
workspace/unify/feedback_sources: e58ec9be19db8789e7096a756d24f2b2
workspace/unify/feedback_sources_directory_access_multiple: 11d613bc1e9825aa6faa3db17ae678eb
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Geben Sie den benutzerdefinierten Quelltyp ein",
"default_connector_name_csv": "CSV-Import",
"default_connector_name_formbricks": "Formbricks-Umfrage-Verbindung",
"delete_feedback_record": "Feedback-Eintrag löschen",
"delete_feedback_record_confirmation": "Dadurch wird der Feedback-Eintrag dauerhaft gelöscht und aus dem verbundenen Verzeichnis entfernt.",
"delete_feedback_records_confirmation": "Dadurch werden {count} Feedback-Einträge dauerhaft gelöscht und aus dem verbundenen Verzeichnis entfernt.",
"discard_feedback_record_changes_description": "Ihre Änderungen gehen verloren, wenn Sie diese Schublade schließen.",
"discard_feedback_record_changes_title": "Nicht gespeicherte Änderungen verwerfen?",
"drop_a_field_here": "Ziehe ein Feld hierher",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Quellenname ist erforderlich",
"error_connector_questions_required": "Wähle mindestens eine Frage aus",
"error_connector_survey_required": "Wähle eine Umfrage aus",
"failed_to_delete_feedback_records": "Feedback-Einträge konnten nicht gelöscht werden",
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
"feedback_date": "Aktuelles Datum",
"feedback_directory": "Feedback-Verzeichnis",
"feedback_record_created_successfully": "Feedback-Datensatz erfolgreich erstellt",
"feedback_record_deleted_successfully": "Feedback-Eintrag erfolgreich gelöscht",
"feedback_record_details": "Details zum Feedback-Datensatz",
"feedback_record_details_description": "Überprüfen und aktualisieren Sie die Felder des Feedback-Datensatzes.",
"feedback_record_fields": "Feedback-Eintragsfelder",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Feedback-Datensatz erfolgreich aktualisiert",
"feedback_record_value_required": "Für den ausgewählten Feldtyp ist ein Wert erforderlich",
"feedback_records": "Feedback-Einträge",
"feedback_records_deleted_successfully": "{count} Feedback-Einträge gelöscht",
"feedback_records_partially_deleted": "{succeeded} von {total} Feedback-Einträgen gelöscht",
"feedback_records_refreshed": "Feedback-Einträge aktualisiert",
"feedback_sources": "Feedback-Quellen",
"feedback_sources_directory_access_multiple": "Neue Datensätze aus diesen Quellen werden gespeichert in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Enter custom source type",
"default_connector_name_csv": "CSV Import",
"default_connector_name_formbricks": "Formbricks Survey Connection",
"delete_feedback_record": "Delete feedback record",
"delete_feedback_record_confirmation": "This will permanently delete the feedback record and remove it from the connected directory.",
"delete_feedback_records_confirmation": "This will permanently delete {count} feedback records and remove them from the connected directory.",
"discard_feedback_record_changes_description": "Your changes will be lost if you close this drawer.",
"discard_feedback_record_changes_title": "Discard unsaved changes?",
"drop_a_field_here": "Drop a field here",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Source name is required",
"error_connector_questions_required": "Select at least one question",
"error_connector_survey_required": "Select a survey",
"failed_to_delete_feedback_records": "Failed to delete feedback records",
"failed_to_load_feedback_records": "Failed to load feedback records",
"feedback_date": "Current date",
"feedback_directory": "Feedback Directory",
"feedback_record_created_successfully": "Feedback record created successfully",
"feedback_record_deleted_successfully": "Feedback record deleted successfully",
"feedback_record_details": "Feedback record details",
"feedback_record_details_description": "Review and update feedback record fields.",
"feedback_record_fields": "Feedback Record Fields",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Feedback record updated successfully",
"feedback_record_value_required": "A value is required for the selected field type",
"feedback_records": "Feedback Records",
"feedback_records_deleted_successfully": "{count} feedback records deleted",
"feedback_records_partially_deleted": "{succeeded} of {total} feedback records deleted",
"feedback_records_refreshed": "Feedback records refreshed",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Ingrese el tipo de fuente personalizado",
"default_connector_name_csv": "Importación CSV",
"default_connector_name_formbricks": "Conexión de encuesta de Formbricks",
"delete_feedback_record": "Eliminar registro de comentarios",
"delete_feedback_record_confirmation": "Esto eliminará permanentemente el registro de comentarios y lo quitará del directorio conectado.",
"delete_feedback_records_confirmation": "Esto eliminará permanentemente {count} registros de comentarios y los quitará del directorio conectado.",
"discard_feedback_record_changes_description": "Sus cambios se perderán si cierra este cajón.",
"discard_feedback_record_changes_title": "¿Descartar los cambios no guardados?",
"drop_a_field_here": "Suelta un campo aquí",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "El nombre de origen es obligatorio",
"error_connector_questions_required": "Selecciona al menos una pregunta",
"error_connector_survey_required": "Selecciona una encuesta",
"failed_to_delete_feedback_records": "No se pudieron eliminar los registros de comentarios",
"failed_to_load_feedback_records": "Error al cargar los registros de comentarios",
"feedback_date": "Fecha actual",
"feedback_directory": "Directorio de feedback",
"feedback_record_created_successfully": "Registro de comentarios creado correctamente",
"feedback_record_deleted_successfully": "Registro de comentarios eliminado correctamente",
"feedback_record_details": "Detalles del registro de comentarios",
"feedback_record_details_description": "Revise y actualice los campos del registro de comentarios.",
"feedback_record_fields": "Campos de registro de comentarios",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Registro de comentarios actualizado correctamente",
"feedback_record_value_required": "Se requiere un valor para el tipo de campo seleccionado",
"feedback_records": "Registros de comentarios",
"feedback_records_deleted_successfully": "{count} registros de comentarios eliminados",
"feedback_records_partially_deleted": "{succeeded} de {total} registros de comentarios eliminados",
"feedback_records_refreshed": "Registros de comentarios actualizados",
"feedback_sources": "Fuentes de feedback",
"feedback_sources_directory_access_multiple": "Los nuevos registros de estas fuentes se almacenarán en: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Entrez le type de source personnalisé",
"default_connector_name_csv": "Importation CSV",
"default_connector_name_formbricks": "Connexion de sondage Formbricks",
"delete_feedback_record": "Supprimer l'enregistrement de commentaire",
"delete_feedback_record_confirmation": "Cela supprimera définitivement l'enregistrement de commentaire et le retirera du répertoire connecté.",
"delete_feedback_records_confirmation": "Cela supprimera définitivement {count} enregistrements de commentaires et les retirera du répertoire connecté.",
"discard_feedback_record_changes_description": "Vos modifications seront perdues si vous fermez ce tiroir.",
"discard_feedback_record_changes_title": "Supprimer les modifications non enregistrées ?",
"drop_a_field_here": "Déposez un champ ici",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Le nom de la source est requis",
"error_connector_questions_required": "Sélectionnez au moins une question",
"error_connector_survey_required": "Sélectionnez une enquête",
"failed_to_delete_feedback_records": "Échec de la suppression des enregistrements de commentaires",
"failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback",
"feedback_date": "Date actuelle",
"feedback_directory": "Répertoire de retours",
"feedback_record_created_successfully": "Enregistrement de commentaires créé avec succès",
"feedback_record_deleted_successfully": "Enregistrement de commentaire supprimé avec succès",
"feedback_record_details": "Détails de l'enregistrement des commentaires",
"feedback_record_details_description": "Examiner et mettre à jour les champs denregistrement des commentaires.",
"feedback_record_fields": "Champs d'enregistrement de feedback",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "L'enregistrement des commentaires a été mis à jour avec succès",
"feedback_record_value_required": "Une valeur est requise pour le type de champ sélectionné",
"feedback_records": "Enregistrements de feedback",
"feedback_records_deleted_successfully": "{count} enregistrements de commentaires supprimés",
"feedback_records_partially_deleted": "{succeeded} enregistrements de commentaires supprimés sur {total}",
"feedback_records_refreshed": "Enregistrements de feedback actualisés",
"feedback_sources": "Sources de feedback",
"feedback_sources_directory_access_multiple": "Les nouveaux enregistrements de ces sources seront stockés dans : {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Adja meg az egyéni forrástípust",
"default_connector_name_csv": "CSV importálás",
"default_connector_name_formbricks": "Formbricks kérdőív kapcsolat",
"delete_feedback_record": "Visszajelzési bejegyzés törlése",
"delete_feedback_record_confirmation": "Ez véglegesen törli a visszajelzési bejegyzést, és eltávolítja a csatlakoztatott könyvtárból.",
"delete_feedback_records_confirmation": "Ez véglegesen töröl {count} visszajelzési bejegyzést, és eltávolítja őket a csatlakoztatott könyvtárból.",
"discard_feedback_record_changes_description": "A módosítások elvesznek, ha bezárja ezt a fiókot.",
"discard_feedback_record_changes_title": "Elveti a nem mentett módosításokat?",
"drop_a_field_here": "Húzz ide egy mezőt",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "A forrás neve kötelező",
"error_connector_questions_required": "Válasszon ki legalább egy kérdést",
"error_connector_survey_required": "Válasszon ki egy felmérést",
"failed_to_delete_feedback_records": "A visszajelzési rekordok törlése sikertelen",
"failed_to_load_feedback_records": "Nem sikerült betölteni a visszajelzési rekordokat",
"feedback_date": "Aktuális dátum",
"feedback_directory": "Visszajelzési könyvtár",
"feedback_record_created_successfully": "A visszajelzési rekord sikeresen létrehozva",
"feedback_record_deleted_successfully": "A visszajelzési bejegyzés sikeresen törölve",
"feedback_record_details": "A visszajelzési rekord részletei",
"feedback_record_details_description": "Tekintse át és frissítse a visszajelzési rekordmezőket.",
"feedback_record_fields": "Visszajelzési rekord mezők",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "A visszajelzési rekord sikeresen frissítve",
"feedback_record_value_required": "A kiválasztott mezőtípushoz értéket kell megadni",
"feedback_records": "Visszajelzési rekordok",
"feedback_records_deleted_successfully": "{count} visszajelzési bejegyzés törölve",
"feedback_records_partially_deleted": "{succeeded} / {total} visszajelzési rekord törölve",
"feedback_records_refreshed": "Visszajelzési rekordok frissítve",
"feedback_sources": "Visszajelzési források",
"feedback_sources_directory_access_multiple": "Az ezekből a forrásokból származó új rekordok a következő helyen lesznek tárolva: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "カスタムソースタイプを入力してください",
"default_connector_name_csv": "CSVインポート",
"default_connector_name_formbricks": "Formbricks フォーム接続",
"delete_feedback_record": "フィードバック記録を削除",
"delete_feedback_record_confirmation": "この操作により、フィードバック記録が完全に削除され、接続されているディレクトリから削除されます。",
"delete_feedback_records_confirmation": "この操作により、{count}件のフィードバック記録が完全に削除され、接続されているディレクトリから削除されます。",
"discard_feedback_record_changes_description": "このドロワーを閉じると、変更内容は失われます。",
"discard_feedback_record_changes_title": "保存されていない変更を破棄しますか?",
"drop_a_field_here": "ここにフィールドをドロップ",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "ソース名は必須です",
"error_connector_questions_required": "少なくとも1つの質問を選択してください",
"error_connector_survey_required": "アンケートを選択してください",
"failed_to_delete_feedback_records": "フィードバックレコードの削除に失敗しました",
"failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました",
"feedback_date": "現在の日付",
"feedback_directory": "フィードバックディレクトリ",
"feedback_record_created_successfully": "フィードバックレコードが正常に作成されました",
"feedback_record_deleted_successfully": "フィードバック記録を削除しました",
"feedback_record_details": "フィードバック記録の詳細",
"feedback_record_details_description": "フィードバック レコード フィールドを確認して更新します。",
"feedback_record_fields": "フィードバックレコードフィールド",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "フィードバックレコードが正常に更新されました",
"feedback_record_value_required": "選択したフィールド タイプには値が必要です",
"feedback_records": "フィードバックレコード",
"feedback_records_deleted_successfully": "{count}件のフィードバック記録を削除しました",
"feedback_records_partially_deleted": "{total}件中{succeeded}件のフィードバックレコードを削除しました",
"feedback_records_refreshed": "フィードバックレコードを更新しました",
"feedback_sources": "フィードバックソース",
"feedback_sources_directory_access_multiple": "これらのソースからの新しいレコードは次の場所に保存されます:{directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Voer een aangepast brontype in",
"default_connector_name_csv": "CSV import",
"default_connector_name_formbricks": "Formbricks Survey verbinding",
"delete_feedback_record": "Feedbackrecord verwijderen",
"delete_feedback_record_confirmation": "Hiermee wordt het feedbackrecord permanent verwijderd en uit de gekoppelde map gehaald.",
"delete_feedback_records_confirmation": "Hiermee worden {count} feedbackrecords permanent verwijderd en uit de gekoppelde map gehaald.",
"discard_feedback_record_changes_description": "Als u deze lade sluit, gaan uw wijzigingen verloren.",
"discard_feedback_record_changes_title": "Niet-opgeslagen wijzigingen verwijderen?",
"drop_a_field_here": "Zet hier een veld neer",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Bronnaam is verplicht",
"error_connector_questions_required": "Selecteer minimaal één vraag",
"error_connector_survey_required": "Selecteer een enquête",
"failed_to_delete_feedback_records": "Feedbackgegevens verwijderen mislukt",
"failed_to_load_feedback_records": "Kan feedbackrecords niet laden",
"feedback_date": "Huidige datum",
"feedback_directory": "Feedbackmap",
"feedback_record_created_successfully": "Feedbackrecord is succesvol aangemaakt",
"feedback_record_deleted_successfully": "Feedbackrecord succesvol verwijderd",
"feedback_record_details": "Details van feedbackrecord",
"feedback_record_details_description": "Controleer en update de feedbackrecordvelden.",
"feedback_record_fields": "Feedbackrecordvelden",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Feedbackrecord is succesvol bijgewerkt",
"feedback_record_value_required": "Er is een waarde vereist voor het geselecteerde veldtype",
"feedback_records": "Feedbackrecords",
"feedback_records_deleted_successfully": "{count} feedbackrecords verwijderd",
"feedback_records_partially_deleted": "{succeeded} van {total} feedbackgegevens verwijderd",
"feedback_records_refreshed": "Feedbackrecords vernieuwd",
"feedback_sources": "Feedbackbronnen",
"feedback_sources_directory_access_multiple": "Nieuwe records van deze bronnen worden opgeslagen in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
"default_connector_name_csv": "Importação CSV",
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
"delete_feedback_record": "Excluir registro de feedback",
"delete_feedback_record_confirmation": "Isso excluirá permanentemente o registro de feedback e o removerá do diretório conectado.",
"delete_feedback_records_confirmation": "Isso excluirá permanentemente {count} registros de feedback e os removerá do diretório conectado.",
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
"drop_a_field_here": "Solte um campo aqui",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "O nome da fonte é obrigatório",
"error_connector_questions_required": "Selecione pelo menos uma pergunta",
"error_connector_survey_required": "Selecione uma pesquisa",
"failed_to_delete_feedback_records": "Falha ao excluir registros de feedback",
"failed_to_load_feedback_records": "Falha ao carregar registros de feedback",
"feedback_date": "Data atual",
"feedback_directory": "Diretório de Feedback",
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
"feedback_record_deleted_successfully": "Registro de feedback excluído com sucesso",
"feedback_record_details": "Detalhes do registro de feedback",
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
"feedback_record_fields": "Campos do registro de feedback",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
"feedback_records": "Registros de feedback",
"feedback_records_deleted_successfully": "{count} registros de feedback excluídos",
"feedback_records_partially_deleted": "{succeeded} de {total} registros de feedback excluídos",
"feedback_records_refreshed": "Registros de feedback atualizados",
"feedback_sources": "Fontes de Feedback",
"feedback_sources_directory_access_multiple": "Novos registros dessas fontes serão armazenados em: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
"default_connector_name_csv": "Importação CSV",
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
"delete_feedback_record": "Eliminar registo de feedback",
"delete_feedback_record_confirmation": "Esta ação irá eliminar permanentemente o registo de feedback e removê-lo do diretório associado.",
"delete_feedback_records_confirmation": "Esta ação irá eliminar permanentemente {count} registos de feedback e removê-los do diretório associado.",
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
"drop_a_field_here": "Solte um campo aqui",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "O nome da origem é obrigatório",
"error_connector_questions_required": "Seleciona pelo menos uma pergunta",
"error_connector_survey_required": "Seleciona um inquérito",
"failed_to_delete_feedback_records": "Falha ao eliminar registos de feedback",
"failed_to_load_feedback_records": "Falha ao carregar registos de feedback",
"feedback_date": "Data atual",
"feedback_directory": "Diretório de Feedback",
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
"feedback_record_deleted_successfully": "Registo de feedback eliminado com sucesso",
"feedback_record_details": "Detalhes do registro de feedback",
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
"feedback_record_fields": "Campos de registo de feedback",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
"feedback_records": "Registos de feedback",
"feedback_records_deleted_successfully": "{count} registos de feedback eliminados",
"feedback_records_partially_deleted": "{succeeded} de {total} registos de feedback eliminados",
"feedback_records_refreshed": "Registos de feedback atualizados",
"feedback_sources": "Fontes de Feedback",
"feedback_sources_directory_access_multiple": "Novos registos destas fontes serão armazenados em: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Introduceți tipul de sursă personalizat",
"default_connector_name_csv": "Import CSV",
"default_connector_name_formbricks": "Conexiune chestionar Formbricks",
"delete_feedback_record": "Șterge înregistrarea de feedback",
"delete_feedback_record_confirmation": "Aceasta va șterge definitiv înregistrarea de feedback și o va elimina din directorul conectat.",
"delete_feedback_records_confirmation": "Aceasta va șterge definitiv {count} înregistrări de feedback și le va elimina din directorul conectat.",
"discard_feedback_record_changes_description": "Modificările dvs. se vor pierde dacă închideți acest sertar.",
"discard_feedback_record_changes_title": "Renunțați la modificările nesalvate?",
"drop_a_field_here": "Trage un câmp aici",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Numele sursei este obligatoriu",
"error_connector_questions_required": "Selectează cel puțin o întrebare",
"error_connector_survey_required": "Selectează un sondaj",
"failed_to_delete_feedback_records": "Eșec la ștergerea înregistrărilor de feedback",
"failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback",
"feedback_date": "Data curentă",
"feedback_directory": "Director de Feedback",
"feedback_record_created_successfully": "Înregistrare de feedback creată cu succes",
"feedback_record_deleted_successfully": "Înregistrarea de feedback a fost ștearsă cu succes",
"feedback_record_details": "Detaliile înregistrării feedback-ului",
"feedback_record_details_description": "Examinați și actualizați câmpurile pentru înregistrarea de feedback.",
"feedback_record_fields": "Câmpuri înregistrare feedback",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Înregistrarea feedback-ului a fost actualizată cu succes",
"feedback_record_value_required": "Este necesară o valoare pentru tipul de câmp selectat",
"feedback_records": "Înregistrări de feedback",
"feedback_records_deleted_successfully": "{count} înregistrări de feedback șterse",
"feedback_records_partially_deleted": "{succeeded} din {total} înregistrări de feedback șterse",
"feedback_records_refreshed": "Înregistrările de feedback au fost actualizate",
"feedback_sources": "Surse de feedback",
"feedback_sources_directory_access_multiple": "Înregistrările noi din aceste surse vor fi stocate în: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Введите собственный тип источника",
"default_connector_name_csv": "Импорт CSV",
"default_connector_name_formbricks": "Подключение опроса Formbricks",
"delete_feedback_record": "Удалить запись обратной связи",
"delete_feedback_record_confirmation": "Это навсегда удалит запись обратной связи и уберёт её из подключённого каталога.",
"delete_feedback_records_confirmation": "Это навсегда удалит {count} {count, plural, one {запись обратной связи} few {записи обратной связи} many {записей обратной связи} other {записей обратной связи}} и уберёт их из подключённого каталога.",
"discard_feedback_record_changes_description": "Ваши изменения будут потеряны, если вы закроете этот ящик.",
"discard_feedback_record_changes_title": "Отменить несохраненные изменения?",
"drop_a_field_here": "Перетащи сюда поле",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Необходимо указать название источника",
"error_connector_questions_required": "Выберите хотя бы один вопрос",
"error_connector_survey_required": "Выберите опрос",
"failed_to_delete_feedback_records": "Не удалось удалить записи обратной связи",
"failed_to_load_feedback_records": "Не удалось загрузить отзывы",
"feedback_date": "Текущая дата",
"feedback_directory": "Директория обратной связи",
"feedback_record_created_successfully": "Запись отзыва успешно создана",
"feedback_record_deleted_successfully": "Запись обратной связи успешно удалена",
"feedback_record_details": "Детали записи обратной связи",
"feedback_record_details_description": "Просмотрите и обновите поля записи отзыва.",
"feedback_record_fields": "Поля записи отзыва",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Запись отзыва успешно обновлена.",
"feedback_record_value_required": "Требуется значение для выбранного типа поля.",
"feedback_records": "Записи отзывов",
"feedback_records_deleted_successfully": "Удалено {count} {count, plural, one {запись обратной связи} few {записи обратной связи} many {записей обратной связи} other {записей обратной связи}}",
"feedback_records_partially_deleted": "Удалено {succeeded} из {total} записей обратной связи",
"feedback_records_refreshed": "Записи отзывов обновлены",
"feedback_sources": "Источники обратной связи",
"feedback_sources_directory_access_multiple": "Новые записи из этих источников будут сохранены в: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Ange anpassad källtyp",
"default_connector_name_csv": "CSV-import",
"default_connector_name_formbricks": "Formbricks Survey-anslutning",
"delete_feedback_record": "Ta bort feedbackpost",
"delete_feedback_record_confirmation": "Detta kommer permanent ta bort feedbackposten och radera den från den anslutna katalogen.",
"delete_feedback_records_confirmation": "Detta kommer permanent ta bort {count} feedbackposter och radera dem från den anslutna katalogen.",
"discard_feedback_record_changes_description": "Dina ändringar kommer att gå förlorade om du stänger den här lådan.",
"discard_feedback_record_changes_title": "Vill du ignorera osparade ändringar?",
"drop_a_field_here": "Släpp ett fält här",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Källnamn krävs",
"error_connector_questions_required": "Välj minst en fråga",
"error_connector_survey_required": "Välj en undersökning",
"failed_to_delete_feedback_records": "Misslyckades att ta bort feedbackposter",
"failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter",
"feedback_date": "Aktuellt datum",
"feedback_directory": "Feedback-katalog",
"feedback_record_created_successfully": "Feedbackposten har skapats",
"feedback_record_deleted_successfully": "Feedbackposten har tagits bort",
"feedback_record_details": "Feedbackpostdetaljer",
"feedback_record_details_description": "Granska och uppdatera fält för feedbackposter.",
"feedback_record_fields": "Fält för feedbackpost",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Feedbackposten har uppdaterats",
"feedback_record_value_required": "Ett värde krävs för den valda fälttypen",
"feedback_records": "Feedbackposter",
"feedback_records_deleted_successfully": "{count} feedbackposter har tagits bort",
"feedback_records_partially_deleted": "{succeeded} av {total} feedbackposter raderade",
"feedback_records_refreshed": "Feedbackposter har uppdaterats",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "Özel kaynak türünü girin",
"default_connector_name_csv": "CSV İçe Aktarma",
"default_connector_name_formbricks": "Formbricks Anket Bağlantısı",
"delete_feedback_record": "Geri bildirim kaydını sil",
"delete_feedback_record_confirmation": "Bu işlem geri bildirim kaydını kalıcı olarak silecek ve bağlı dizinden kaldıracaktır.",
"delete_feedback_records_confirmation": "Bu işlem {count} geri bildirim kaydını kalıcı olarak silecek ve bağlı dizinden kaldıracaktır.",
"discard_feedback_record_changes_description": "Bu çekmeceyi kapatırsanız değişiklikleriniz kaybolacak.",
"discard_feedback_record_changes_title": "Kaydedilmemiş değişiklikler silinsin mi?",
"drop_a_field_here": "Buraya bir alan bırakın",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "Kaynak adı gereklidir",
"error_connector_questions_required": "En az bir soru seçin",
"error_connector_survey_required": "Bir anket seçin",
"failed_to_delete_feedback_records": "Geri bildirim kayıtları silinemedi",
"failed_to_load_feedback_records": "Geri bildirim kayıtları yüklenemedi",
"feedback_date": "Geçerli tarih",
"feedback_directory": "Geri Bildirim Dizini",
"feedback_record_created_successfully": "Geri bildirim kaydı başarıyla oluşturuldu",
"feedback_record_deleted_successfully": "Geri bildirim kaydı başarıyla silindi",
"feedback_record_details": "Geri bildirim kaydı ayrıntıları",
"feedback_record_details_description": "Geri bildirim kayıt alanlarını inceleyin ve güncelleyin.",
"feedback_record_fields": "Geri Bildirim Kayıt Alanları",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "Geri bildirim kaydı başarıyla güncellendi",
"feedback_record_value_required": "Seçilen alan türü için bir değer gerekli",
"feedback_records": "Geri Bildirim Kayıtları",
"feedback_records_deleted_successfully": "{count} geri bildirim kaydı silindi",
"feedback_records_partially_deleted": "{total} geri bildirim kaydından {succeeded} tanesi silindi",
"feedback_records_refreshed": "Geri bildirim kayıtları yenilendi",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "输入自定义来源类型",
"default_connector_name_csv": "CSV 导入",
"default_connector_name_formbricks": "Formbricks 调查连接",
"delete_feedback_record": "删除反馈记录",
"delete_feedback_record_confirmation": "这将永久删除该反馈记录并从关联目录中移除。",
"delete_feedback_records_confirmation": "这将永久删除 {count} 条反馈记录并从关联目录中移除。",
"discard_feedback_record_changes_description": "如果关闭此抽屉,您的更改将会丢失。",
"discard_feedback_record_changes_title": "放弃未保存的更改?",
"drop_a_field_here": "将字段拖到这里",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "数据源名称为必填项",
"error_connector_questions_required": "请至少选择一个问题",
"error_connector_survey_required": "请选择一个调查问卷",
"failed_to_delete_feedback_records": "删除反馈记录失败",
"failed_to_load_feedback_records": "加载反馈记录失败",
"feedback_date": "当前日期",
"feedback_directory": "反馈目录",
"feedback_record_created_successfully": "反馈记录创建成功",
"feedback_record_deleted_successfully": "反馈记录已成功删除",
"feedback_record_details": "反馈记录详情",
"feedback_record_details_description": "查看并更新反馈记录字段。",
"feedback_record_fields": "反馈记录字段",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "反馈记录更新成功",
"feedback_record_value_required": "所选字段类型需要一个值",
"feedback_records": "反馈记录",
"feedback_records_deleted_successfully": "已删除 {count} 条反馈记录",
"feedback_records_partially_deleted": "已删除 {succeeded} 条(共 {total} 条)反馈记录",
"feedback_records_refreshed": "反馈记录已刷新",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
+7
View File
@@ -3681,6 +3681,9 @@
"custom_source_type_placeholder": "輸入自訂來源類型",
"default_connector_name_csv": "CSV 匯入",
"default_connector_name_formbricks": "Formbricks 問卷連線",
"delete_feedback_record": "刪除意見回饋記錄",
"delete_feedback_record_confirmation": "這將永久刪除此意見回饋記錄,並從已連結的目錄中移除。",
"delete_feedback_records_confirmation": "這將永久刪除 {count} 筆意見回饋記錄,並從已連結的目錄中移除。",
"discard_feedback_record_changes_description": "如果關閉此抽屜,您的變更將會遺失。",
"discard_feedback_record_changes_title": "放棄未儲存的變更?",
"drop_a_field_here": "請將欄位拖曳到這裡",
@@ -3696,10 +3699,12 @@
"error_connector_name_required": "來源名稱為必填項目",
"error_connector_questions_required": "請至少選擇一個問題",
"error_connector_survey_required": "請選擇一個調查問卷",
"failed_to_delete_feedback_records": "刪除意見回饋記錄失敗",
"failed_to_load_feedback_records": "載入回饋紀錄失敗",
"feedback_date": "目前日期",
"feedback_directory": "意見回饋目錄",
"feedback_record_created_successfully": "回饋記錄創建成功",
"feedback_record_deleted_successfully": "意見回饋記錄已成功刪除",
"feedback_record_details": "反饋記錄詳情",
"feedback_record_details_description": "查看並更新回饋記錄欄位。",
"feedback_record_fields": "回饋紀錄欄位",
@@ -3707,6 +3712,8 @@
"feedback_record_updated_successfully": "回饋記錄更新成功",
"feedback_record_value_required": "所選欄位類型需要一個值",
"feedback_records": "回饋紀錄",
"feedback_records_deleted_successfully": "已刪除 {count} 筆意見回饋記錄",
"feedback_records_partially_deleted": "已刪除 {succeeded} 筆意見回饋記錄,共 {total} 筆",
"feedback_records_refreshed": "回饋紀錄已更新",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
+55 -10
View File
@@ -1,19 +1,25 @@
"use server";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory";
import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils";
import { createFeedbackRecord, retrieveFeedbackRecord, updateFeedbackRecord } from "@/modules/hub/service";
import {
createFeedbackRecord,
deleteFeedbackRecord,
retrieveFeedbackRecord,
updateFeedbackRecord,
} from "@/modules/hub/service";
import type { FeedbackRecordCreateParams, FeedbackRecordUpdateParams } from "@/modules/hub/types";
import {
TCreateFeedbackRecordAction,
TRetrieveFeedbackRecordAction,
TUpdateFeedbackRecordAction,
ZCreateFeedbackRecordAction,
ZDeleteFeedbackRecordAction,
ZRetrieveFeedbackRecordAction,
ZUpdateFeedbackRecordAction,
} from "./types";
@@ -50,10 +56,14 @@ const getWorkspaceDirectoryIds = async (workspaceId: string): Promise<Set<string
return new Set(directories.map((directory) => directory.id));
};
const assertRecordBelongsToWorkspace = (directoryIds: Set<string>, tenantId: string): void => {
const assertRecordBelongsToWorkspace = (
directoryIds: Set<string>,
tenantId: string,
recordId: string | null
): void => {
if (!directoryIds.has(tenantId)) {
// Throw a generic error indistinguishable from "not found" to prevent IDOR
throw new Error("Feedback record not found");
// Same error shape as a genuine "not found" to prevent IDOR via response differences
throw new ResourceNotFoundError("Feedback record", recordId);
}
};
@@ -74,10 +84,14 @@ export const retrieveFeedbackRecordAction = authenticatedActionClient
const recordResult = await retrieveFeedbackRecord(parsedInput.recordId);
if (!recordResult.data || recordResult.error) {
throw new Error("Feedback record not found");
throw new ResourceNotFoundError("Feedback record", parsedInput.recordId);
}
assertRecordBelongsToWorkspace(workspaceDirectoryIds, recordResult.data.tenant_id);
assertRecordBelongsToWorkspace(
workspaceDirectoryIds,
recordResult.data.tenant_id,
parsedInput.recordId
);
return recordResult.data;
}
@@ -96,7 +110,7 @@ export const createFeedbackRecordAction = authenticatedActionClient
await ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite");
const workspaceDirectoryIds = await getWorkspaceDirectoryIds(parsedInput.workspaceId);
assertRecordBelongsToWorkspace(workspaceDirectoryIds, parsedInput.recordInput.tenant_id);
assertRecordBelongsToWorkspace(workspaceDirectoryIds, parsedInput.recordInput.tenant_id, null);
const { recordInput } = parsedInput;
const createParams: FeedbackRecordCreateParams = {
@@ -146,10 +160,14 @@ export const updateFeedbackRecordAction = authenticatedActionClient
const currentRecordResult = await retrieveFeedbackRecord(parsedInput.recordId);
if (!currentRecordResult.data || currentRecordResult.error) {
throw new Error("Feedback record not found");
throw new ResourceNotFoundError("Feedback record", parsedInput.recordId);
}
assertRecordBelongsToWorkspace(workspaceDirectoryIds, currentRecordResult.data.tenant_id);
assertRecordBelongsToWorkspace(
workspaceDirectoryIds,
currentRecordResult.data.tenant_id,
parsedInput.recordId
);
const { updateInput } = parsedInput;
const updateParams: FeedbackRecordUpdateParams = {
@@ -176,3 +194,30 @@ export const updateFeedbackRecordAction = authenticatedActionClient
return updateResult.data;
}
);
export const deleteFeedbackRecordAction = authenticatedActionClient
.inputSchema(ZDeleteFeedbackRecordAction)
.action(async ({ ctx, parsedInput }) => {
const [, workspaceDirectoryIds] = await Promise.all([
ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite"),
getWorkspaceDirectoryIds(parsedInput.workspaceId),
]);
const currentRecordResult = await retrieveFeedbackRecord(parsedInput.recordId);
if (!currentRecordResult.data || currentRecordResult.error) {
throw new ResourceNotFoundError("Feedback record", parsedInput.recordId);
}
assertRecordBelongsToWorkspace(
workspaceDirectoryIds,
currentRecordResult.data.tenant_id,
parsedInput.recordId
);
const deleteResult = await deleteFeedbackRecord(parsedInput.recordId);
if (!deleteResult.data || deleteResult.error) {
throw new Error(deleteResult.error?.message || "Failed to delete feedback record");
}
return { recordId: parsedInput.recordId };
});
@@ -10,6 +10,7 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
FormControl,
FormError,
@@ -37,6 +38,7 @@ import {
import { Switch } from "@/modules/ui/components/switch";
import {
createFeedbackRecordAction,
deleteFeedbackRecordAction,
retrieveFeedbackRecordAction,
updateFeedbackRecordAction,
} from "../actions";
@@ -87,6 +89,8 @@ export const FeedbackRecordFormDrawer = ({
const [isLoadingRecord, setIsLoadingRecord] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const defaultValues = useMemo(() => getCreateDefaults(directories), [directories]);
@@ -283,6 +287,24 @@ export const FeedbackRecordFormDrawer = ({
return true;
};
const handleDelete = async () => {
if (!recordId) return;
setIsDeleting(true);
try {
const result = await deleteFeedbackRecordAction({ workspaceId, recordId });
if (!result?.data) {
toast.error(getFormattedErrorMessage(result));
return;
}
toast.success(t("workspace.unify.feedback_record_deleted_successfully"));
setIsDeleteDialogOpen(false);
await onSuccess();
onOpenChange(false);
} finally {
setIsDeleting(false);
}
};
const handleSubmit = form.handleSubmit(async (values) => {
form.clearErrors();
@@ -785,15 +807,30 @@ export const FeedbackRecordFormDrawer = ({
</FormProvider>
)}
<SheetFooter className="mt-2">
<Button variant="outline" onClick={requestClose} disabled={isSubmitting}>
{t("common.cancel")}
</Button>
{canWrite && (
<Button onClick={handleSubmit} loading={isSubmitting} disabled={isLoadingRecord}>
{mode === "create" ? t("workspace.unify.add_feedback_record") : t("common.save")}
<SheetFooter className="mt-2 sm:justify-between">
{isEditMode && canWrite && recordId ? (
<Button
variant="destructive"
onClick={() => setIsDeleteDialogOpen(true)}
disabled={isSubmitting || isLoadingRecord || isDeleting}>
{t("common.delete")}
</Button>
) : (
<span />
)}
<div className="flex gap-2">
<Button variant="outline" onClick={requestClose} disabled={isSubmitting || isDeleting}>
{t("common.cancel")}
</Button>
{canWrite && (
<Button
onClick={handleSubmit}
loading={isSubmitting}
disabled={isLoadingRecord || isDeleting}>
{mode === "create" ? t("workspace.unify.add_feedback_record") : t("common.save")}
</Button>
)}
</div>
</SheetFooter>
</SheetContent>
</Sheet>
@@ -809,6 +846,15 @@ export const FeedbackRecordFormDrawer = ({
onDecline={() => setIsDiscardDialogOpen(false)}
onConfirm={handleDiscardChanges}
/>
<DeleteDialog
open={isDeleteDialogOpen}
setOpen={setIsDeleteDialogOpen}
deleteWhat={t("workspace.unify.delete_feedback_record")}
text={t("workspace.unify.delete_feedback_record_confirmation")}
onDelete={handleDelete}
isDeleting={isDeleting}
/>
</>
);
};
@@ -0,0 +1,52 @@
"use client";
import { Trash2Icon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/modules/ui/components/button";
interface FeedbackRecordsTableToolbarLeftProps {
selectedCount: number;
recordsCount: number;
isEmpty: boolean;
onClearSelection: () => void;
onBulkDelete: () => void;
}
export const FeedbackRecordsTableToolbarLeft = ({
selectedCount,
recordsCount,
isEmpty,
onClearSelection,
onBulkDelete,
}: Readonly<FeedbackRecordsTableToolbarLeftProps>) => {
const { t } = useTranslation();
if (selectedCount > 0) {
return (
<div className="flex items-center gap-x-2 rounded-md bg-primary p-1 px-2 text-xs text-white">
<span className="lowercase">
{`${selectedCount} ${t("workspace.unify.feedback_records").toLowerCase()} ${t("common.selected")}`}
</span>
<span>|</span>
<Button variant="outline" size="sm" className="h-6 border-none px-2" onClick={onClearSelection}>
{t("common.clear_selection")}
</Button>
<span>|</span>
<Button variant="secondary" size="sm" className="h-6 gap-1 px-2" onClick={onBulkDelete}>
{t("common.delete")}
<Trash2Icon />
</Button>
</div>
);
}
if (isEmpty) {
return <span />;
}
return (
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count_loaded", { count: recordsCount })}
</p>
);
};
@@ -21,6 +21,8 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -29,9 +31,11 @@ import {
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { deleteFeedbackRecordAction } from "../actions";
import { formatSourceType } from "../lib/utils";
import { CsvImportModal } from "../sources/components/csv-import-modal";
import { FeedbackRecordFormDrawer } from "./feedback-record-form-drawer";
import { FeedbackRecordsTableToolbarLeft } from "./feedback-records-table-toolbar-left";
const RECORDS_PER_PAGE = 50;
@@ -87,8 +91,40 @@ export const FeedbackRecordsTable = ({
const [drawerRecordId, setDrawerRecordId] = useState<string | undefined>();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [csvImportSource, setCsvImportSource] = useState<{ id: string; name: string } | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const hasMore = Object.keys(cursors).length > 0;
const selectedCount = selectedIds.size;
const allOnPageSelected = records.length > 0 && records.every((record) => selectedIds.has(record.id));
const someOnPageSelected = records.some((record) => selectedIds.has(record.id));
const toggleAllOnPage = (checked: boolean) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (checked) {
records.forEach((record) => next.add(record.id));
} else {
records.forEach((record) => next.delete(record.id));
}
return next;
});
};
const toggleOne = (recordId: string, checked: boolean) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (checked) {
next.add(recordId);
} else {
next.delete(recordId);
}
return next;
});
};
const clearSelection = () => setSelectedIds(new Set());
const directories = useMemo(
() =>
@@ -160,6 +196,7 @@ export const FeedbackRecordsTable = ({
const mergedRecords = result.records.toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1));
setRecords(mergedRecords);
setCursors(result.newCursors);
setSelectedIds(new Set());
setIsRefreshing(false);
toast.success(t("workspace.unify.feedback_records_refreshed"), { id: toastId });
};
@@ -199,6 +236,56 @@ export const FeedbackRecordsTable = ({
const isEmpty = records.length === 0 && !isRefreshing;
const handleBulkDelete = async () => {
const ids = Array.from(selectedIds);
if (ids.length === 0) return;
setIsDeleting(true);
const CHUNK_SIZE = 5;
const failedIds: string[] = [];
try {
for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
const chunk = ids.slice(i, i + CHUNK_SIZE);
const results = await Promise.all(
chunk.map(async (recordId) => ({
recordId,
result: await deleteFeedbackRecordAction({ workspaceId, recordId }),
}))
);
results.forEach(({ recordId, result }) => {
if (!result?.data) failedIds.push(recordId);
});
}
const succeeded = ids.filter((id) => !failedIds.includes(id));
if (succeeded.length > 0) {
setRecords((prev) => prev.filter((record) => !succeeded.includes(record.id)));
setSelectedIds((prev) => {
const next = new Set(prev);
succeeded.forEach((id) => next.delete(id));
return next;
});
}
if (failedIds.length === 0) {
toast.success(
t("workspace.unify.feedback_records_deleted_successfully", { count: succeeded.length })
);
} else if (succeeded.length === 0) {
toast.error(t("workspace.unify.failed_to_delete_feedback_records"));
} else {
toast.error(
t("workspace.unify.feedback_records_partially_deleted", {
succeeded: succeeded.length,
total: ids.length,
})
);
}
} finally {
setIsDeleting(false);
setIsBulkDeleteDialogOpen(false);
}
};
const openEditDrawer = (recordId: string) => {
setDrawerMode("edit");
setDrawerRecordId(recordId);
@@ -213,19 +300,24 @@ export const FeedbackRecordsTable = ({
const hasCsvSources = csvSources.length > 0;
let headerCheckboxChecked: boolean | "indeterminate" = false;
if (allOnPageSelected) {
headerCheckboxChecked = true;
} else if (someOnPageSelected) {
headerCheckboxChecked = "indeterminate";
}
return (
<>
<div className="space-y-3">
<div className="flex items-center justify-between">
{isEmpty ? (
<span />
) : (
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count_loaded", {
count: records.length,
})}
</p>
)}
<FeedbackRecordsTableToolbarLeft
selectedCount={selectedCount}
recordsCount={records.length}
isEmpty={isEmpty}
onClearSelection={clearSelection}
onBulkDelete={() => setIsBulkDeleteDialogOpen(true)}
/>
<div className="flex items-center gap-2">
{canWrite &&
(hasCsvSources ? (
@@ -280,6 +372,13 @@ export const FeedbackRecordsTable = ({
<table className="w-full min-w-[900px]">
<thead>
<tr className="border-b border-slate-200 text-left text-sm text-slate-900 [&>th]:font-semibold">
<th className="w-10 px-4 py-3">
<Checkbox
aria-label={t("common.select_all")}
checked={headerCheckboxChecked}
onCheckedChange={(checked) => toggleAllOnPage(checked === true)}
/>
</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.collected_at")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_name")}</th>
@@ -292,7 +391,7 @@ export const FeedbackRecordsTable = ({
{isEmpty ? (
<tbody>
<tr>
<td colSpan={7}>
<td colSpan={8}>
<div className="flex h-32 items-center justify-center">
<p className="text-sm text-slate-500">{t("workspace.unify.no_feedback_records")}</p>
</div>
@@ -308,6 +407,8 @@ export const FeedbackRecordsTable = ({
workspaceId={workspaceId}
locale={i18n.resolvedLanguage ?? i18n.language ?? "en-US"}
t={t}
isSelected={selectedIds.has(record.id)}
onSelectChange={(checked) => toggleOne(record.id, checked)}
onClick={() => openEditDrawer(record.id)}
/>
))}
@@ -342,6 +443,15 @@ export const FeedbackRecordsTable = ({
onSuccess={handleRefresh}
/>
<DeleteDialog
open={isBulkDeleteDialogOpen}
setOpen={setIsBulkDeleteDialogOpen}
deleteWhat={t("workspace.unify.feedback_records")}
text={t("workspace.unify.delete_feedback_records_confirmation", { count: selectedCount })}
onDelete={handleBulkDelete}
isDeleting={isDeleting}
/>
{csvImportSource && (
<CsvImportModal
open={csvImportSource !== null}
@@ -363,12 +473,16 @@ const FeedbackRecordRow = ({
workspaceId,
locale,
t,
isSelected,
onSelectChange,
onClick,
}: {
record: FeedbackRecordData;
workspaceId: string;
locale: string;
t: TFunction;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onClick: () => void;
}) => {
const value = formatValue(record, t, locale);
@@ -379,10 +493,10 @@ const FeedbackRecordRow = ({
return (
<tr
className="cursor-pointer text-sm text-slate-700 transition-colors focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400"
className={`cursor-pointer text-sm text-slate-700 transition-colors focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 ${isSelected ? "bg-slate-50" : ""}`}
tabIndex={0}
role="button"
aria-label={record.field_label ?? record.field_id}
aria-selected={isSelected}
onClick={onClick}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
@@ -390,6 +504,16 @@ const FeedbackRecordRow = ({
onClick();
}
}}>
<td
className="w-10 px-4 py-3"
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}>
<Checkbox
aria-label={record.field_label ?? record.field_id}
checked={isSelected}
onCheckedChange={(checked) => onSelectChange(checked === true)}
/>
</td>
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
{formatDateTimeForDisplay(new Date(record.collected_at), locale)}
</td>
@@ -18,6 +18,7 @@ import { Alert } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
@@ -425,7 +426,7 @@ export const CreateConnectorModal = ({
<DialogDescription>{getDialogDescription(currentStep, selectedType, t)}</DialogDescription>
</DialogHeader>
<div className="py-4">
<DialogBody className="min-h-0 min-w-0 overflow-y-auto py-4">
{currentStep === "selectType" && (
<ConnectorTypeSelector
selectedType={selectedType}
@@ -593,7 +594,7 @@ export const CreateConnectorModal = ({
)}
</div>
)}
</div>
</DialogBody>
<DialogFooter>
{currentStep === "mapping" && (
@@ -78,3 +78,10 @@ export const ZUpdateFeedbackRecordAction = z.object({
});
export type TUpdateFeedbackRecordAction = z.infer<typeof ZUpdateFeedbackRecordAction>;
export const ZDeleteFeedbackRecordAction = z.object({
workspaceId: ZId,
recordId: ZFeedbackRecordId,
});
export type TDeleteFeedbackRecordAction = z.infer<typeof ZDeleteFeedbackRecordAction>;
+48
View File
@@ -4,6 +4,7 @@ import FormbricksHub from "@formbricks/hub";
import {
createFeedbackRecord,
createFeedbackRecordsBatch,
deleteFeedbackRecord,
getFeedbackRecordTenant,
listFeedbackRecords,
retrieveFeedbackRecord,
@@ -278,6 +279,53 @@ describe("hub service", () => {
});
});
describe("deleteFeedbackRecord", () => {
test("returns config error when getHubClient returns null", async () => {
vi.mocked(getHubClient).mockReturnValue(null);
const result = await deleteFeedbackRecord("rec-1");
expect(result.data).toBeNull();
expect(result.error?.message).toContain("HUB_API_KEY");
});
test("returns data when client.delete resolves", async () => {
const deleteSpy = vi.fn().mockResolvedValue(undefined);
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { delete: deleteSpy },
} as any);
const result = await deleteFeedbackRecord("rec-1");
expect(deleteSpy).toHaveBeenCalledWith("rec-1");
expect(result.data).toEqual({ deleted: true });
expect(result.error).toBeNull();
});
test("returns error when client.delete throws APIError", async () => {
const apiError = new (FormbricksHub as any).APIError("Forbidden", 403);
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { delete: vi.fn().mockRejectedValue(apiError) },
} as any);
const result = await deleteFeedbackRecord("rec-1");
expect(result.data).toBeNull();
expect(result.error).toMatchObject({ status: 403, message: "Forbidden" });
});
test("returns error when client.delete throws non-API error", async () => {
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { delete: vi.fn().mockRejectedValue(new Error("network")) },
} as any);
const result = await deleteFeedbackRecord("rec-1");
expect(result.data).toBeNull();
expect(result.error).toMatchObject({ status: 0, message: "network" });
});
});
describe("createFeedbackRecordsBatch", () => {
test("returns all errors when getHubClient returns null", async () => {
vi.mocked(getHubClient).mockReturnValue(null);
+25
View File
@@ -98,6 +98,31 @@ export const updateFeedbackRecord = async (
}
};
export type HubFeedbackRecordDeleteResult = {
data: { deleted: true } | null;
error: HubError | null;
};
/**
* Delete a single feedback record in the Hub by id.
*/
export const deleteFeedbackRecord = async (id: string): Promise<HubFeedbackRecordDeleteResult> => {
const client = getHubClient();
if (!client) {
return { data: null, error: { ...NO_CONFIG_ERROR } };
}
try {
await client.feedbackRecords.delete(id);
return { data: { deleted: true }, error: null };
} catch (err) {
logger.warn({ err, id }, "Hub: deleteFeedbackRecord failed");
const status = err instanceof FormbricksHub.APIError ? err.status : 0;
const message = getErrorMessage(err);
return { data: null, error: { status, message, detail: message } };
}
};
export type ListFeedbackRecordsResult = {
data: FeedbackRecordListResponse | null;
error: HubError | null;