diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 3dc10210ab10..2410537c9bc1 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -2186,12 +2186,12 @@ checksums: workspace/look/advanced_styling_field_track_bg_description: 8a56258273dfe49e83fe752ea9e8daed workspace/look/advanced_styling_field_track_height: 9ce57cb4583039c224a37e013efb6b8f workspace/look/advanced_styling_field_track_height_description: 90243a4374e15d9118ad0fd93d5f3614 - workspace/look/advanced_styling_field_upper_label_color: 896bc12d6c77f162abf189589e11287e - workspace/look/advanced_styling_field_upper_label_color_description: 1bc4b92821ff694c18396a872ca0fb60 - workspace/look/advanced_styling_field_upper_label_size: 0d915cf63b845a0c4c6004efc731babf - workspace/look/advanced_styling_field_upper_label_size_description: 8e9e348318b72837c9e04f81701b2fb6 - workspace/look/advanced_styling_field_upper_label_weight: 9128dcfea0e5b1e0fdf4f5902f6ea2d6 - workspace/look/advanced_styling_field_upper_label_weight_description: 3a9006a41ab84aea98d4291a6d5db9db + workspace/look/advanced_styling_field_upper_label_color: 2767a5db32742073a01aac16488e93dc + workspace/look/advanced_styling_field_upper_label_color_description: 58f43ce21b7f6539cc937aa80c7e8060 + workspace/look/advanced_styling_field_upper_label_size: 3342babd1df61a3bdf7a3284137f7c24 + workspace/look/advanced_styling_field_upper_label_size_description: 867a89a79ed7ac7f1c6b0f3481a67f26 + workspace/look/advanced_styling_field_upper_label_weight: a9a0de9e840518d282cfdbcb02d059b5 + workspace/look/advanced_styling_field_upper_label_weight_description: 3cee88e1c8e75548dcb6004f0e44f31c workspace/look/advanced_styling_section_buttons: 3b44d6e2800e7bf3f133f1bce435f4c2 workspace/look/advanced_styling_section_headlines: 6def704c0ac2ecb5951400c806856a41 workspace/look/advanced_styling_section_inputs: 76bbeb561122a72fd3ec8c49eff7c563 @@ -2465,7 +2465,6 @@ checksums: workspace/settings/feedback_directories/archive_not_allowed: 3ffe3336572a633406858887de60a470 workspace/settings/feedback_directories/are_you_sure_you_want_to_archive: d249e6e8bc0345835a13f70856eb1c30 workspace/settings/feedback_directories/assign_workspaces_description: 182522a7d2eedb04b2770b1555adfb41 - workspace/settings/feedback_directories/connectors_description: 633588d48b503578e8d813f0e15bb78a workspace/settings/feedback_directories/create_feedback_directory: c178dd6dbd702398df3ac08a9fa43324 workspace/settings/feedback_directories/description: 2054e053da5c3708388aa538a7bcb180 workspace/settings/feedback_directories/directory_archived_successfully: fba5b99ced59d0546c8f2241c092a5dd @@ -2477,21 +2476,22 @@ checksums: workspace/settings/feedback_directories/directory_unarchived_successfully: 08d56e260decc62fe664b50ab774b728 workspace/settings/feedback_directories/directory_updated_successfully: 638cb6c92f535328d809274cf2be4d7d workspace/settings/feedback_directories/empty_state: 5c78f7469c3730895735e00b008df2d3 - workspace/settings/feedback_directories/error_directory_has_connectors: 5d20dc2f62b1113b5f4e813e86e302d5 + workspace/settings/feedback_directories/error_directory_has_feedback_sources: 5d20dc2f62b1113b5f4e813e86e302d5 workspace/settings/feedback_directories/error_directory_name_duplicate: 723aeaee8d7fc94ee5f6bb7157208058 workspace/settings/feedback_directories/error_directory_name_required: 0f42d7292979006a1069063ab213b8e3 workspace/settings/feedback_directories/error_directory_workspaces_invalid_org: 477b5c1a466c4194668544ffd42ec9bf workspace/settings/feedback_directories/error_workspace_already_assigned: 6f851ad28a4e91e48fe13da917ea1ae0 + workspace/settings/feedback_directories/feedback_sources_description: 633588d48b503578e8d813f0e15bb78a workspace/settings/feedback_directories/grant_access_confirm: 0b040e675cf418d8051d7ad92096ccdd workspace/settings/feedback_directories/grant_workspace_access_title: e959a57192546b1d5ea7062b0ace6aec workspace/settings/feedback_directories/grant_workspace_access_warning: 6c3634d8b5fd61d97068e69d7a0aa8ca workspace/settings/feedback_directories/nav_label: cf9a57b3cbac0f04b98e06fb693e986e workspace/settings/feedback_directories/no_access: 707627df25fbaa28f18aa0f0d03dcb81 - workspace/settings/feedback_directories/no_connectors: ccc725ff9a82a7b8ab68de735490a9b9 + workspace/settings/feedback_directories/no_feedback_sources: ccc725ff9a82a7b8ab68de735490a9b9 workspace/settings/feedback_directories/no_unassigned_workspaces_description: c96a260b582e6c930de72e6e69f9a9a6 workspace/settings/feedback_directories/no_unassigned_workspaces_title: 458d4289d73d799561bec26a0bb1a1a3 - workspace/settings/feedback_directories/pause_connectors_confirmation_description: 0e30f827576b931651b9eae44e00279b - workspace/settings/feedback_directories/pause_connectors_confirmation_title: da1950dbb9ce62caa65c87ae8b88b1a1 + workspace/settings/feedback_directories/pause_feedback_sources_confirmation_description: 0e30f827576b931651b9eae44e00279b + workspace/settings/feedback_directories/pause_feedback_sources_confirmation_title: da1950dbb9ce62caa65c87ae8b88b1a1 workspace/settings/feedback_directories/select_workspaces_placeholder: 7d8c8f5910b264525f73bd32107765db workspace/settings/feedback_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de workspace/settings/feedback_directories/title: cf9a57b3cbac0f04b98e06fb693e986e @@ -3528,14 +3528,6 @@ checksums: workspace/unify/collected_at: b41902ddb4586ba4a4611d726b5014aa workspace/unify/configure_import: 71d550661f7e9fe322b60e7e870aa2fd workspace/unify/configure_mapping: c794411c50bc511f8fc332def0e4e2f9 - workspace/unify/connector_created_successfully: b04ab49a6ca938dd7c46bfb124cf4925 - workspace/unify/connector_deleted_successfully: 4ef727e15effca6390190c08219a9176 - workspace/unify/connector_duplicated_successfully: 6b9e3ae0f97ae78a5db25582f153c898 - workspace/unify/connector_name: 157675beca12efcd8ec512c5256b1a61 - workspace/unify/connector_name_hint: 1d60dcd2bd03f751b2d52326d8902e4e - workspace/unify/connector_status_updated_successfully: 404d548d43e618df82f4924ffaa4ea62 - workspace/unify/connector_updated_successfully: 2d0bc4c50ec3c195eaec2123b70ba0ba - workspace/unify/connectors: ee5fd409e10c96357b36d5b6535b60c7 workspace/unify/create_mapping: cbe8c951e7819f574ca7d793920b2b60 workspace/unify/created_by: 6775c2fa7d495fea48f1ad816daea93b workspace/unify/csv_advanced: 16e9b03eda6f6c1324cce54b96bbccd6 @@ -3572,8 +3564,8 @@ checksums: workspace/unify/csv_unmapped_columns_explainer: 58cdfc3c06c141f156288dc434f8e0ca workspace/unify/custom_source_type: d931a8a74d3a5becd568e398107979da workspace/unify/custom_source_type_placeholder: f139e3e5d70dbf426d7c6b5ab2b198cc - workspace/unify/default_connector_name_csv: ef4060fef24c4fec064987b9d2a9fa4b - workspace/unify/default_connector_name_formbricks: 9088b01e1085b1cf42412597ebeea051 + workspace/unify/default_source_name_csv: ef4060fef24c4fec064987b9d2a9fa4b + workspace/unify/default_source_name_formbricks: 9088b01e1085b1cf42412597ebeea051 workspace/unify/delete_feedback_record: 86d7262c000cfb1f91ea373036cd3616 workspace/unify/delete_feedback_record_confirmation: dd2b12e75cb52a73f92c997c347a8f36 workspace/unify/delete_feedback_records_confirmation: cd8a4ba828963fb6dab5c96606565079 @@ -3585,12 +3577,12 @@ checksums: workspace/unify/edit_source_connection: eee85426384e6569665ac2b342ca02e4 workspace/unify/enter_name_for_source: de6d02a0a8ccc99204ad831ca6dcdbd3 workspace/unify/enum: 96fc644f35edd6b1c09d1d503f078acc - workspace/unify/error_connector_field_mapping_duplicate: 4e507ae4a99f53dfa75d849d32566bf2 - workspace/unify/error_connector_formbricks_mapping_duplicate: 48524e22fa33bd0b2829f7aea49c711b - workspace/unify/error_connector_name_duplicate: 56a1a05212e87dff21261d1db90fdb11 - workspace/unify/error_connector_name_required: b2d5f79f6126e23128d7bef0c1736ff2 - workspace/unify/error_connector_questions_required: d7d8e388959ab83a9195ba63bebf6516 - workspace/unify/error_connector_survey_required: 1f49086dfb874307aae1136e88c3d514 + workspace/unify/error_source_field_mapping_duplicate: 4e507ae4a99f53dfa75d849d32566bf2 + workspace/unify/error_source_formbricks_mapping_duplicate: 48524e22fa33bd0b2829f7aea49c711b + workspace/unify/error_source_name_duplicate: 56a1a05212e87dff21261d1db90fdb11 + workspace/unify/error_source_name_required: b2d5f79f6126e23128d7bef0c1736ff2 + workspace/unify/error_source_questions_required: d7d8e388959ab83a9195ba63bebf6516 + workspace/unify/error_source_survey_required: 1f49086dfb874307aae1136e88c3d514 workspace/unify/failed_to_delete_feedback_records: 6096404d164fda196734675885e278c3 workspace/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8 workspace/unify/feedback_directory: 156fa9957e1ee5eadf1f44226a2365e4 @@ -3679,8 +3671,13 @@ checksums: workspace/unify/source_connect_csv_description: 8db7e23acbfa89920694d61ac9585b44 workspace/unify/source_connect_feedback_record_mcp_description: a3f56e2a6e403f4021e83f1b1a466d95 workspace/unify/source_connect_formbricks_description: 2d7aa0bb9b9f9251184c0e60ee409a8a + workspace/unify/source_created_successfully: b04ab49a6ca938dd7c46bfb124cf4925 + workspace/unify/source_deleted_successfully: 4ef727e15effca6390190c08219a9176 + workspace/unify/source_duplicated_successfully: 6b9e3ae0f97ae78a5db25582f153c898 workspace/unify/source_id: 134a9a7d473508c5623ac724a5ba4be9 workspace/unify/source_name: 157675beca12efcd8ec512c5256b1a61 + workspace/unify/source_name_hint: 1d60dcd2bd03f751b2d52326d8902e4e + workspace/unify/source_status_updated_successfully: 404d548d43e618df82f4924ffaa4ea62 workspace/unify/source_type: d1ff69af76c687eb189db72030717570 workspace/unify/source_type_cannot_be_changed: bb5232c6e92df7f88731310fabbb1eb1 workspace/unify/source_type_label_feedback_form: 65e0f65a81cca1c9034943ee6a95c3f4 @@ -3691,6 +3688,8 @@ checksums: workspace/unify/source_type_label_support: 55aab5fd0f31a9cb055a2edeeedfaf63 workspace/unify/source_type_label_survey: b659d270a53dada994d926e0cc6e9a54 workspace/unify/source_type_label_usability_test: 33a7b1e9ee8b975008c48e0a524f0e57 + workspace/unify/source_updated_successfully: 2d0bc4c50ec3c195eaec2123b70ba0ba + workspace/unify/sources: ee5fd409e10c96357b36d5b6535b60c7 workspace/unify/status_error: 3c95bcb32c2104b99a46f5b3dd015248 workspace/unify/status_live_sync: 7e794257419414f57d34845ef38d0939 workspace/unify/status_ready: 437c0eea608e15ad5cdab94bde2f4b48 diff --git a/apps/web/lib/connector/service.test.ts b/apps/web/lib/connector/service.test.ts deleted file mode 100644 index 1169f8123107..000000000000 --- a/apps/web/lib/connector/service.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -import { Prisma } from "@prisma/client"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { prisma } from "@formbricks/database"; -import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { - createConnectorWithMappings, - deleteConnector, - getConnectorsBySurveyId, - getConnectorsWithMappings, - updateConnector, - updateConnectorWithMappings, -} from "./service"; - -vi.mock("@formbricks/database", () => ({ - prisma: { - connector: { - findMany: vi.fn(), - findUniqueOrThrow: vi.fn(), - create: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - }, - connectorFormbricksMapping: { - create: vi.fn(), - deleteMany: vi.fn(), - }, - connectorFieldMapping: { - create: vi.fn(), - deleteMany: vi.fn(), - }, - $transaction: vi.fn(), - }, -})); - -vi.mock("@/lib/utils/validate", () => ({ - validateInputs: vi.fn(), -})); - -const ENV_ID = "clxxxxxxxxxxxxxxxx001"; -const CONNECTOR_ID = "clxxxxxxxxxxxxxxxx002"; -const SURVEY_ID = "clxxxxxxxxxxxxxxxx003"; -const FRD_ID = "clxxxxxxxxxxxxxxxx004"; -const NOW = new Date("2026-02-24T10:00:00.000Z"); - -const mockConnector = { - id: CONNECTOR_ID, - createdAt: NOW, - updatedAt: NOW, - name: "Test Connector", - type: "formbricks_survey" as const, - status: "active" as const, - workspaceId: ENV_ID, - lastSyncAt: null, - createdBy: null, -}; - -const mockConnectorWithMappingsFromDb = { - ...mockConnector, - creator: null, - formbricksMappings: [ - { - id: "mapping-1", - createdAt: NOW, - connectorId: CONNECTOR_ID, - workspaceId: ENV_ID, - surveyId: SURVEY_ID, - elementId: "el-1", - hubFieldType: "text", - customFieldLabel: null, - }, - ], - fieldMappings: [], -}; - -const mockConnectorWithMappings = { - ...mockConnector, - creatorName: null, - formbricksMappings: mockConnectorWithMappingsFromDb.formbricksMappings, - fieldMappings: [], -}; - -describe("getConnectorsWithMappings", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("returns connectors for the given environment", async () => { - vi.mocked(prisma.connector.findMany).mockResolvedValue([mockConnectorWithMappingsFromDb] as never); - - const result = await getConnectorsWithMappings(ENV_ID); - - expect(prisma.connector.findMany).toHaveBeenCalledWith( - expect.objectContaining({ - where: { workspaceId: ENV_ID }, - orderBy: { createdAt: "desc" }, - }) - ); - expect(result).toHaveLength(1); - expect(result[0].id).toBe(CONNECTOR_ID); - }); - - test("applies pagination when page is provided", async () => { - vi.mocked(prisma.connector.findMany).mockResolvedValue([] as never); - - await getConnectorsWithMappings(ENV_ID, 2); - - expect(prisma.connector.findMany).toHaveBeenCalledWith( - expect.objectContaining({ - take: expect.any(Number), - skip: expect.any(Number), - }) - ); - }); - - test("returns empty array when no connectors exist", async () => { - vi.mocked(prisma.connector.findMany).mockResolvedValue([] as never); - - const result = await getConnectorsWithMappings(ENV_ID); - expect(result).toEqual([]); - }); - - test("throws DatabaseError on Prisma error", async () => { - vi.mocked(prisma.connector.findMany).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("connection error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect(getConnectorsWithMappings(ENV_ID)).rejects.toThrow(DatabaseError); - }); -}); - -describe("getConnectorsBySurveyId", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("returns active formbricks connectors linked to the survey", async () => { - vi.mocked(prisma.connector.findMany).mockResolvedValue([mockConnectorWithMappingsFromDb] as never); - - const result = await getConnectorsBySurveyId(SURVEY_ID); - - expect(prisma.connector.findMany).toHaveBeenCalledWith( - expect.objectContaining({ - where: { - type: "formbricks_survey", - status: "active", - formbricksMappings: { some: { surveyId: SURVEY_ID } }, - }, - }) - ); - expect(result).toHaveLength(1); - }); - - test("returns empty when no connectors match", async () => { - vi.mocked(prisma.connector.findMany).mockResolvedValue([] as never); - - const result = await getConnectorsBySurveyId(SURVEY_ID); - expect(result).toEqual([]); - }); - - test("throws DatabaseError on Prisma error", async () => { - vi.mocked(prisma.connector.findMany).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("DB error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect(getConnectorsBySurveyId(SURVEY_ID)).rejects.toThrow(DatabaseError); - }); -}); - -describe("updateConnector", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("updates connector name and returns the result", async () => { - const updated = { ...mockConnector, name: "Renamed" }; - vi.mocked(prisma.connector.update).mockResolvedValue(updated as never); - - const result = await updateConnector(CONNECTOR_ID, ENV_ID, { name: "Renamed" }); - - expect(prisma.connector.update).toHaveBeenCalledWith( - expect.objectContaining({ - where: { id: CONNECTOR_ID, workspaceId: ENV_ID }, - data: expect.objectContaining({ name: "Renamed" }), - }) - ); - expect(result.name).toBe("Renamed"); - }); - - test("updates connector status", async () => { - const updated = { ...mockConnector, status: "paused" }; - vi.mocked(prisma.connector.update).mockResolvedValue(updated as never); - - const result = await updateConnector(CONNECTOR_ID, ENV_ID, { status: "paused" }); - expect(result.status).toBe("paused"); - }); - - test("throws ResourceNotFoundError when connector does not exist", async () => { - vi.mocked(prisma.connector.update).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Not found", { - code: "P2015", - clientVersion: "5.0.0", - }) - ); - - await expect(updateConnector(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow(ResourceNotFoundError); - }); - - test("throws DatabaseError on generic Prisma error", async () => { - vi.mocked(prisma.connector.update).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("DB error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect(updateConnector(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow(DatabaseError); - }); - - test("rethrows non-Prisma errors", async () => { - vi.mocked(prisma.connector.update).mockRejectedValue(new Error("unexpected")); - - await expect(updateConnector(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow("unexpected"); - }); -}); - -describe("deleteConnector", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test("deletes the connector and returns it", async () => { - vi.mocked(prisma.connector.delete).mockResolvedValue(mockConnector as never); - - const result = await deleteConnector(CONNECTOR_ID, ENV_ID); - - expect(prisma.connector.delete).toHaveBeenCalledWith( - expect.objectContaining({ - where: { id: CONNECTOR_ID, workspaceId: ENV_ID }, - }) - ); - expect(result.id).toBe(CONNECTOR_ID); - }); - - test("throws ResourceNotFoundError when connector does not exist", async () => { - vi.mocked(prisma.connector.delete).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Not found", { - code: "P2015", - clientVersion: "5.0.0", - }) - ); - - await expect(deleteConnector(CONNECTOR_ID, ENV_ID)).rejects.toThrow(ResourceNotFoundError); - }); - - test("throws DatabaseError on generic Prisma error", async () => { - vi.mocked(prisma.connector.delete).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("DB error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect(deleteConnector(CONNECTOR_ID, ENV_ID)).rejects.toThrow(DatabaseError); - }); -}); - -describe("createConnectorWithMappings", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const setupTransaction = () => { - const txMethods = { - connector: { - create: vi.fn(), - findUniqueOrThrow: vi.fn(), - }, - connectorFormbricksMapping: { - create: vi.fn(), - }, - connectorFieldMapping: { - create: vi.fn(), - }, - }; - - vi.mocked(prisma.$transaction).mockImplementation(async (fn) => { - return (fn as (tx: typeof txMethods) => Promise)(txMethods); - }); - - return txMethods; - }; - - test("creates connector without mappings", async () => { - const tx = setupTransaction(); - tx.connector.create.mockResolvedValue({ id: CONNECTOR_ID, workspaceId: ENV_ID }); - tx.connector.findUniqueOrThrow.mockResolvedValue(mockConnectorWithMappingsFromDb); - - const result = await createConnectorWithMappings(ENV_ID, { - name: "New", - type: "formbricks_survey", - feedbackDirectoryId: FRD_ID, - }); - - expect(tx.connector.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - name: "New", - type: "formbricks_survey", - workspaceId: ENV_ID, - feedbackDirectoryId: FRD_ID, - }, - }) - ); - expect(tx.connectorFormbricksMapping.create).not.toHaveBeenCalled(); - expect(tx.connectorFieldMapping.create).not.toHaveBeenCalled(); - expect(result).toEqual(mockConnectorWithMappings); - }); - - test("creates connector with formbricks mappings", async () => { - const tx = setupTransaction(); - tx.connector.create.mockResolvedValue({ id: CONNECTOR_ID, workspaceId: ENV_ID }); - tx.connectorFormbricksMapping.create.mockResolvedValue({}); - tx.connector.findUniqueOrThrow.mockResolvedValue(mockConnectorWithMappingsFromDb); - - await createConnectorWithMappings( - ENV_ID, - { name: "FB", type: "formbricks_survey", feedbackDirectoryId: FRD_ID }, - { - type: "formbricks_survey", - mappings: [ - { surveyId: SURVEY_ID, elementId: "el-1", hubFieldType: "text" }, - { surveyId: SURVEY_ID, elementId: "el-2", hubFieldType: "nps" }, - ], - } - ); - - expect(tx.connectorFormbricksMapping.create).toHaveBeenCalledTimes(2); - expect(tx.connectorFormbricksMapping.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - connectorId: CONNECTOR_ID, - workspaceId: ENV_ID, - surveyId: SURVEY_ID, - elementId: "el-1", - hubFieldType: "text", - }), - }) - ); - }); - - test("creates connector with field mappings", async () => { - const tx = setupTransaction(); - tx.connector.create.mockResolvedValue({ id: CONNECTOR_ID, workspaceId: ENV_ID }); - tx.connectorFieldMapping.create.mockResolvedValue({}); - tx.connector.findUniqueOrThrow.mockResolvedValue({ - ...mockConnector, - formbricksMappings: [], - fieldMappings: [], - }); - - await createConnectorWithMappings( - ENV_ID, - { name: "CSV", type: "csv", feedbackDirectoryId: FRD_ID }, - { - type: "field", - mappings: [{ sourceFieldId: "col-1", targetFieldId: "value_text" }], - } - ); - - expect(tx.connectorFieldMapping.create).toHaveBeenCalledTimes(1); - expect(tx.connectorFieldMapping.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - connectorId: CONNECTOR_ID, - workspaceId: ENV_ID, - sourceFieldId: "col-1", - targetFieldId: "value_text", - }), - }) - ); - }); - - test("throws CONNECTOR_NAME_DUPLICATE on Connector name unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "name"] }, - }) - ); - - await expect( - createConnectorWithMappings(ENV_ID, { - name: "Dup", - type: "formbricks_survey", - feedbackDirectoryId: FRD_ID, - }) - ).rejects.toThrow(new InvalidInputError("CONNECTOR_NAME_DUPLICATE")); - }); - - test("throws CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE on mapping unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "connectorId", "surveyId", "elementId"] }, - }) - ); - - await expect( - createConnectorWithMappings(ENV_ID, { - name: "Dup mapping", - type: "formbricks_survey", - feedbackDirectoryId: FRD_ID, - }) - ).rejects.toThrow(new InvalidInputError("CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE")); - }); - - test("throws CONNECTOR_FIELD_MAPPING_DUPLICATE on field mapping unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "connectorId", "sourceFieldId", "targetFieldId"] }, - }) - ); - - await expect( - createConnectorWithMappings(ENV_ID, { - name: "Dup field mapping", - type: "csv", - feedbackDirectoryId: FRD_ID, - }) - ).rejects.toThrow(new InvalidInputError("CONNECTOR_FIELD_MAPPING_DUPLICATE")); - }); - - test("throws DatabaseError on generic Prisma error", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("DB error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect( - createConnectorWithMappings(ENV_ID, { name: "Fail", type: "csv", feedbackDirectoryId: FRD_ID }) - ).rejects.toThrow(DatabaseError); - }); -}); - -describe("updateConnectorWithMappings", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const setupTransaction = () => { - const txMethods = { - connector: { - update: vi.fn(), - findUniqueOrThrow: vi.fn(), - }, - connectorFormbricksMapping: { - create: vi.fn(), - deleteMany: vi.fn(), - }, - connectorFieldMapping: { - create: vi.fn(), - deleteMany: vi.fn(), - }, - }; - - vi.mocked(prisma.$transaction).mockImplementation(async (fn) => { - return (fn as (tx: typeof txMethods) => Promise)(txMethods); - }); - - return txMethods; - }; - - test("updates connector name without changing mappings", async () => { - const tx = setupTransaction(); - tx.connector.update.mockResolvedValue(undefined); - tx.connector.findUniqueOrThrow.mockResolvedValue(mockConnectorWithMappingsFromDb); - - const result = await updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "Updated" }); - - expect(tx.connector.update).toHaveBeenCalledWith( - expect.objectContaining({ - where: { id: CONNECTOR_ID, workspaceId: ENV_ID }, - data: expect.objectContaining({ name: "Updated" }), - }) - ); - expect(tx.connectorFormbricksMapping.deleteMany).not.toHaveBeenCalled(); - expect(tx.connectorFieldMapping.deleteMany).not.toHaveBeenCalled(); - expect(result).toEqual(mockConnectorWithMappings); - }); - - test("replaces formbricks mappings when provided", async () => { - const tx = setupTransaction(); - tx.connector.update.mockResolvedValue(undefined); - tx.connectorFormbricksMapping.deleteMany.mockResolvedValue({ count: 1 }); - tx.connectorFormbricksMapping.create.mockResolvedValue({}); - tx.connector.findUniqueOrThrow.mockResolvedValue(mockConnectorWithMappingsFromDb); - - await updateConnectorWithMappings( - CONNECTOR_ID, - ENV_ID, - { name: "Updated" }, - { - type: "formbricks_survey", - mappings: [{ surveyId: SURVEY_ID, elementId: "el-new", hubFieldType: "nps" }], - } - ); - - expect(tx.connectorFormbricksMapping.deleteMany).toHaveBeenCalledWith({ - where: { connectorId: CONNECTOR_ID, workspaceId: ENV_ID }, - }); - expect(tx.connectorFormbricksMapping.create).toHaveBeenCalledTimes(1); - }); - - test("replaces field mappings when provided", async () => { - const tx = setupTransaction(); - tx.connector.update.mockResolvedValue(undefined); - tx.connectorFieldMapping.deleteMany.mockResolvedValue({ count: 1 }); - tx.connectorFieldMapping.create.mockResolvedValue({}); - tx.connector.findUniqueOrThrow.mockResolvedValue({ - ...mockConnector, - formbricksMappings: [], - fieldMappings: [], - }); - - await updateConnectorWithMappings( - CONNECTOR_ID, - ENV_ID, - { name: "CSV Updated" }, - { - type: "field", - mappings: [{ sourceFieldId: "col-x", targetFieldId: "value_number" }], - } - ); - - expect(tx.connectorFieldMapping.deleteMany).toHaveBeenCalledWith({ - where: { connectorId: CONNECTOR_ID, workspaceId: ENV_ID }, - }); - expect(tx.connectorFieldMapping.create).toHaveBeenCalledTimes(1); - }); - - test("throws ResourceNotFoundError when connector does not exist", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Not found", { - code: "P2015", - clientVersion: "5.0.0", - }) - ); - - await expect(updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow( - ResourceNotFoundError - ); - }); - - test("throws CONNECTOR_NAME_DUPLICATE on Connector name unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "name"] }, - }) - ); - - await expect(updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "Dup" })).rejects.toThrow( - new InvalidInputError("CONNECTOR_NAME_DUPLICATE") - ); - }); - - test("throws CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE on formbricks mapping unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "connectorId", "surveyId", "elementId"] }, - }) - ); - - await expect(updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow( - new InvalidInputError("CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE") - ); - }); - - test("throws CONNECTOR_FIELD_MAPPING_DUPLICATE on field mapping unique violation", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("Unique constraint", { - code: "P2002", - clientVersion: "5.0.0", - meta: { target: ["workspaceId", "connectorId", "sourceFieldId", "targetFieldId"] }, - }) - ); - - await expect(updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow( - new InvalidInputError("CONNECTOR_FIELD_MAPPING_DUPLICATE") - ); - }); - - test("throws DatabaseError on generic Prisma error", async () => { - vi.mocked(prisma.$transaction).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError("DB error", { - code: "P1001", - clientVersion: "5.0.0", - }) - ); - - await expect(updateConnectorWithMappings(CONNECTOR_ID, ENV_ID, { name: "x" })).rejects.toThrow( - DatabaseError - ); - }); -}); diff --git a/apps/web/lib/connector/actions.ts b/apps/web/lib/feedback-source/actions.ts similarity index 75% rename from apps/web/lib/connector/actions.ts rename to apps/web/lib/feedback-source/actions.ts index 862af49b2e79..5601065e4814 100644 --- a/apps/web/lib/connector/actions.ts +++ b/apps/web/lib/feedback-source/actions.ts @@ -4,15 +4,15 @@ import { z } from "zod"; import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; +import { AuthorizationError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { - TConnectorWithMappings, + TFeedbackSourceWithMappings, THubFieldType, - ZConnectorCreateInput, - ZConnectorFieldMappingCreateInput, - ZConnectorUpdateInput, + ZFeedbackSourceCreateInput, + ZFeedbackSourceFieldMappingCreateInput, + ZFeedbackSourceUpdateInput, getHubFieldTypeFromElementType, -} from "@formbricks/types/connector"; -import { AuthorizationError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +} from "@formbricks/types/feedback-source"; import { getResponseCountBySurveyId } from "@/lib/response/service"; import { getSurvey } from "@/lib/survey/service"; import { getElementsFromBlocks } from "@/lib/survey/utils"; @@ -20,7 +20,7 @@ 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 { - getOrganizationIdFromConnectorId, + getOrganizationIdFromFeedbackSourceId, getOrganizationIdFromSurveyId, getOrganizationIdFromWorkspaceId, } from "@/lib/utils/helper"; @@ -31,11 +31,11 @@ import { importCsvData } from "./csv-import"; import { importHistoricalResponses } from "./import"; import { TMappingsInput, - createConnectorWithMappings, - deleteConnector, - getConnectorWithMappingsById, - updateConnector, - updateConnectorWithMappings, + createFeedbackSourceWithMappings, + deleteFeedbackSource, + getFeedbackSourceWithMappingsById, + updateFeedbackSource, + updateFeedbackSourceWithMappings, } from "./service"; import { formatMissingRequiredCsvFieldMappingsMessage, @@ -43,22 +43,22 @@ import { sanitizeCsvFieldMappings, } from "./utils"; -const ZDeleteConnectorAction = z.object({ - connectorId: ZId, +const ZDeleteFeedbackSourceAction = z.object({ + feedbackSourceId: ZId, workspaceId: ZId, }); -export const deleteConnectorAction = authenticatedActionClient - .inputSchema(ZDeleteConnectorAction) +export const deleteFeedbackSourceAction = authenticatedActionClient + .inputSchema(ZDeleteFeedbackSourceAction) .action( async ({ ctx, parsedInput, }: { ctx: AuthenticatedActionClientCtx; - parsedInput: z.infer; + parsedInput: z.infer; }) => { - const organizationId = await getOrganizationIdFromConnectorId(parsedInput.connectorId); + const organizationId = await getOrganizationIdFromFeedbackSourceId(parsedInput.feedbackSourceId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, @@ -75,7 +75,7 @@ export const deleteConnectorAction = authenticatedActionClient ], }); - return deleteConnector(parsedInput.connectorId, parsedInput.workspaceId); + return deleteFeedbackSource(parsedInput.feedbackSourceId, parsedInput.workspaceId); } ); @@ -94,7 +94,10 @@ const resolveSurveyMappings = async ( return elementIds.flatMap((elementId) => { const element = elementMap.get(elementId); if (!element) { - logger.warn({ surveyId, elementId }, "Skipping unknown elementId when building connector mappings"); + logger.warn( + { surveyId, elementId }, + "Skipping unknown elementId when building feedbackSource mappings" + ); return []; } @@ -102,7 +105,7 @@ const resolveSurveyMappings = async ( if (!hubFieldType) { logger.warn( { surveyId, elementId, elementType: element.type }, - "Skipping unmappable element type when building connector mappings" + "Skipping unmappable element type when building feedbackSource mappings" ); return []; } @@ -119,7 +122,7 @@ const resolveFormbricksMappingsInput = async ( ); const flattenedMappings = allMappings.flat(); if (flattenedMappings.length === 0) { - throw new InvalidInputError("No supported survey questions selected for connector mapping"); + throw new InvalidInputError("No supported survey questions selected for feedbackSource mapping"); } return { type: "formbricks_survey", mappings: flattenedMappings }; @@ -131,7 +134,7 @@ const ZFormbricksSurveyMapping = z.object({ }); const sanitizeAndValidateCsvFieldMappings = ( - fieldMappings: z.infer[] + fieldMappings: z.infer[] ) => { const sanitized = sanitizeCsvFieldMappings(fieldMappings) ?? []; const missing = getMissingRequiredCsvFieldMappings(sanitized); @@ -143,36 +146,36 @@ const sanitizeAndValidateCsvFieldMappings = ( return sanitized; }; -const ZCreateConnectorWithMappingsAction = z +const ZCreateFeedbackSourceWithMappingsAction = z .object({ workspaceId: ZId, - connectorInput: ZConnectorCreateInput, + feedbackSourceInput: ZFeedbackSourceCreateInput, formbricksMappings: z.array(ZFormbricksSurveyMapping).optional(), - fieldMappings: z.array(ZConnectorFieldMappingCreateInput).optional(), + fieldMappings: z.array(ZFeedbackSourceFieldMappingCreateInput).optional(), }) .superRefine((data, ctx) => { - if (data.connectorInput.type === "formbricks_survey") { + if (data.feedbackSourceInput.type === "formbricks_survey") { if (!data.formbricksMappings?.length) { ctx.addIssue({ code: "custom", path: ["formbricksMappings"], - message: "At least one survey mapping is required for Formbricks connectors", + message: "At least one survey mapping is required for Formbricks feedbackSources", }); } - } else if (data.connectorInput.type === "csv") { + } else if (data.feedbackSourceInput.type === "csv") { if (!data.fieldMappings?.length) { ctx.addIssue({ code: "custom", path: ["fieldMappings"], - message: "At least one field mapping is required for CSV connectors", + message: "At least one field mapping is required for CSV feedbackSources", }); } } }); -export const createConnectorWithMappingsAction = authenticatedActionClient - .inputSchema(ZCreateConnectorWithMappingsAction) - .action(async ({ ctx, parsedInput }): Promise => { +export const createFeedbackSourceWithMappingsAction = authenticatedActionClient + .inputSchema(ZCreateFeedbackSourceWithMappingsAction) + .action(async ({ ctx, parsedInput }): Promise => { const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId); await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -192,7 +195,7 @@ export const createConnectorWithMappingsAction = authenticatedActionClient // Verify FRD belongs to same org const frd = await prisma.feedbackDirectory.findUnique({ - where: { id: parsedInput.connectorInput.feedbackDirectoryId }, + where: { id: parsedInput.feedbackSourceInput.feedbackDirectoryId }, select: { organizationId: true }, }); if (frd?.organizationId !== organizationId) { @@ -218,38 +221,38 @@ export const createConnectorWithMappingsAction = authenticatedActionClient mappingsInput = { type: "field", mappings: - parsedInput.connectorInput.type === "csv" + parsedInput.feedbackSourceInput.type === "csv" ? sanitizeAndValidateCsvFieldMappings(fieldMappings) : fieldMappings, }; } - return createConnectorWithMappings( + return createFeedbackSourceWithMappings( parsedInput.workspaceId, - { ...parsedInput.connectorInput, createdBy: ctx.user.id }, + { ...parsedInput.feedbackSourceInput, createdBy: ctx.user.id }, mappingsInput ); }); -const ZUpdateConnectorWithMappingsAction = z.object({ - connectorId: ZId, +const ZUpdateFeedbackSourceWithMappingsAction = z.object({ + feedbackSourceId: ZId, workspaceId: ZId, - connectorInput: ZConnectorUpdateInput, + feedbackSourceInput: ZFeedbackSourceUpdateInput, formbricksMappings: z.array(ZFormbricksSurveyMapping).min(1).optional(), - fieldMappings: z.array(ZConnectorFieldMappingCreateInput).optional(), + fieldMappings: z.array(ZFeedbackSourceFieldMappingCreateInput).optional(), }); -export const updateConnectorWithMappingsAction = authenticatedActionClient - .inputSchema(ZUpdateConnectorWithMappingsAction) +export const updateFeedbackSourceWithMappingsAction = authenticatedActionClient + .inputSchema(ZUpdateFeedbackSourceWithMappingsAction) .action( async ({ ctx, parsedInput, }: { ctx: AuthenticatedActionClientCtx; - parsedInput: z.infer; - }): Promise => { - const organizationId = await getOrganizationIdFromConnectorId(parsedInput.connectorId); + parsedInput: z.infer; + }): Promise => { + const organizationId = await getOrganizationIdFromFeedbackSourceId(parsedInput.feedbackSourceId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, @@ -280,48 +283,48 @@ export const updateConnectorWithMappingsAction = authenticatedActionClient mappingsInput = await resolveFormbricksMappingsInput(parsedInput.formbricksMappings); } else if (parsedInput.fieldMappings && parsedInput.fieldMappings.length > 0) { - const connector = await prisma.connector.findUnique({ - where: { id: parsedInput.connectorId, workspaceId: parsedInput.workspaceId }, + const feedbackSource = await prisma.feedbackSource.findUnique({ + where: { id: parsedInput.feedbackSourceId, workspaceId: parsedInput.workspaceId }, select: { type: true }, }); - if (!connector) { - throw new ResourceNotFoundError("Connector", parsedInput.connectorId); + if (!feedbackSource) { + throw new ResourceNotFoundError("FeedbackSource", parsedInput.feedbackSourceId); } mappingsInput = { type: "field", mappings: - connector.type === "csv" + feedbackSource.type === "csv" ? sanitizeAndValidateCsvFieldMappings(parsedInput.fieldMappings) : parsedInput.fieldMappings, }; } - return updateConnectorWithMappings( - parsedInput.connectorId, + return updateFeedbackSourceWithMappings( + parsedInput.feedbackSourceId, parsedInput.workspaceId, - parsedInput.connectorInput, + parsedInput.feedbackSourceInput, mappingsInput ); } ); -const ZDuplicateConnectorAction = z.object({ - connectorId: ZId, +const ZDuplicateFeedbackSourceAction = z.object({ + feedbackSourceId: ZId, workspaceId: ZId, }); -export const duplicateConnectorAction = authenticatedActionClient - .inputSchema(ZDuplicateConnectorAction) +export const duplicateFeedbackSourceAction = authenticatedActionClient + .inputSchema(ZDuplicateFeedbackSourceAction) .action( async ({ ctx, parsedInput, }: { ctx: AuthenticatedActionClientCtx; - parsedInput: z.infer; - }): Promise => { - const organizationId = await getOrganizationIdFromConnectorId(parsedInput.connectorId); + parsedInput: z.infer; + }): Promise => { + const organizationId = await getOrganizationIdFromFeedbackSourceId(parsedInput.feedbackSourceId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, @@ -338,9 +341,12 @@ export const duplicateConnectorAction = authenticatedActionClient ], }); - const source = await getConnectorWithMappingsById(parsedInput.connectorId, parsedInput.workspaceId); + const source = await getFeedbackSourceWithMappingsById( + parsedInput.feedbackSourceId, + parsedInput.workspaceId + ); if (!source) { - throw new ResourceNotFoundError("Connector", parsedInput.connectorId); + throw new ResourceNotFoundError("FeedbackSource", parsedInput.feedbackSourceId); } let mappingsInput: TMappingsInput | undefined; @@ -367,7 +373,7 @@ export const duplicateConnectorAction = authenticatedActionClient }; } - return createConnectorWithMappings( + return createFeedbackSourceWithMappings( parsedInput.workspaceId, { name: `${source.name} (copy)`, @@ -417,7 +423,7 @@ export const getResponseCountAction = authenticatedActionClient ); const ZImportHistoricalResponsesAction = z.object({ - connectorId: ZId, + feedbackSourceId: ZId, workspaceId: ZId, surveyId: ZId, }); @@ -432,7 +438,7 @@ export const importHistoricalResponsesAction = authenticatedActionClient ctx: AuthenticatedActionClientCtx; parsedInput: z.infer; }) => { - const organizationId = await getOrganizationIdFromConnectorId(parsedInput.connectorId); + const organizationId = await getOrganizationIdFromFeedbackSourceId(parsedInput.feedbackSourceId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, @@ -449,9 +455,12 @@ export const importHistoricalResponsesAction = authenticatedActionClient ], }); - const connector = await getConnectorWithMappingsById(parsedInput.connectorId, parsedInput.workspaceId); - if (!connector) { - throw new ResourceNotFoundError("Connector", parsedInput.connectorId); + const feedbackSource = await getFeedbackSourceWithMappingsById( + parsedInput.feedbackSourceId, + parsedInput.workspaceId + ); + if (!feedbackSource) { + throw new ResourceNotFoundError("FeedbackSource", parsedInput.feedbackSourceId); } const survey = await getSurvey(parsedInput.surveyId); @@ -459,12 +468,12 @@ export const importHistoricalResponsesAction = authenticatedActionClient throw new ResourceNotFoundError("Survey", parsedInput.surveyId); } - return importHistoricalResponses(connector, survey); + return importHistoricalResponses(feedbackSource, survey); } ); const ZImportCsvDataAction = z.object({ - connectorId: ZId, + feedbackSourceId: ZId, workspaceId: ZId, csvData: z.array(z.record(z.string(), z.string())).min(1), }); @@ -479,7 +488,7 @@ export const importCsvDataAction = authenticatedActionClient ctx: AuthenticatedActionClientCtx; parsedInput: z.infer; }) => { - const organizationId = await getOrganizationIdFromConnectorId(parsedInput.connectorId); + const organizationId = await getOrganizationIdFromFeedbackSourceId(parsedInput.feedbackSourceId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, @@ -496,15 +505,18 @@ export const importCsvDataAction = authenticatedActionClient ], }); - const connector = await getConnectorWithMappingsById(parsedInput.connectorId, parsedInput.workspaceId); - if (!connector) { - throw new ResourceNotFoundError("Connector", parsedInput.connectorId); + const feedbackSource = await getFeedbackSourceWithMappingsById( + parsedInput.feedbackSourceId, + parsedInput.workspaceId + ); + if (!feedbackSource) { + throw new ResourceNotFoundError("FeedbackSource", parsedInput.feedbackSourceId); } - const result = await importCsvData(connector, parsedInput.csvData); + const result = await importCsvData(feedbackSource, parsedInput.csvData); if (result.successes > 0) { - await updateConnector(parsedInput.connectorId, parsedInput.workspaceId, { + await updateFeedbackSource(parsedInput.feedbackSourceId, parsedInput.workspaceId, { lastSyncAt: new Date(), }); } diff --git a/apps/web/lib/connector/csv-import.test.ts b/apps/web/lib/feedback-source/csv-import.test.ts similarity index 83% rename from apps/web/lib/connector/csv-import.test.ts rename to apps/web/lib/feedback-source/csv-import.test.ts index f3754f134e25..502603a58c37 100644 --- a/apps/web/lib/connector/csv-import.test.ts +++ b/apps/web/lib/feedback-source/csv-import.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; -import type { TConnectorWithMappings } from "@formbricks/types/connector"; import { InvalidInputError } from "@formbricks/types/errors"; +import type { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { CSV_IMPORT_MISSING_COLUMNS_ERROR_CODE } from "@/modules/ee/unify-feedback/sources/types"; import { importCsvData } from "./csv-import"; @@ -24,7 +24,9 @@ const matchingCsvRow = { feedback: "Great", }; -const makeConnector = (overrides?: Partial): TConnectorWithMappings => ({ +const makeFeedbackSource = ( + overrides?: Partial +): TFeedbackSourceWithMappings => ({ id: "conn-1", createdAt: NOW, updatedAt: NOW, @@ -41,7 +43,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-1", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "response_id", targetFieldId: "submission_id", @@ -50,7 +52,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-2", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "question_id", targetFieldId: "field_id", @@ -59,7 +61,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-3", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "question", targetFieldId: "field_label", @@ -68,7 +70,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-4", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "", targetFieldId: "field_type", @@ -77,7 +79,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-5", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "feedback", targetFieldId: "response_value", @@ -86,7 +88,7 @@ const makeConnector = (overrides?: Partial): TConnectorW { id: "fm-6", createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId: "", targetFieldId: "source_type", @@ -101,37 +103,37 @@ describe("importCsvData", () => { vi.clearAllMocks(); }); - test("throws InvalidInputError for non-csv connector", async () => { - const connector = makeConnector({ type: "formbricks_survey" }); - await expect(importCsvData(connector, [])).rejects.toThrow(InvalidInputError); + test("throws InvalidInputError for non-csv feedbackSource", async () => { + const feedbackSource = makeFeedbackSource({ type: "formbricks_survey" }); + await expect(importCsvData(feedbackSource, [])).rejects.toThrow(InvalidInputError); }); test("throws InvalidInputError when no field mappings configured", async () => { - const connector = makeConnector({ fieldMappings: [] }); - await expect(importCsvData(connector, [{ feedback: "test" }])).rejects.toThrow(InvalidInputError); + const feedbackSource = makeFeedbackSource({ fieldMappings: [] }); + await expect(importCsvData(feedbackSource, [{ feedback: "test" }])).rejects.toThrow(InvalidInputError); }); test("throws InvalidInputError when submission_id is not mapped", async () => { - const connector = makeConnector({ - fieldMappings: makeConnector().fieldMappings.filter( + const feedbackSource = makeFeedbackSource({ + fieldMappings: makeFeedbackSource().fieldMappings.filter( (mapping) => mapping.targetFieldId !== "submission_id" ), }); - await expect(importCsvData(connector, [matchingCsvRow])).rejects.toThrow( + await expect(importCsvData(feedbackSource, [matchingCsvRow])).rejects.toThrow( "This saved CSV mapping is incomplete" ); expect(transformCsvRowsToFeedbackRecords).not.toHaveBeenCalled(); }); test("throws InvalidInputError when uploaded CSV is missing a mapped source column", async () => { - const connector = makeConnector({ - fieldMappings: makeConnector().fieldMappings.map((mapping) => + const feedbackSource = makeFeedbackSource({ + fieldMappings: makeFeedbackSource().fieldMappings.map((mapping) => mapping.targetFieldId === "submission_id" ? { ...mapping, sourceFieldId: "source_id" } : mapping ), }); - await expect(importCsvData(connector, [matchingCsvRow])).rejects.toThrow( + await expect(importCsvData(feedbackSource, [matchingCsvRow])).rejects.toThrow( CSV_IMPORT_MISSING_COLUMNS_ERROR_CODE ); expect(transformCsvRowsToFeedbackRecords).not.toHaveBeenCalled(); @@ -140,7 +142,7 @@ describe("importCsvData", () => { test("returns zeros when all rows are skipped", async () => { transformCsvRowsToFeedbackRecords.mockReturnValue({ records: [], skipped: 3 }); - const result = await importCsvData(makeConnector(), [ + const result = await importCsvData(makeFeedbackSource(), [ matchingCsvRow, { ...matchingCsvRow, response_id: "resp-2" }, { ...matchingCsvRow, response_id: "resp-3" }, @@ -180,7 +182,7 @@ describe("importCsvData", () => { ], } as never); - const result = await importCsvData(makeConnector(), [ + const result = await importCsvData(makeFeedbackSource(), [ matchingCsvRow, { ...matchingCsvRow, response_id: "resp-2" }, { ...matchingCsvRow, response_id: "resp-3" }, @@ -219,7 +221,7 @@ describe("importCsvData", () => { ], } as never); - const result = await importCsvData(makeConnector(), [ + const result = await importCsvData(makeFeedbackSource(), [ matchingCsvRow, { ...matchingCsvRow, response_id: "resp-2" }, ]); @@ -266,7 +268,7 @@ describe("importCsvData", () => { ], } as never); - const result = await importCsvData(makeConnector(), [ + const result = await importCsvData(makeFeedbackSource(), [ matchingCsvRow, { ...matchingCsvRow, response_id: "resp-2" }, { ...matchingCsvRow, response_id: "resp-3" }, @@ -291,7 +293,7 @@ describe("importCsvData", () => { } as never); await importCsvData( - makeConnector(), + makeFeedbackSource(), Array.from({ length: 120 }, (_, i) => ({ ...matchingCsvRow, response_id: `resp-${i}` })) ); diff --git a/apps/web/lib/connector/csv-import.ts b/apps/web/lib/feedback-source/csv-import.ts similarity index 77% rename from apps/web/lib/connector/csv-import.ts rename to apps/web/lib/feedback-source/csv-import.ts index 8161f44a02c5..d6eed2bad890 100644 --- a/apps/web/lib/connector/csv-import.ts +++ b/apps/web/lib/feedback-source/csv-import.ts @@ -1,6 +1,6 @@ import "server-only"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; import { InvalidInputError } from "@formbricks/types/errors"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { CSV_IMPORT_MISSING_COLUMNS_ERROR_CODE } from "@/modules/ee/unify-feedback/sources/types"; import { createFeedbackRecordsBatch } from "@/modules/hub"; import { transformCsvRowsToFeedbackRecords } from "./csv-transform"; @@ -14,34 +14,34 @@ import { const CSV_BATCH_SIZE = 50; export const importCsvData = async ( - connector: TConnectorWithMappings, + feedbackSource: TFeedbackSourceWithMappings, csvRows: Record[] ): Promise => { - if (connector.type !== "csv") { - throw new InvalidInputError("CSV import is only supported for CSV connectors"); + if (feedbackSource.type !== "csv") { + throw new InvalidInputError("CSV import is only supported for CSV feedbackSources"); } - if (connector.fieldMappings.length === 0) { - throw new InvalidInputError("Connector has no field mappings configured"); + if (feedbackSource.fieldMappings.length === 0) { + throw new InvalidInputError("FeedbackSource has no field mappings configured"); } const missingMappedColumns = getMissingCsvMappedSourceColumns( - connector.fieldMappings, + feedbackSource.fieldMappings, Object.keys(csvRows[0] ?? {}) ); if (missingMappedColumns.length > 0) { throw new InvalidInputError(CSV_IMPORT_MISSING_COLUMNS_ERROR_CODE); } - const missing = getMissingRequiredCsvFieldMappings(connector.fieldMappings); + const missing = getMissingRequiredCsvFieldMappings(feedbackSource.fieldMappings); if (missing.length > 0) { throw new InvalidInputError(formatMissingRequiredCsvFieldMappingsMessage()); } const { records, skipped: transformSkipped } = transformCsvRowsToFeedbackRecords( csvRows, - connector.fieldMappings, - connector.feedbackDirectoryId + feedbackSource.fieldMappings, + feedbackSource.feedbackDirectoryId ); let successes = 0; diff --git a/apps/web/lib/connector/csv-transform.test.ts b/apps/web/lib/feedback-source/csv-transform.test.ts similarity index 94% rename from apps/web/lib/connector/csv-transform.test.ts rename to apps/web/lib/feedback-source/csv-transform.test.ts index 03200d95a0cb..dd6e4e80c43e 100644 --- a/apps/web/lib/connector/csv-transform.test.ts +++ b/apps/web/lib/feedback-source/csv-transform.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import { TConnectorFieldMapping } from "@formbricks/types/connector"; +import { TFeedbackSourceFieldMapping } from "@formbricks/types/feedback-source"; import { transformCsvRowToFeedbackRecord, transformCsvRowsToFeedbackRecords } from "./csv-transform"; const NOW = new Date("2026-02-25T10:00:00.000Z"); @@ -9,17 +9,17 @@ const makeMapping = ( sourceFieldId: string, targetFieldId: string, staticValue?: string -): TConnectorFieldMapping => ({ +): TFeedbackSourceFieldMapping => ({ id: `mapping-${targetFieldId}`, createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", sourceFieldId, - targetFieldId: targetFieldId as TConnectorFieldMapping["targetFieldId"], + targetFieldId: targetFieldId as TFeedbackSourceFieldMapping["targetFieldId"], staticValue: staticValue ?? null, }); -const baseMappings: TConnectorFieldMapping[] = [ +const baseMappings: TFeedbackSourceFieldMapping[] = [ makeMapping("feedback_text", "value_text"), makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), @@ -198,7 +198,7 @@ describe("transformCsvRowToFeedbackRecord", () => { vi.useFakeTimers(); vi.setSystemTime(NOW); - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), makeMapping("", "source_type", "csv"), @@ -217,7 +217,7 @@ describe("transformCsvRowToFeedbackRecord", () => { }); test("ignores source_type mappings and uses csv", () => { - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), makeMapping("type_column", "source_type", "always_survey"), @@ -315,7 +315,7 @@ describe("transformCsvRowsToFeedbackRecords", () => { { feedback_text: "No question field" }, ]; - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("feedback_text", "value_text"), makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), @@ -342,7 +342,7 @@ describe("transformCsvRowsToFeedbackRecords", () => { }); describe("response_value routing", () => { - const responseMappings = (fieldType: string): TConnectorFieldMapping[] => [ + const responseMappings = (fieldType: string): TFeedbackSourceFieldMapping[] => [ makeMapping("answer", "response_value"), makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), @@ -408,7 +408,7 @@ describe("response_value routing", () => { }); test("invalid field_type causes the row to be skipped", () => { - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("answer", "response_value"), makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), @@ -425,7 +425,7 @@ describe("response_value routing", () => { }); test("missing field_type causes the row to be skipped", () => { - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("answer", "response_value"), makeMapping("question", "field_id"), makeMapping("response_id", "submission_id"), @@ -442,8 +442,8 @@ describe("response_value routing", () => { }); describe("tenant_id defense-in-depth", () => { - test("ignores a user-supplied tenant_id mapping and uses the connector value", () => { - const mappings: TConnectorFieldMapping[] = [ + test("ignores a user-supplied tenant_id mapping and uses the feedbackSource value", () => { + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("malicious", "tenant_id"), makeMapping("feedback_text", "value_text"), makeMapping("question", "field_id"), @@ -467,7 +467,7 @@ describe("tenant_id defense-in-depth", () => { }); test("ignores a static tenant_id mapping", () => { - const mappings: TConnectorFieldMapping[] = [ + const mappings: TFeedbackSourceFieldMapping[] = [ makeMapping("", "tenant_id", "stolen-tenant"), makeMapping("feedback_text", "value_text"), makeMapping("question", "field_id"), diff --git a/apps/web/lib/connector/csv-transform.ts b/apps/web/lib/feedback-source/csv-transform.ts similarity index 95% rename from apps/web/lib/connector/csv-transform.ts rename to apps/web/lib/feedback-source/csv-transform.ts index b2b1451adfec..fa5d006e6e61 100644 --- a/apps/web/lib/connector/csv-transform.ts +++ b/apps/web/lib/feedback-source/csv-transform.ts @@ -1,9 +1,9 @@ import { - TConnectorFieldMapping, + TFeedbackSourceFieldMapping, THubFieldType, THubTargetField, ZHubFieldType, -} from "@formbricks/types/connector"; +} from "@formbricks/types/feedback-source"; import { FeedbackRecordCreateParams } from "@/modules/hub"; import { routeResponseValueTarget } from "./utils"; @@ -50,7 +50,7 @@ const coerceValue = (value: string, targetField: THubTargetField): string | numb const resolveValue = ( row: Record, - mapping: TConnectorFieldMapping, + mapping: TFeedbackSourceFieldMapping, effectiveTargetFieldId: THubTargetField ): string | number | boolean | undefined => { if (mapping.staticValue) { @@ -68,7 +68,7 @@ const resolveValue = ( const resolveFieldTypeForRow = ( row: Record, - mappings: TConnectorFieldMapping[] + mappings: TFeedbackSourceFieldMapping[] ): THubFieldType | null => { const mapping = mappings.find((m) => m.targetFieldId === "field_type"); if (!mapping) return null; @@ -87,7 +87,7 @@ const resolveFieldTypeForRow = ( */ export const transformCsvRowToFeedbackRecord = ( row: Record, - mappings: TConnectorFieldMapping[], + mappings: TFeedbackSourceFieldMapping[], tenantId?: string ): FeedbackRecordCreateParams | null => { const record: Record | undefined> = {}; @@ -151,7 +151,7 @@ export const transformCsvRowToFeedbackRecord = ( */ export const transformCsvRowsToFeedbackRecords = ( rows: Record[], - mappings: TConnectorFieldMapping[], + mappings: TFeedbackSourceFieldMapping[], tenantId?: string ): { records: FeedbackRecordCreateParams[]; skipped: number } => { const records: FeedbackRecordCreateParams[] = []; diff --git a/apps/web/lib/connector/import.test.ts b/apps/web/lib/feedback-source/import.test.ts similarity index 82% rename from apps/web/lib/connector/import.test.ts rename to apps/web/lib/feedback-source/import.test.ts index cd14796e163f..5ebae0b903d1 100644 --- a/apps/web/lib/connector/import.test.ts +++ b/apps/web/lib/feedback-source/import.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; import { InvalidInputError } from "@formbricks/types/errors"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { TSurvey } from "@formbricks/types/surveys/types"; import { importHistoricalResponses } from "./import"; @@ -21,15 +21,15 @@ const { createFeedbackRecordsBatch } = vi.mocked(await import("@/modules/hub")); const { transformResponseToFeedbackRecords } = vi.mocked(await import("./transform")); const ENV_ID = "clxxxxxxxxxxxxxxxx001"; -const CONNECTOR_ID = "clxxxxxxxxxxxxxxxx002"; +const FEEDBACK_SOURCE_ID = "clxxxxxxxxxxxxxxxx002"; const SURVEY_ID = "clxxxxxxxxxxxxxxxx003"; const NOW = new Date("2026-02-24T10:00:00.000Z"); -const mockConnector: TConnectorWithMappings = { - id: CONNECTOR_ID, +const mockFeedbackSource: TFeedbackSourceWithMappings = { + id: FEEDBACK_SOURCE_ID, createdAt: NOW, updatedAt: NOW, - name: "Test Connector", + name: "Test FeedbackSource", type: "formbricks_survey", status: "active", workspaceId: ENV_ID, @@ -40,7 +40,7 @@ const mockConnector: TConnectorWithMappings = { { id: "mapping-1", createdAt: NOW, - connectorId: CONNECTOR_ID, + feedbackSourceId: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID, surveyId: SURVEY_ID, elementId: "el-1", @@ -58,17 +58,17 @@ describe("importHistoricalResponses", () => { vi.clearAllMocks(); }); - test("throws InvalidInputError for non-formbricks connector", async () => { - const csvConnector = { ...mockConnector, type: "csv" as const }; + test("throws InvalidInputError for non-formbricks feedbackSource", async () => { + const csvFeedbackSource = { ...mockFeedbackSource, type: "csv" as const }; - await expect(importHistoricalResponses(csvConnector, mockSurvey)).rejects.toThrow(InvalidInputError); + await expect(importHistoricalResponses(csvFeedbackSource, mockSurvey)).rejects.toThrow(InvalidInputError); expect(getResponses).not.toHaveBeenCalled(); }); test("returns zeros when there are no responses", async () => { getResponses.mockResolvedValue([]); - const result = await importHistoricalResponses(mockConnector, mockSurvey); + const result = await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(result).toEqual({ successes: 0, failures: 0, skipped: 0 }); }); @@ -90,7 +90,7 @@ describe("importHistoricalResponses", () => { ], } as never); - const result = await importHistoricalResponses(mockConnector, mockSurvey); + const result = await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(result.successes).toBe(2); expect(result.failures).toBe(0); @@ -108,7 +108,7 @@ describe("importHistoricalResponses", () => { results: [{ data: null, error: { status: 400, message: "Bad request" } }], } as never); - const result = await importHistoricalResponses(mockConnector, mockSurvey); + const result = await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(result.successes).toBe(0); expect(result.failures).toBe(1); @@ -129,7 +129,7 @@ describe("importHistoricalResponses", () => { ], } as never); - const result = await importHistoricalResponses(mockConnector, mockSurvey); + const result = await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(result.successes).toBe(1); expect(result.failures).toBe(1); @@ -149,7 +149,7 @@ describe("importHistoricalResponses", () => { results: [{ data: { id: "fb" }, error: null }], } as never); - await importHistoricalResponses(mockConnector, mockSurvey); + await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(getResponses).toHaveBeenCalledWith(SURVEY_ID, 50, 0); expect(getResponses).toHaveBeenCalledWith(SURVEY_ID, 50, 50); @@ -162,7 +162,7 @@ describe("importHistoricalResponses", () => { transformResponseToFeedbackRecords.mockReturnValue([]); - const result = await importHistoricalResponses(mockConnector, mockSurvey); + const result = await importHistoricalResponses(mockFeedbackSource, mockSurvey); expect(createFeedbackRecordsBatch).not.toHaveBeenCalled(); expect(result).toEqual({ successes: 0, failures: 0, skipped: 2 }); diff --git a/apps/web/lib/connector/import.ts b/apps/web/lib/feedback-source/import.ts similarity index 83% rename from apps/web/lib/connector/import.ts rename to apps/web/lib/feedback-source/import.ts index c92e2b07a371..8962d8915712 100644 --- a/apps/web/lib/connector/import.ts +++ b/apps/web/lib/feedback-source/import.ts @@ -1,6 +1,9 @@ import "server-only"; -import { TConnectorFormbricksMapping, TConnectorWithMappings } from "@formbricks/types/connector"; import { InvalidInputError } from "@formbricks/types/errors"; +import { + TFeedbackSourceFormbricksMapping, + TFeedbackSourceWithMappings, +} from "@formbricks/types/feedback-source"; import { TSurvey } from "@formbricks/types/surveys/types"; import { createFeedbackRecordsBatch } from "@/modules/hub"; import { getResponses } from "../response/service"; @@ -13,7 +16,7 @@ export type TImportResult = { successes: number; failures: number; skipped: numb const processBatch = async ( responses: Awaited>, survey: TSurvey, - mappings: TConnectorFormbricksMapping[], + mappings: TFeedbackSourceFormbricksMapping[], tenantId: string ): Promise => { let successes = 0; @@ -37,11 +40,11 @@ const processBatch = async ( }; export const importHistoricalResponses = async ( - connector: TConnectorWithMappings, + feedbackSource: TFeedbackSourceWithMappings, survey: TSurvey ): Promise => { - if (connector.type !== "formbricks_survey") { - throw new InvalidInputError("Historical import is only supported for Formbricks connectors"); + if (feedbackSource.type !== "formbricks_survey") { + throw new InvalidInputError("Historical import is only supported for Formbricks feedbackSources"); } let successes = 0; @@ -56,8 +59,8 @@ export const importHistoricalResponses = async ( const batch = await processBatch( responses, survey, - connector.formbricksMappings, - connector.feedbackDirectoryId + feedbackSource.formbricksMappings, + feedbackSource.feedbackDirectoryId ); successes += batch.successes; failures += batch.failures; diff --git a/apps/web/lib/connector/pipeline-handler.test.ts b/apps/web/lib/feedback-source/pipeline-handler.test.ts similarity index 64% rename from apps/web/lib/connector/pipeline-handler.test.ts rename to apps/web/lib/feedback-source/pipeline-handler.test.ts index a14d6aa8b4c7..583cf210c9e1 100644 --- a/apps/web/lib/connector/pipeline-handler.test.ts +++ b/apps/web/lib/feedback-source/pipeline-handler.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -20,17 +20,17 @@ vi.mock("@formbricks/logger", () => ({ })); vi.mock("./service", () => ({ - getConnectorsBySurveyId: vi.fn(), - updateConnector: vi.fn(), + getFeedbackSourcesBySurveyId: vi.fn(), + updateFeedbackSource: vi.fn(), })); vi.mock("./transform", () => ({ transformResponseToFeedbackRecords: vi.fn(), })); -const { getConnectorsBySurveyId, updateConnector } = await import("./service"); +const { getFeedbackSourcesBySurveyId, updateFeedbackSource } = await import("./service"); const { transformResponseToFeedbackRecords } = await import("./transform"); -const { handleConnectorPipeline } = await import("./pipeline-handler"); +const { handleFeedbackSourcePipeline } = await import("./pipeline-handler"); const mockResponse = { id: "resp-1", @@ -45,14 +45,14 @@ const mockSurvey = { blocks: [{ id: "block-1", name: "Block", elements: [{ id: "el-1", headline: { default: "Question?" } }] }], } as unknown as TSurvey; -function createConnector( - overrides: Partial> = {} -): TConnectorWithMappings { +function createFeedbackSource( + overrides: Partial> = {} +): TFeedbackSourceWithMappings { return { id: "conn-1", createdAt: new Date(), updatedAt: new Date(), - name: "Test Connector", + name: "Test FeedbackSource", type: "formbricks_survey", status: "active", workspaceId: "env-1", @@ -62,7 +62,7 @@ function createConnector( { id: "map-1", createdAt: new Date(), - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", surveyId: "survey-1", elementId: "el-1", @@ -72,7 +72,7 @@ function createConnector( ], fieldMappings: [], ...overrides, - } as TConnectorWithMappings; + } as TFeedbackSourceWithMappings; } const oneFeedbackRecord = [ @@ -94,68 +94,68 @@ const noConfigError = { detail: "HUB_API_KEY is not set; Hub integration is disabled.", }; -describe("handleConnectorPipeline", () => { +describe("handleFeedbackSourcePipeline", () => { beforeEach(() => { vi.clearAllMocks(); }); - test("returns early when no connectors for survey", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([]); + test("returns early when no feedbackSources for survey", async () => { + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([]); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); expect(transformResponseToFeedbackRecords).not.toHaveBeenCalled(); expect(mockCreateFeedbackRecordsBatch).not.toHaveBeenCalled(); - expect(updateConnector).not.toHaveBeenCalled(); + expect(updateFeedbackSource).not.toHaveBeenCalled(); }); test("continues when transform returns no feedback records", async () => { - const connector = createConnector(); - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([connector]); + const feedbackSource = createFeedbackSource(); + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([feedbackSource]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue([]); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); expect(transformResponseToFeedbackRecords).toHaveBeenCalledWith( mockResponse, mockSurvey, - connector.formbricksMappings, + feedbackSource.formbricksMappings, "frd-1" ); expect(mockCreateFeedbackRecordsBatch).not.toHaveBeenCalled(); - expect(updateConnector).not.toHaveBeenCalled(); + expect(updateFeedbackSource).not.toHaveBeenCalled(); }); - test("does not update connector when Hub returns no-config (HUB_API_KEY not set)", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + test("does not update feedbackSource when Hub returns no-config (HUB_API_KEY not set)", async () => { + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([createFeedbackSource()]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); mockCreateFeedbackRecordsBatch.mockResolvedValue({ results: oneFeedbackRecord.map(() => ({ data: null, error: noConfigError })), }); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); expect(mockCreateFeedbackRecordsBatch).toHaveBeenCalledWith(oneFeedbackRecord); - expect(updateConnector).not.toHaveBeenCalled(); + expect(updateFeedbackSource).not.toHaveBeenCalled(); }); test("sends records to Hub and updates lastSyncAt on full success", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([createFeedbackSource()]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); mockCreateFeedbackRecordsBatch.mockResolvedValue({ results: [{ data: { id: "hub-1", ...oneFeedbackRecord[0] }, error: null }], }); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); expect(mockCreateFeedbackRecordsBatch).toHaveBeenCalledWith(oneFeedbackRecord); - expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", { + expect(updateFeedbackSource).toHaveBeenCalledWith("conn-1", "env-1", { lastSyncAt: expect.any(Date), }); }); - test("does not update connector when all Hub creates fail", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + test("does not update feedbackSource when all Hub creates fail", async () => { + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([createFeedbackSource()]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); mockCreateFeedbackRecordsBatch.mockResolvedValue({ results: [ @@ -163,23 +163,23 @@ describe("handleConnectorPipeline", () => { ], }); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); - expect(updateConnector).not.toHaveBeenCalled(); + expect(updateFeedbackSource).not.toHaveBeenCalled(); }); test("updates lastSyncAt on partial failure when some creates succeed", async () => { const twoRecords = [...oneFeedbackRecord, { ...oneFeedbackRecord[0], field_id: "el-2", value_number: 3 }]; const baseMapping = { createdAt: new Date(), - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", surveyId: "survey-1", hubFieldType: "rating" as const, customFieldLabel: null as string | null, }; - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([ - createConnector({ + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([ + createFeedbackSource({ formbricksMappings: [ { ...baseMapping, id: "m1", elementId: "el-1" }, { ...baseMapping, id: "m2", elementId: "el-2" }, @@ -194,21 +194,21 @@ describe("handleConnectorPipeline", () => { ], }); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); - expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", { + expect(updateFeedbackSource).toHaveBeenCalledWith("conn-1", "env-1", { lastSyncAt: expect.any(Date), }); }); - test("does not update connector when transform throws", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + test("does not update feedbackSource when transform throws", async () => { + vi.mocked(getFeedbackSourcesBySurveyId).mockResolvedValue([createFeedbackSource()]); vi.mocked(transformResponseToFeedbackRecords).mockImplementation(() => { throw new Error("Transform failed"); }); - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + await handleFeedbackSourcePipeline(mockResponse, mockSurvey, "env-1"); - expect(updateConnector).not.toHaveBeenCalled(); + expect(updateFeedbackSource).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/lib/connector/pipeline-handler.ts b/apps/web/lib/feedback-source/pipeline-handler.ts similarity index 63% rename from apps/web/lib/connector/pipeline-handler.ts rename to apps/web/lib/feedback-source/pipeline-handler.ts index 29bfdd39cbcd..f35ab11036ea 100644 --- a/apps/web/lib/connector/pipeline-handler.ts +++ b/apps/web/lib/feedback-source/pipeline-handler.ts @@ -1,24 +1,24 @@ import "server-only"; import { logger } from "@formbricks/logger"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; import { createFeedbackRecordsBatch } from "@/modules/hub"; -import { getConnectorsBySurveyId, updateConnector } from "./service"; +import { getFeedbackSourcesBySurveyId, updateFeedbackSource } from "./service"; import { transformResponseToFeedbackRecords } from "./transform"; const getErrorMessage = (error: unknown): string => error instanceof Error ? error.message : "Unknown error"; const logFailedRecords = ( - connectorId: string, + feedbackSourceId: string, results: Awaited>["results"] ): void => { for (const [index, result] of results.entries()) { if (!result.error) continue; logger.error( { - connectorId, + feedbackSourceId, feedbackRecordIndex: index, error: { status: result.error.status, @@ -31,8 +31,8 @@ const logFailedRecords = ( } }; -const processConnector = async ( - connector: TConnectorWithMappings, +const processFeedbackSource = async ( + feedbackSource: TFeedbackSourceWithMappings, response: TResponse, survey: Pick, workspaceId: string @@ -40,8 +40,8 @@ const processConnector = async ( const feedbackRecords = transformResponseToFeedbackRecords( response, survey, - connector.formbricksMappings, - connector.feedbackDirectoryId + feedbackSource.formbricksMappings, + feedbackSource.feedbackDirectoryId ); if (feedbackRecords.length === 0) { @@ -56,66 +56,66 @@ const processConnector = async ( if (failures > 0) { logger.warn( { - connectorId: connector.id, + feedbackSourceId: feedbackSource.id, surveyId: survey.id, responseId: response.id, successes, failures, }, - `Connector pipeline: ${failures}/${feedbackRecords.length} FeedbackRecords failed to send` + `FeedbackSource pipeline: ${failures}/${feedbackRecords.length} FeedbackRecords failed to send` ); - logFailedRecords(connector.id, results); + logFailedRecords(feedbackSource.id, results); } else { logger.info( { - connectorId: connector.id, + feedbackSourceId: feedbackSource.id, surveyId: survey.id, responseId: response.id, feedbackRecordsCreated: successes, }, - `Connector pipeline: Successfully sent ${successes} FeedbackRecords` + `FeedbackSource pipeline: Successfully sent ${successes} FeedbackRecords` ); } if (successes > 0) { - await updateConnector(connector.id, workspaceId, { lastSyncAt: new Date() }); + await updateFeedbackSource(feedbackSource.id, workspaceId, { lastSyncAt: new Date() }); } }; /** - * Handle connector pipeline for a survey response + * Handle feedbackSource pipeline for a survey response * * This function is called from the pipeline when a response is created/finished. - * It looks up active connectors for the survey and sends the response data. + * It looks up active feedbackSources for the survey and sends the response data. * * @param response - The survey response * @param survey - The survey * @param workspaceId - The workspace ID (used as tenant_id) */ -export const handleConnectorPipeline = async ( +export const handleFeedbackSourcePipeline = async ( response: TResponse, survey: Pick, workspaceId: string ): Promise => { try { - const connectors = await getConnectorsBySurveyId(survey.id); + const feedbackSources = await getFeedbackSourcesBySurveyId(survey.id); - if (connectors.length === 0) { + if (feedbackSources.length === 0) { return; } - for (const connector of connectors) { + for (const feedbackSource of feedbackSources) { try { - await processConnector(connector, response, survey, workspaceId); + await processFeedbackSource(feedbackSource, response, survey, workspaceId); } catch (error) { logger.error( { - connectorId: connector.id, + feedbackSourceId: feedbackSource.id, surveyId: survey.id, responseId: response.id, error: getErrorMessage(error), }, - "Connector pipeline: Failed to process connector" + "FeedbackSource pipeline: Failed to process feedbackSource" ); } } @@ -126,7 +126,7 @@ export const handleConnectorPipeline = async ( responseId: response.id, error: getErrorMessage(error), }, - "Connector pipeline: Failed to handle connectors" + "FeedbackSource pipeline: Failed to handle feedbackSources" ); } }; diff --git a/apps/web/lib/feedback-source/service.test.ts b/apps/web/lib/feedback-source/service.test.ts new file mode 100644 index 000000000000..85e0a884f07f --- /dev/null +++ b/apps/web/lib/feedback-source/service.test.ts @@ -0,0 +1,630 @@ +import { Prisma } from "@prisma/client"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { + createFeedbackSourceWithMappings, + deleteFeedbackSource, + getFeedbackSourcesBySurveyId, + getFeedbackSourcesWithMappings, + updateFeedbackSource, + updateFeedbackSourceWithMappings, +} from "./service"; + +vi.mock("@formbricks/database", () => ({ + prisma: { + feedbackSource: { + findMany: vi.fn(), + findUniqueOrThrow: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + feedbackSourceFormbricksMapping: { + create: vi.fn(), + deleteMany: vi.fn(), + }, + feedbackSourceFieldMapping: { + create: vi.fn(), + deleteMany: vi.fn(), + }, + $transaction: vi.fn(), + }, +})); + +vi.mock("@/lib/utils/validate", () => ({ + validateInputs: vi.fn(), +})); + +const ENV_ID = "clxxxxxxxxxxxxxxxx001"; +const FEEDBACK_SOURCE_ID = "clxxxxxxxxxxxxxxxx002"; +const SURVEY_ID = "clxxxxxxxxxxxxxxxx003"; +const FRD_ID = "clxxxxxxxxxxxxxxxx004"; +const NOW = new Date("2026-02-24T10:00:00.000Z"); + +const mockFeedbackSource = { + id: FEEDBACK_SOURCE_ID, + createdAt: NOW, + updatedAt: NOW, + name: "Test FeedbackSource", + type: "formbricks_survey" as const, + status: "active" as const, + workspaceId: ENV_ID, + lastSyncAt: null, + createdBy: null, +}; + +const mockFeedbackSourceWithMappingsFromDb = { + ...mockFeedbackSource, + creator: null, + formbricksMappings: [ + { + id: "mapping-1", + createdAt: NOW, + feedbackSourceId: FEEDBACK_SOURCE_ID, + workspaceId: ENV_ID, + surveyId: SURVEY_ID, + elementId: "el-1", + hubFieldType: "text", + customFieldLabel: null, + }, + ], + fieldMappings: [], +}; + +const mockFeedbackSourceWithMappings = { + ...mockFeedbackSource, + creatorName: null, + formbricksMappings: mockFeedbackSourceWithMappingsFromDb.formbricksMappings, + fieldMappings: [], +}; + +describe("getFeedbackSourcesWithMappings", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("returns feedbackSources for the given environment", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockResolvedValue([ + mockFeedbackSourceWithMappingsFromDb, + ] as never); + + const result = await getFeedbackSourcesWithMappings(ENV_ID); + + expect(prisma.feedbackSource.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { workspaceId: ENV_ID }, + orderBy: { createdAt: "desc" }, + }) + ); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(FEEDBACK_SOURCE_ID); + }); + + test("applies pagination when page is provided", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockResolvedValue([] as never); + + await getFeedbackSourcesWithMappings(ENV_ID, 2); + + expect(prisma.feedbackSource.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + take: expect.any(Number), + skip: expect.any(Number), + }) + ); + }); + + test("returns empty array when no feedbackSources exist", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockResolvedValue([] as never); + + const result = await getFeedbackSourcesWithMappings(ENV_ID); + expect(result).toEqual([]); + }); + + test("throws DatabaseError on Prisma error", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("connection error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect(getFeedbackSourcesWithMappings(ENV_ID)).rejects.toThrow(DatabaseError); + }); +}); + +describe("getFeedbackSourcesBySurveyId", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("returns active formbricks feedbackSources linked to the survey", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockResolvedValue([ + mockFeedbackSourceWithMappingsFromDb, + ] as never); + + const result = await getFeedbackSourcesBySurveyId(SURVEY_ID); + + expect(prisma.feedbackSource.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + type: "formbricks_survey", + status: "active", + formbricksMappings: { some: { surveyId: SURVEY_ID } }, + }, + }) + ); + expect(result).toHaveLength(1); + }); + + test("returns empty when no feedbackSources match", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockResolvedValue([] as never); + + const result = await getFeedbackSourcesBySurveyId(SURVEY_ID); + expect(result).toEqual([]); + }); + + test("throws DatabaseError on Prisma error", async () => { + vi.mocked(prisma.feedbackSource.findMany).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("DB error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect(getFeedbackSourcesBySurveyId(SURVEY_ID)).rejects.toThrow(DatabaseError); + }); +}); + +describe("updateFeedbackSource", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("updates feedbackSource name and returns the result", async () => { + const updated = { ...mockFeedbackSource, name: "Renamed" }; + vi.mocked(prisma.feedbackSource.update).mockResolvedValue(updated as never); + + const result = await updateFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID, { name: "Renamed" }); + + expect(prisma.feedbackSource.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }, + data: expect.objectContaining({ name: "Renamed" }), + }) + ); + expect(result.name).toBe("Renamed"); + }); + + test("updates feedbackSource status", async () => { + const updated = { ...mockFeedbackSource, status: "paused" }; + vi.mocked(prisma.feedbackSource.update).mockResolvedValue(updated as never); + + const result = await updateFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID, { status: "paused" }); + expect(result.status).toBe("paused"); + }); + + test("throws ResourceNotFoundError when feedbackSource does not exist", async () => { + vi.mocked(prisma.feedbackSource.update).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Not found", { + code: "P2015", + clientVersion: "5.0.0", + }) + ); + + await expect(updateFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + ResourceNotFoundError + ); + }); + + test("throws DatabaseError on generic Prisma error", async () => { + vi.mocked(prisma.feedbackSource.update).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("DB error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect(updateFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + DatabaseError + ); + }); + + test("rethrows non-Prisma errors", async () => { + vi.mocked(prisma.feedbackSource.update).mockRejectedValue(new Error("unexpected")); + + await expect(updateFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + "unexpected" + ); + }); +}); + +describe("deleteFeedbackSource", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("deletes the feedbackSource and returns it", async () => { + vi.mocked(prisma.feedbackSource.delete).mockResolvedValue(mockFeedbackSource as never); + + const result = await deleteFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID); + + expect(prisma.feedbackSource.delete).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }, + }) + ); + expect(result.id).toBe(FEEDBACK_SOURCE_ID); + }); + + test("throws ResourceNotFoundError when feedbackSource does not exist", async () => { + vi.mocked(prisma.feedbackSource.delete).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Not found", { + code: "P2015", + clientVersion: "5.0.0", + }) + ); + + await expect(deleteFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID)).rejects.toThrow(ResourceNotFoundError); + }); + + test("throws DatabaseError on generic Prisma error", async () => { + vi.mocked(prisma.feedbackSource.delete).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("DB error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect(deleteFeedbackSource(FEEDBACK_SOURCE_ID, ENV_ID)).rejects.toThrow(DatabaseError); + }); +}); + +describe("createFeedbackSourceWithMappings", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const setupTransaction = () => { + const txMethods = { + feedbackSource: { + create: vi.fn(), + findUniqueOrThrow: vi.fn(), + }, + feedbackSourceFormbricksMapping: { + create: vi.fn(), + }, + feedbackSourceFieldMapping: { + create: vi.fn(), + }, + }; + + vi.mocked(prisma.$transaction).mockImplementation(async (fn) => { + return (fn as unknown as (tx: typeof txMethods) => Promise)(txMethods); + }); + + return txMethods; + }; + + test("creates feedbackSource without mappings", async () => { + const tx = setupTransaction(); + tx.feedbackSource.create.mockResolvedValue({ id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue(mockFeedbackSourceWithMappingsFromDb); + + const result = await createFeedbackSourceWithMappings(ENV_ID, { + name: "New", + type: "formbricks_survey", + feedbackDirectoryId: FRD_ID, + }); + + expect(tx.feedbackSource.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: { + name: "New", + type: "formbricks_survey", + workspaceId: ENV_ID, + feedbackDirectoryId: FRD_ID, + }, + }) + ); + expect(tx.feedbackSourceFormbricksMapping.create).not.toHaveBeenCalled(); + expect(tx.feedbackSourceFieldMapping.create).not.toHaveBeenCalled(); + expect(result).toEqual(mockFeedbackSourceWithMappings); + }); + + test("creates feedbackSource with formbricks mappings", async () => { + const tx = setupTransaction(); + tx.feedbackSource.create.mockResolvedValue({ id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }); + tx.feedbackSourceFormbricksMapping.create.mockResolvedValue({}); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue(mockFeedbackSourceWithMappingsFromDb); + + await createFeedbackSourceWithMappings( + ENV_ID, + { name: "FB", type: "formbricks_survey", feedbackDirectoryId: FRD_ID }, + { + type: "formbricks_survey", + mappings: [ + { surveyId: SURVEY_ID, elementId: "el-1", hubFieldType: "text" }, + { surveyId: SURVEY_ID, elementId: "el-2", hubFieldType: "nps" }, + ], + } + ); + + expect(tx.feedbackSourceFormbricksMapping.create).toHaveBeenCalledTimes(2); + expect(tx.feedbackSourceFormbricksMapping.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + feedbackSourceId: FEEDBACK_SOURCE_ID, + workspaceId: ENV_ID, + surveyId: SURVEY_ID, + elementId: "el-1", + hubFieldType: "text", + }), + }) + ); + }); + + test("creates feedbackSource with field mappings", async () => { + const tx = setupTransaction(); + tx.feedbackSource.create.mockResolvedValue({ id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }); + tx.feedbackSourceFieldMapping.create.mockResolvedValue({}); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue({ + ...mockFeedbackSource, + formbricksMappings: [], + fieldMappings: [], + }); + + await createFeedbackSourceWithMappings( + ENV_ID, + { name: "CSV", type: "csv", feedbackDirectoryId: FRD_ID }, + { + type: "field", + mappings: [{ sourceFieldId: "col-1", targetFieldId: "value_text" }], + } + ); + + expect(tx.feedbackSourceFieldMapping.create).toHaveBeenCalledTimes(1); + expect(tx.feedbackSourceFieldMapping.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + feedbackSourceId: FEEDBACK_SOURCE_ID, + workspaceId: ENV_ID, + sourceFieldId: "col-1", + targetFieldId: "value_text", + }), + }) + ); + }); + + test("throws FEEDBACK_SOURCE_NAME_DUPLICATE on FeedbackSource name unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "name"] }, + }) + ); + + await expect( + createFeedbackSourceWithMappings(ENV_ID, { + name: "Dup", + type: "formbricks_survey", + feedbackDirectoryId: FRD_ID, + }) + ).rejects.toThrow(new InvalidInputError("FEEDBACK_SOURCE_NAME_DUPLICATE")); + }); + + test("throws FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE on mapping unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "feedbackSourceId", "surveyId", "elementId"] }, + }) + ); + + await expect( + createFeedbackSourceWithMappings(ENV_ID, { + name: "Dup mapping", + type: "formbricks_survey", + feedbackDirectoryId: FRD_ID, + }) + ).rejects.toThrow(new InvalidInputError("FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE")); + }); + + test("throws FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE on field mapping unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "feedbackSourceId", "sourceFieldId", "targetFieldId"] }, + }) + ); + + await expect( + createFeedbackSourceWithMappings(ENV_ID, { + name: "Dup field mapping", + type: "csv", + feedbackDirectoryId: FRD_ID, + }) + ).rejects.toThrow(new InvalidInputError("FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE")); + }); + + test("throws DatabaseError on generic Prisma error", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("DB error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect( + createFeedbackSourceWithMappings(ENV_ID, { name: "Fail", type: "csv", feedbackDirectoryId: FRD_ID }) + ).rejects.toThrow(DatabaseError); + }); +}); + +describe("updateFeedbackSourceWithMappings", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const setupTransaction = () => { + const txMethods = { + feedbackSource: { + update: vi.fn(), + findUniqueOrThrow: vi.fn(), + }, + feedbackSourceFormbricksMapping: { + create: vi.fn(), + deleteMany: vi.fn(), + }, + feedbackSourceFieldMapping: { + create: vi.fn(), + deleteMany: vi.fn(), + }, + }; + + vi.mocked(prisma.$transaction).mockImplementation(async (fn) => { + return (fn as unknown as (tx: typeof txMethods) => Promise)(txMethods); + }); + + return txMethods; + }; + + test("updates feedbackSource name without changing mappings", async () => { + const tx = setupTransaction(); + tx.feedbackSource.update.mockResolvedValue(undefined); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue(mockFeedbackSourceWithMappingsFromDb); + + const result = await updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "Updated" }); + + expect(tx.feedbackSource.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }, + data: expect.objectContaining({ name: "Updated" }), + }) + ); + expect(tx.feedbackSourceFormbricksMapping.deleteMany).not.toHaveBeenCalled(); + expect(tx.feedbackSourceFieldMapping.deleteMany).not.toHaveBeenCalled(); + expect(result).toEqual(mockFeedbackSourceWithMappings); + }); + + test("replaces formbricks mappings when provided", async () => { + const tx = setupTransaction(); + tx.feedbackSource.update.mockResolvedValue(undefined); + tx.feedbackSourceFormbricksMapping.deleteMany.mockResolvedValue({ count: 1 }); + tx.feedbackSourceFormbricksMapping.create.mockResolvedValue({}); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue(mockFeedbackSourceWithMappingsFromDb); + + await updateFeedbackSourceWithMappings( + FEEDBACK_SOURCE_ID, + ENV_ID, + { name: "Updated" }, + { + type: "formbricks_survey", + mappings: [{ surveyId: SURVEY_ID, elementId: "el-new", hubFieldType: "nps" }], + } + ); + + expect(tx.feedbackSourceFormbricksMapping.deleteMany).toHaveBeenCalledWith({ + where: { feedbackSourceId: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }, + }); + expect(tx.feedbackSourceFormbricksMapping.create).toHaveBeenCalledTimes(1); + }); + + test("replaces field mappings when provided", async () => { + const tx = setupTransaction(); + tx.feedbackSource.update.mockResolvedValue(undefined); + tx.feedbackSourceFieldMapping.deleteMany.mockResolvedValue({ count: 1 }); + tx.feedbackSourceFieldMapping.create.mockResolvedValue({}); + tx.feedbackSource.findUniqueOrThrow.mockResolvedValue({ + ...mockFeedbackSource, + formbricksMappings: [], + fieldMappings: [], + }); + + await updateFeedbackSourceWithMappings( + FEEDBACK_SOURCE_ID, + ENV_ID, + { name: "CSV Updated" }, + { + type: "field", + mappings: [{ sourceFieldId: "col-x", targetFieldId: "value_number" }], + } + ); + + expect(tx.feedbackSourceFieldMapping.deleteMany).toHaveBeenCalledWith({ + where: { feedbackSourceId: FEEDBACK_SOURCE_ID, workspaceId: ENV_ID }, + }); + expect(tx.feedbackSourceFieldMapping.create).toHaveBeenCalledTimes(1); + }); + + test("throws ResourceNotFoundError when feedbackSource does not exist", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Not found", { + code: "P2015", + clientVersion: "5.0.0", + }) + ); + + await expect(updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + ResourceNotFoundError + ); + }); + + test("throws FEEDBACK_SOURCE_NAME_DUPLICATE on FeedbackSource name unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "name"] }, + }) + ); + + await expect( + updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "Dup" }) + ).rejects.toThrow(new InvalidInputError("FEEDBACK_SOURCE_NAME_DUPLICATE")); + }); + + test("throws FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE on formbricks mapping unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "feedbackSourceId", "surveyId", "elementId"] }, + }) + ); + + await expect(updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + new InvalidInputError("FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE") + ); + }); + + test("throws FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE on field mapping unique violation", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Unique constraint", { + code: "P2002", + clientVersion: "5.0.0", + meta: { target: ["workspaceId", "feedbackSourceId", "sourceFieldId", "targetFieldId"] }, + }) + ); + + await expect(updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + new InvalidInputError("FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE") + ); + }); + + test("throws DatabaseError on generic Prisma error", async () => { + vi.mocked(prisma.$transaction).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("DB error", { + code: "P1001", + clientVersion: "5.0.0", + }) + ); + + await expect(updateFeedbackSourceWithMappings(FEEDBACK_SOURCE_ID, ENV_ID, { name: "x" })).rejects.toThrow( + DatabaseError + ); + }); +}); diff --git a/apps/web/lib/connector/service.ts b/apps/web/lib/feedback-source/service.ts similarity index 58% rename from apps/web/lib/connector/service.ts rename to apps/web/lib/feedback-source/service.ts index ac45af18bb21..ff7c70473de7 100644 --- a/apps/web/lib/connector/service.ts +++ b/apps/web/lib/feedback-source/service.ts @@ -5,21 +5,21 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; -import { - TConnector, - TConnectorCreateInput, - TConnectorFieldMappingCreateInput, - TConnectorFormbricksMappingCreateInput, - TConnectorUpdateInput, - TConnectorWithMappings, - ZConnectorCreateInput, - ZConnectorUpdateInput, -} from "@formbricks/types/connector"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { + TFeedbackSource, + TFeedbackSourceCreateInput, + TFeedbackSourceFieldMappingCreateInput, + TFeedbackSourceFormbricksMappingCreateInput, + TFeedbackSourceUpdateInput, + TFeedbackSourceWithMappings, + ZFeedbackSourceCreateInput, + ZFeedbackSourceUpdateInput, +} from "@formbricks/types/feedback-source"; import { ITEMS_PER_PAGE } from "../constants"; import { validateInputs } from "../utils/validate"; -const selectConnectorWithMappings = { +const selectFeedbackSourceWithMappings = { id: true, createdAt: true, updatedAt: true, @@ -35,7 +35,7 @@ const selectConnectorWithMappings = { select: { id: true, createdAt: true, - connectorId: true, + feedbackSourceId: true, workspaceId: true, surveyId: true, elementId: true, @@ -47,16 +47,16 @@ const selectConnectorWithMappings = { select: { id: true, createdAt: true, - connectorId: true, + feedbackSourceId: true, workspaceId: true, sourceFieldId: true, targetFieldId: true, staticValue: true, }, }, -} satisfies Prisma.ConnectorSelect; +} satisfies Prisma.FeedbackSourceSelect; -const selectConnector = { +const selectFeedbackSource = { id: true, createdAt: true, updatedAt: true, @@ -67,25 +67,29 @@ const selectConnector = { feedbackDirectoryId: true, lastSyncAt: true, createdBy: true, -} satisfies Prisma.ConnectorSelect; +} satisfies Prisma.FeedbackSourceSelect; -type PrismaConnectorWithCreator = Prisma.ConnectorGetPayload<{ select: typeof selectConnectorWithMappings }>; +type PrismaFeedbackSourceWithCreator = Prisma.FeedbackSourceGetPayload<{ + select: typeof selectFeedbackSourceWithMappings; +}>; -const mapConnectorWithMappings = (connector: PrismaConnectorWithCreator): TConnectorWithMappings => { - const { creator, ...rest } = connector; - return { ...rest, creatorName: creator?.name ?? null } as TConnectorWithMappings; +const mapFeedbackSourceWithMappings = ( + feedbackSource: PrismaFeedbackSourceWithCreator +): TFeedbackSourceWithMappings => { + const { creator, ...rest } = feedbackSource; + return { ...rest, creatorName: creator?.name ?? null } as TFeedbackSourceWithMappings; }; -export const getConnectorsWithMappings = reactCache( - async (workspaceId: string, page?: number): Promise => { +export const getFeedbackSourcesWithMappings = reactCache( + async (workspaceId: string, page?: number): Promise => { validateInputs([workspaceId, ZId], [page, ZOptionalNumber]); try { - const connectors = await prisma.connector.findMany({ + const feedbackSources = await prisma.feedbackSource.findMany({ where: { workspaceId, }, - select: selectConnectorWithMappings, + select: selectFeedbackSourceWithMappings, orderBy: { createdAt: "desc", }, @@ -93,7 +97,7 @@ export const getConnectorsWithMappings = reactCache( skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, }); - return connectors.map(mapConnectorWithMappings); + return feedbackSources.map(mapFeedbackSourceWithMappings); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -103,20 +107,20 @@ export const getConnectorsWithMappings = reactCache( } ); -export const getConnectorWithMappingsById = reactCache( - async (connectorId: string, workspaceId: string): Promise => { - validateInputs([connectorId, ZId], [workspaceId, ZId]); +export const getFeedbackSourceWithMappingsById = reactCache( + async (feedbackSourceId: string, workspaceId: string): Promise => { + validateInputs([feedbackSourceId, ZId], [workspaceId, ZId]); try { - const connector = await prisma.connector.findUnique({ + const feedbackSource = await prisma.feedbackSource.findUnique({ where: { - id: connectorId, + id: feedbackSourceId, workspaceId, }, - select: selectConnectorWithMappings, + select: selectFeedbackSourceWithMappings, }); - return connector ? mapConnectorWithMappings(connector) : null; + return feedbackSource ? mapFeedbackSourceWithMappings(feedbackSource) : null; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -126,12 +130,12 @@ export const getConnectorWithMappingsById = reactCache( } ); -export const getConnectorsBySurveyId = reactCache( - async (surveyId: string): Promise => { +export const getFeedbackSourcesBySurveyId = reactCache( + async (surveyId: string): Promise => { validateInputs([surveyId, ZId]); try { - const connectors = await prisma.connector.findMany({ + const feedbackSources = await prisma.feedbackSource.findMany({ where: { type: "formbricks_survey", status: "active", @@ -141,10 +145,10 @@ export const getConnectorsBySurveyId = reactCache( }, }, }, - select: selectConnectorWithMappings, + select: selectFeedbackSourceWithMappings, }); - return connectors.map(mapConnectorWithMappings); + return feedbackSources.map(mapFeedbackSourceWithMappings); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -154,17 +158,17 @@ export const getConnectorsBySurveyId = reactCache( } ); -export const updateConnector = async ( - connectorId: string, +export const updateFeedbackSource = async ( + feedbackSourceId: string, workspaceId: string, - data: TConnectorUpdateInput -): Promise => { - validateInputs([connectorId, ZId], [data, ZConnectorUpdateInput], [workspaceId, ZId]); + data: TFeedbackSourceUpdateInput +): Promise => { + validateInputs([feedbackSourceId, ZId], [data, ZFeedbackSourceUpdateInput], [workspaceId, ZId]); try { - const connector = await prisma.connector.update({ + const feedbackSource = await prisma.feedbackSource.update({ where: { - id: connectorId, + id: feedbackSourceId, workspaceId, }, data: { @@ -172,14 +176,14 @@ export const updateConnector = async ( status: data.status, lastSyncAt: data.lastSyncAt, }, - select: selectConnector, + select: selectFeedbackSource, }); - return connector as TConnector; + return feedbackSource; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === PrismaErrorType.RecordDoesNotExist) { - throw new ResourceNotFoundError("Connector", connectorId); + throw new ResourceNotFoundError("FeedbackSource", feedbackSourceId); } throw new DatabaseError(error.message); } @@ -187,23 +191,26 @@ export const updateConnector = async ( } }; -export const deleteConnector = async (connectorId: string, workspaceId: string): Promise => { - validateInputs([connectorId, ZId], [workspaceId, ZId]); +export const deleteFeedbackSource = async ( + feedbackSourceId: string, + workspaceId: string +): Promise => { + validateInputs([feedbackSourceId, ZId], [workspaceId, ZId]); try { - const connector = await prisma.connector.delete({ + const feedbackSource = await prisma.feedbackSource.delete({ where: { - id: connectorId, + id: feedbackSourceId, workspaceId, }, - select: selectConnector, + select: selectFeedbackSource, }); - return connector as TConnector; + return feedbackSource; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === PrismaErrorType.RecordDoesNotExist) { - throw new ResourceNotFoundError("Connector", connectorId); + throw new ResourceNotFoundError("FeedbackSource", feedbackSourceId); } throw new DatabaseError(error.message); } @@ -217,36 +224,36 @@ const mapUniqueConstraintError = (error: PrismaClientKnownRequestError): Invalid const target = error.meta?.target; const targetFields = Array.isArray(target) ? (target as string[]) : []; if (targetFields.includes("elementId") || targetFields.includes("surveyId")) { - return new InvalidInputError("CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE"); + return new InvalidInputError("FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE"); } if (targetFields.includes("sourceFieldId") || targetFields.includes("targetFieldId")) { - return new InvalidInputError("CONNECTOR_FIELD_MAPPING_DUPLICATE"); + return new InvalidInputError("FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE"); } - return new InvalidInputError("CONNECTOR_NAME_DUPLICATE"); + return new InvalidInputError("FEEDBACK_SOURCE_NAME_DUPLICATE"); }; export type TFormbricksMappingsInput = { type: "formbricks_survey"; - mappings: TConnectorFormbricksMappingCreateInput[]; + mappings: TFeedbackSourceFormbricksMappingCreateInput[]; }; export type TFieldMappingsInput = { type: "field"; - mappings: TConnectorFieldMappingCreateInput[]; + mappings: TFeedbackSourceFieldMappingCreateInput[]; }; export type TMappingsInput = TFormbricksMappingsInput | TFieldMappingsInput; -export const createConnectorWithMappings = async ( +export const createFeedbackSourceWithMappings = async ( workspaceId: string, - data: TConnectorCreateInput, + data: TFeedbackSourceCreateInput, mappingsInput?: TMappingsInput -): Promise => { - validateInputs([workspaceId, ZId], [data, ZConnectorCreateInput]); +): Promise => { + validateInputs([workspaceId, ZId], [data, ZFeedbackSourceCreateInput]); try { const result = await prisma.$transaction(async (tx) => { - const connector = await tx.connector.create({ + const feedbackSource = await tx.feedbackSource.create({ data: { name: data.name, type: data.type, @@ -259,9 +266,9 @@ export const createConnectorWithMappings = async ( if (mappingsInput?.type === "formbricks_survey") { await Promise.all( mappingsInput.mappings.map((mapping) => - tx.connectorFormbricksMapping.create({ + tx.feedbackSourceFormbricksMapping.create({ data: { - connectorId: connector.id, + feedbackSourceId: feedbackSource.id, workspaceId, surveyId: mapping.surveyId, elementId: mapping.elementId, @@ -274,9 +281,9 @@ export const createConnectorWithMappings = async ( } else if (mappingsInput?.type === "field") { await Promise.all( mappingsInput.mappings.map((mapping) => - tx.connectorFieldMapping.create({ + tx.feedbackSourceFieldMapping.create({ data: { - connectorId: connector.id, + feedbackSourceId: feedbackSource.id, workspaceId, sourceFieldId: mapping.sourceFieldId, targetFieldId: mapping.targetFieldId, @@ -287,13 +294,13 @@ export const createConnectorWithMappings = async ( ); } - return tx.connector.findUniqueOrThrow({ - where: { id: connector.id }, - select: selectConnectorWithMappings, + return tx.feedbackSource.findUniqueOrThrow({ + where: { id: feedbackSource.id }, + select: selectFeedbackSourceWithMappings, }); }); - return mapConnectorWithMappings(result); + return mapFeedbackSourceWithMappings(result); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === PrismaErrorType.UniqueConstraintViolation) { @@ -305,18 +312,18 @@ export const createConnectorWithMappings = async ( } }; -export const updateConnectorWithMappings = async ( - connectorId: string, +export const updateFeedbackSourceWithMappings = async ( + feedbackSourceId: string, workspaceId: string, - data: TConnectorUpdateInput, + data: TFeedbackSourceUpdateInput, mappingsInput?: TMappingsInput -): Promise => { - validateInputs([connectorId, ZId], [data, ZConnectorUpdateInput], [workspaceId, ZId]); +): Promise => { + validateInputs([feedbackSourceId, ZId], [data, ZFeedbackSourceUpdateInput], [workspaceId, ZId]); try { const result = await prisma.$transaction(async (tx) => { - await tx.connector.update({ - where: { id: connectorId, workspaceId }, + await tx.feedbackSource.update({ + where: { id: feedbackSourceId, workspaceId }, data: { name: data.name, status: data.status, @@ -325,15 +332,15 @@ export const updateConnectorWithMappings = async ( }); if (mappingsInput?.type === "formbricks_survey") { - await tx.connectorFormbricksMapping.deleteMany({ - where: { connectorId, workspaceId }, + await tx.feedbackSourceFormbricksMapping.deleteMany({ + where: { feedbackSourceId, workspaceId }, }); await Promise.all( mappingsInput.mappings.map((mapping) => - tx.connectorFormbricksMapping.create({ + tx.feedbackSourceFormbricksMapping.create({ data: { - connectorId, + feedbackSourceId, workspaceId, surveyId: mapping.surveyId, elementId: mapping.elementId, @@ -344,15 +351,15 @@ export const updateConnectorWithMappings = async ( ) ); } else if (mappingsInput?.type === "field") { - await tx.connectorFieldMapping.deleteMany({ - where: { connectorId, workspaceId }, + await tx.feedbackSourceFieldMapping.deleteMany({ + where: { feedbackSourceId, workspaceId }, }); await Promise.all( mappingsInput.mappings.map((mapping) => - tx.connectorFieldMapping.create({ + tx.feedbackSourceFieldMapping.create({ data: { - connectorId, + feedbackSourceId, workspaceId, sourceFieldId: mapping.sourceFieldId, targetFieldId: mapping.targetFieldId, @@ -363,20 +370,20 @@ export const updateConnectorWithMappings = async ( ); } - return tx.connector.findUniqueOrThrow({ - where: { id: connectorId }, - select: selectConnectorWithMappings, + return tx.feedbackSource.findUniqueOrThrow({ + where: { id: feedbackSourceId }, + select: selectFeedbackSourceWithMappings, }); }); - return mapConnectorWithMappings(result); + return mapFeedbackSourceWithMappings(result); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === PrismaErrorType.UniqueConstraintViolation) { throw mapUniqueConstraintError(error); } if (error.code === PrismaErrorType.RecordDoesNotExist) { - throw new ResourceNotFoundError("Connector", connectorId); + throw new ResourceNotFoundError("FeedbackSource", feedbackSourceId); } throw new DatabaseError(error.message); } diff --git a/apps/web/lib/connector/transform.test.ts b/apps/web/lib/feedback-source/transform.test.ts similarity index 98% rename from apps/web/lib/connector/transform.test.ts rename to apps/web/lib/feedback-source/transform.test.ts index 4279a6987ab7..99588804ce0c 100644 --- a/apps/web/lib/connector/transform.test.ts +++ b/apps/web/lib/feedback-source/transform.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; -import { TConnectorFormbricksMapping } from "@formbricks/types/connector"; +import { TFeedbackSourceFormbricksMapping } from "@formbricks/types/feedback-source"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; import { transformResponseToFeedbackRecords } from "./transform"; @@ -58,19 +58,19 @@ const mockResponse = { } as unknown as TResponse; const createMapping = ( - overrides: Partial & - Pick -): TConnectorFormbricksMapping => ({ + overrides: Partial & + Pick +): TFeedbackSourceFormbricksMapping => ({ id: `mapping-${overrides.elementId}`, createdAt: NOW, - connectorId: "conn-1", + feedbackSourceId: "conn-1", workspaceId: "env-1", surveyId: "survey-1", customFieldLabel: null, ...overrides, }); -const allMappings: TConnectorFormbricksMapping[] = [ +const allMappings: TFeedbackSourceFormbricksMapping[] = [ createMapping({ elementId: "el-text", hubFieldType: "text" }), createMapping({ elementId: "el-nps", hubFieldType: "nps" }), createMapping({ elementId: "el-rating", hubFieldType: "rating" }), @@ -374,7 +374,7 @@ describe("transformResponseToFeedbackRecords", () => { const mappings = [ createMapping({ elementId: "el-multi", - hubFieldType: "unknown-type" as TConnectorFormbricksMapping["hubFieldType"], + hubFieldType: "unknown-type" as TFeedbackSourceFormbricksMapping["hubFieldType"], }), ]; const result = transformResponseToFeedbackRecords(response, mockSurvey, mappings, mockTenantId); diff --git a/apps/web/lib/connector/transform.ts b/apps/web/lib/feedback-source/transform.ts similarity index 97% rename from apps/web/lib/connector/transform.ts rename to apps/web/lib/feedback-source/transform.ts index 8990e37ab3e7..8db2442bd12d 100644 --- a/apps/web/lib/connector/transform.ts +++ b/apps/web/lib/feedback-source/transform.ts @@ -1,5 +1,5 @@ import "server-only"; -import { TConnectorFormbricksMapping, THubFieldType } from "@formbricks/types/connector"; +import { TFeedbackSourceFormbricksMapping, THubFieldType } from "@formbricks/types/feedback-source"; import { TResponse, TResponseData, TResponseDataValue } from "@formbricks/types/responses"; import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants"; import type { @@ -138,7 +138,7 @@ const buildBaseFields = ( const expandMatrixToRecords = ( element: TSurveyMatrixElement, - mapping: TConnectorFormbricksMapping, + mapping: TFeedbackSourceFormbricksMapping, value: TResponseDataValue, baseFields: BaseRecordFields ): FeedbackRecordCreateParams[] => { @@ -173,7 +173,7 @@ const expandMatrixToRecords = ( const expandRankingToRecords = ( element: TSurveyRankingElement, - mapping: TConnectorFormbricksMapping, + mapping: TFeedbackSourceFormbricksMapping, value: TResponseDataValue, baseFields: BaseRecordFields ): FeedbackRecordCreateParams[] => { @@ -214,7 +214,7 @@ const expandRankingToRecords = ( export function transformResponseToFeedbackRecords( response: TResponse, survey: Pick, - mappings: TConnectorFormbricksMapping[], + mappings: TFeedbackSourceFormbricksMapping[], tenantId: string ): FeedbackRecordCreateParams[] { const responseData = response.data; diff --git a/apps/web/lib/connector/utils.test.ts b/apps/web/lib/feedback-source/utils.test.ts similarity index 95% rename from apps/web/lib/connector/utils.test.ts rename to apps/web/lib/feedback-source/utils.test.ts index 2578fa1c48b0..32eabd3d00c1 100644 --- a/apps/web/lib/connector/utils.test.ts +++ b/apps/web/lib/feedback-source/utils.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "vitest"; -import { type TConnectorFieldMappingCreateInput, ZHubFieldType } from "@formbricks/types/connector"; +import { + type TFeedbackSourceFieldMappingCreateInput, + ZHubFieldType, +} from "@formbricks/types/feedback-source"; import { formatCsvMissingMappedSourceColumns, formatMissingRequiredCsvFieldMappingsMessage, @@ -12,7 +15,7 @@ import { describe("sanitizeCsvFieldMappings", () => { test("drops user-controlled tenant_id and source_type mappings", () => { - const mappings: TConnectorFieldMappingCreateInput[] = [ + const mappings: TFeedbackSourceFieldMappingCreateInput[] = [ { sourceFieldId: "tenant", targetFieldId: "tenant_id" }, { sourceFieldId: "type", targetFieldId: "source_type" }, { sourceFieldId: "question", targetFieldId: "field_id" }, @@ -30,7 +33,7 @@ describe("sanitizeCsvFieldMappings", () => { }); test("returns only the static csv source_type mapping when input is all-protected", () => { - const mappings: TConnectorFieldMappingCreateInput[] = [ + const mappings: TFeedbackSourceFieldMappingCreateInput[] = [ { sourceFieldId: "tenant", targetFieldId: "tenant_id" }, { sourceFieldId: "type", targetFieldId: "source_type" }, ]; @@ -42,7 +45,7 @@ describe("sanitizeCsvFieldMappings", () => { }); describe("getMissingRequiredCsvFieldMappings", () => { - const requiredMappings: TConnectorFieldMappingCreateInput[] = [ + const requiredMappings: TFeedbackSourceFieldMappingCreateInput[] = [ { sourceFieldId: "response_id", targetFieldId: "submission_id" }, { sourceFieldId: "question_id", targetFieldId: "field_id" }, { sourceFieldId: "question", targetFieldId: "field_label" }, diff --git a/apps/web/lib/connector/utils.ts b/apps/web/lib/feedback-source/utils.ts similarity index 90% rename from apps/web/lib/connector/utils.ts rename to apps/web/lib/feedback-source/utils.ts index 14c84bf1a0f4..06df52b0672a 100644 --- a/apps/web/lib/connector/utils.ts +++ b/apps/web/lib/feedback-source/utils.ts @@ -1,5 +1,8 @@ -import type { TConnectorFieldMappingCreateInput, THubFieldType } from "@formbricks/types/connector"; -import { ZHubFieldType } from "@formbricks/types/connector"; +import type { + TFeedbackSourceFieldMappingCreateInput, + THubFieldType, +} from "@formbricks/types/feedback-source"; +import { ZHubFieldType } from "@formbricks/types/feedback-source"; import { CSV_HIDDEN_STATIC_MAPPINGS, CSV_PROTECTED_TARGET_IDS, @@ -7,15 +10,15 @@ import { } from "@/modules/ee/unify-feedback/sources/types"; export const sanitizeCsvFieldMappings = ( - fieldMappings: TConnectorFieldMappingCreateInput[] | undefined -): TConnectorFieldMappingCreateInput[] | undefined => { + fieldMappings: TFeedbackSourceFieldMappingCreateInput[] | undefined +): TFeedbackSourceFieldMappingCreateInput[] | undefined => { if (!fieldMappings?.length) return undefined; const userMappings = fieldMappings.filter((mapping) => CSV_PROTECTED_TARGET_IDS.every((id) => mapping.targetFieldId !== id) ); - return [...userMappings, ...(CSV_HIDDEN_STATIC_MAPPINGS as TConnectorFieldMappingCreateInput[])]; + return [...userMappings, ...(CSV_HIDDEN_STATIC_MAPPINGS as TFeedbackSourceFieldMappingCreateInput[])]; }; type TCsvFieldMappingLike = { diff --git a/apps/web/lib/utils/helper.test.ts b/apps/web/lib/utils/helper.test.ts index 562e02d6a2b3..81c23a8e0582 100644 --- a/apps/web/lib/utils/helper.test.ts +++ b/apps/web/lib/utils/helper.test.ts @@ -5,8 +5,9 @@ import { getFormattedErrorMessage, getOrganizationIdFromActionClassId, getOrganizationIdFromApiKeyId, - getOrganizationIdFromConnectorId, + getOrganizationIdFromContactAttributeKeyId, getOrganizationIdFromContactId, + getOrganizationIdFromFeedbackSourceId, getOrganizationIdFromIntegrationId, getOrganizationIdFromInviteId, getOrganizationIdFromLanguageId, @@ -19,6 +20,7 @@ import { getOrganizationIdFromWebhookId, getOrganizationIdFromWorkspaceId, getWorkspaceIdFromActionClassId, + getWorkspaceIdFromContactAttributeKeyId, getWorkspaceIdFromContactId, getWorkspaceIdFromIntegrationId, getWorkspaceIdFromLanguageId, @@ -37,6 +39,7 @@ vi.mock("@/lib/utils/services", () => ({ getSurvey: vi.fn(), getResponse: vi.fn(), getContact: vi.fn(), + getContactAttributeKey: vi.fn(), getQuota: vi.fn(), getSegment: vi.fn(), getActionClass: vi.fn(), @@ -47,7 +50,7 @@ vi.mock("@/lib/utils/services", () => ({ getLanguage: vi.fn(), getTeam: vi.fn(), getTag: vi.fn(), - getConnector: vi.fn(), + getFeedbackSource: vi.fn(), })); describe("Helper Utilities", () => { @@ -169,6 +172,26 @@ describe("Helper Utilities", () => { await expect(getOrganizationIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError); }); + test("getOrganizationIdFromContactAttributeKeyId returns organization ID correctly", async () => { + vi.mocked(services.getContactAttributeKey).mockResolvedValueOnce({ + workspaceId: "workspace1", + }); + vi.mocked(services.getWorkspace).mockResolvedValueOnce({ + organizationId: "org1", + }); + + const orgId = await getOrganizationIdFromContactAttributeKeyId("attrKey1"); + expect(orgId).toBe("org1"); + }); + + test("getOrganizationIdFromContactAttributeKeyId throws error when key not found", async () => { + vi.mocked(services.getContactAttributeKey).mockResolvedValueOnce(null); + + await expect(getOrganizationIdFromContactAttributeKeyId("nonexistent")).rejects.toThrow( + ResourceNotFoundError + ); + }); + test("getOrganizationIdFromTagId returns organization ID correctly", async () => { vi.mocked(services.getTag).mockResolvedValueOnce({ workspaceId: "workspace1", @@ -329,25 +352,27 @@ describe("Helper Utilities", () => { expect(orgId).toBe("org1"); }); - test("getOrganizationIdFromConnectorId returns organization ID through workspace", async () => { - vi.mocked(services.getConnector).mockResolvedValueOnce({ + test("getOrganizationIdFromFeedbackSourceId returns organization ID through workspace", async () => { + vi.mocked(services.getFeedbackSource).mockResolvedValueOnce({ workspaceId: "workspace1", }); vi.mocked(services.getWorkspace).mockResolvedValueOnce({ organizationId: "org1", }); - const orgId = await getOrganizationIdFromConnectorId("connector1"); + const orgId = await getOrganizationIdFromFeedbackSourceId("feedbackSource1"); expect(orgId).toBe("org1"); - expect(services.getConnector).toHaveBeenCalledWith("connector1"); + expect(services.getFeedbackSource).toHaveBeenCalledWith("feedbackSource1"); expect(services.getWorkspace).toHaveBeenCalledWith("workspace1"); }); - test("getOrganizationIdFromConnectorId throws error when connector not found", async () => { - vi.mocked(services.getConnector).mockResolvedValueOnce(null); + test("getOrganizationIdFromFeedbackSourceId throws error when feedbackSource not found", async () => { + vi.mocked(services.getFeedbackSource).mockResolvedValueOnce(null); - await expect(getOrganizationIdFromConnectorId("nonexistent")).rejects.toThrow(ResourceNotFoundError); - expect(services.getConnector).toHaveBeenCalledWith("nonexistent"); + await expect(getOrganizationIdFromFeedbackSourceId("nonexistent")).rejects.toThrow( + ResourceNotFoundError + ); + expect(services.getFeedbackSource).toHaveBeenCalledWith("nonexistent"); }); }); @@ -381,6 +406,22 @@ describe("Helper Utilities", () => { await expect(getWorkspaceIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError); }); + test("getWorkspaceIdFromContactAttributeKeyId returns workspace ID correctly", async () => { + vi.mocked(services.getContactAttributeKey).mockResolvedValueOnce({ + workspaceId: "workspace1", + }); + + const workspaceId = await getWorkspaceIdFromContactAttributeKeyId("attrKey1"); + expect(workspaceId).toBe("workspace1"); + }); + + test("getWorkspaceIdFromContactAttributeKeyId throws error when key not found", async () => { + vi.mocked(services.getContactAttributeKey).mockResolvedValueOnce(null); + await expect(getWorkspaceIdFromContactAttributeKeyId("nonexistent")).rejects.toThrow( + ResourceNotFoundError + ); + }); + test("getWorkspaceIdFromSegmentId returns workspace ID correctly", async () => { vi.mocked(services.getSegment).mockResolvedValueOnce({ workspaceId: "workspace1", diff --git a/apps/web/lib/utils/helper.ts b/apps/web/lib/utils/helper.ts index 378746f048ad..dab939425a7d 100644 --- a/apps/web/lib/utils/helper.ts +++ b/apps/web/lib/utils/helper.ts @@ -2,8 +2,9 @@ import { ResourceNotFoundError } from "@formbricks/types/errors"; import { getActionClass, getApiKey, - getConnector, getContact, + getContactAttributeKey, + getFeedbackSource, getIntegration, getInvite, getLanguage, @@ -88,6 +89,15 @@ export const getOrganizationIdFromContactId = async (contactId: string) => { return await getOrganizationIdFromWorkspaceId(contact.workspaceId); }; +export const getOrganizationIdFromContactAttributeKeyId = async (contactAttributeKeyId: string) => { + const contactAttributeKey = await getContactAttributeKey(contactAttributeKeyId); + if (!contactAttributeKey) { + throw new ResourceNotFoundError("ContactAttributeKey", contactAttributeKeyId); + } + + return await getOrganizationIdFromWorkspaceId(contactAttributeKey.workspaceId); +}; + export const getOrganizationIdFromTagId = async (tagId: string) => { const tag = await getTag(tagId); if (!tag) { @@ -239,6 +249,15 @@ export const getWorkspaceIdFromContactId = async (contactId: string) => { return contact.workspaceId; }; +export const getWorkspaceIdFromContactAttributeKeyId = async (contactAttributeKeyId: string) => { + const contactAttributeKey = await getContactAttributeKey(contactAttributeKeyId); + if (!contactAttributeKey) { + throw new ResourceNotFoundError("ContactAttributeKey", contactAttributeKeyId); + } + + return contactAttributeKey.workspaceId; +}; + export const getWorkspaceIdFromIntegrationId = async (integrationId: string) => { const integration = await getIntegration(integrationId); if (!integration) { @@ -274,12 +293,12 @@ export const isStringMatch = (query: string, value: string): boolean => { return valueModified.includes(queryModified); }; -// Connector helpers -export const getOrganizationIdFromConnectorId = async (connectorId: string) => { - const connector = await getConnector(connectorId); - if (!connector) { - throw new ResourceNotFoundError("connector", connectorId); +// FeedbackSource helpers +export const getOrganizationIdFromFeedbackSourceId = async (feedbackSourceId: string) => { + const feedbackSource = await getFeedbackSource(feedbackSourceId); + if (!feedbackSource) { + throw new ResourceNotFoundError("feedbackSource", feedbackSourceId); } - return await getOrganizationIdFromWorkspaceId(connector.workspaceId); + return await getOrganizationIdFromWorkspaceId(feedbackSource.workspaceId); }; diff --git a/apps/web/lib/utils/services.test.ts b/apps/web/lib/utils/services.test.ts index 124d6a07d9df..27244cbe29da 100644 --- a/apps/web/lib/utils/services.test.ts +++ b/apps/web/lib/utils/services.test.ts @@ -23,8 +23,9 @@ import { getQuota as getQuotaService } from "@/modules/ee/quotas/lib/quotas"; import { getActionClass, getApiKey, - getConnector, getContact, + getContactAttributeKey, + getFeedbackSource, getIntegration, getInvite, getLanguage, @@ -90,7 +91,7 @@ vi.mock("@formbricks/database", () => ({ contact: { findUnique: vi.fn(), }, - connector: { + feedbackSource: { findUnique: vi.fn(), }, segment: { @@ -99,6 +100,9 @@ vi.mock("@formbricks/database", () => ({ surveyQuota: { findUnique: vi.fn(), }, + contactAttributeKey: { + findUnique: vi.fn(), + }, }, })); @@ -561,45 +565,87 @@ describe("Service Functions", () => { }); }); - describe("getConnector", () => { - const connectorId = "connector123"; + describe("getFeedbackSource", () => { + const feedbackSourceId = "feedback_source_123"; + + test("returns the feedbackSource when found", async () => { + const mockFeedbackSource = { workspaceId: "ws123" }; + vi.mocked(prisma.feedbackSource.findUnique).mockResolvedValue(mockFeedbackSource as never); + + const result = await getFeedbackSource(feedbackSourceId); + expect(validateInputs).toHaveBeenCalled(); + expect(prisma.feedbackSource.findUnique).toHaveBeenCalledWith({ + where: { id: feedbackSourceId }, + select: { workspaceId: true }, + }); + expect(result).toEqual(mockFeedbackSource); + }); + + test("returns null when feedbackSource not found", async () => { + vi.mocked(prisma.feedbackSource.findUnique).mockResolvedValue(null); + + const result = await getFeedbackSource(feedbackSourceId); + expect(result).toBeNull(); + }); + + test("throws DatabaseError when Prisma throws a known request error", async () => { + vi.mocked(prisma.feedbackSource.findUnique).mockRejectedValue( + new Prisma.PrismaClientKnownRequestError("Error", { + code: "P2002", + clientVersion: "4.7.0", + }) + ); + + await expect(getFeedbackSource(feedbackSourceId)).rejects.toThrow(DatabaseError); + }); + + test("rethrows unknown errors", async () => { + const unknownError = new Error("Something unexpected"); + vi.mocked(prisma.feedbackSource.findUnique).mockRejectedValue(unknownError); + + await expect(getFeedbackSource(feedbackSourceId)).rejects.toThrow(unknownError); + }); + }); + + describe("getContactAttributeKey", () => { + const contactAttributeKeyId = "attrKey123"; - test("returns the connector when found", async () => { - const mockConnector = { workspaceId: "ws123" }; - vi.mocked(prisma.connector.findUnique).mockResolvedValue(mockConnector); + test("returns the contact attribute key when found", async () => { + const mockAttributeKey = { workspaceId: "ws123" }; + vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValue(mockAttributeKey as never); - const result = await getConnector(connectorId); + const result = await getContactAttributeKey(contactAttributeKeyId); expect(validateInputs).toHaveBeenCalled(); - expect(prisma.connector.findUnique).toHaveBeenCalledWith({ - where: { id: connectorId }, + expect(prisma.contactAttributeKey.findUnique).toHaveBeenCalledWith({ + where: { id: contactAttributeKeyId }, select: { workspaceId: true }, }); - expect(result).toEqual(mockConnector); + expect(result).toEqual(mockAttributeKey); }); - test("returns null when connector not found", async () => { - vi.mocked(prisma.connector.findUnique).mockResolvedValue(null); + test("returns null when contact attribute key not found", async () => { + vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValue(null); - const result = await getConnector(connectorId); + const result = await getContactAttributeKey(contactAttributeKeyId); expect(result).toBeNull(); }); test("throws DatabaseError when Prisma throws a known request error", async () => { - vi.mocked(prisma.connector.findUnique).mockRejectedValue( + vi.mocked(prisma.contactAttributeKey.findUnique).mockRejectedValue( new Prisma.PrismaClientKnownRequestError("Error", { code: "P2002", clientVersion: "4.7.0", }) ); - await expect(getConnector(connectorId)).rejects.toThrow(DatabaseError); + await expect(getContactAttributeKey(contactAttributeKeyId)).rejects.toThrow(DatabaseError); }); test("rethrows unknown errors", async () => { const unknownError = new Error("Something unexpected"); - vi.mocked(prisma.connector.findUnique).mockRejectedValue(unknownError); + vi.mocked(prisma.contactAttributeKey.findUnique).mockRejectedValue(unknownError); - await expect(getConnector(connectorId)).rejects.toThrow(unknownError); + await expect(getContactAttributeKey(contactAttributeKeyId)).rejects.toThrow(unknownError); }); }); }); diff --git a/apps/web/lib/utils/services.ts b/apps/web/lib/utils/services.ts index 87c243a44457..1437d90e6417 100644 --- a/apps/web/lib/utils/services.ts +++ b/apps/web/lib/utils/services.ts @@ -314,18 +314,40 @@ export const getSegment = reactCache(async (segmentId: string): Promise<{ worksp } }); -export const getConnector = reactCache( - async (connectorId: string): Promise<{ workspaceId: string } | null> => { - validateInputs([connectorId, ZId]); +export const getFeedbackSource = reactCache( + async (feedbackSourceId: string): Promise<{ workspaceId: string } | null> => { + validateInputs([feedbackSourceId, ZId]); try { - const connector = await prisma.connector.findUnique({ + const feedbackSource = await prisma.feedbackSource.findUnique({ where: { - id: connectorId, + id: feedbackSourceId, }, select: { workspaceId: true }, }); - return connector; + return feedbackSource; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + } +); + +export const getContactAttributeKey = reactCache( + async (contactAttributeKeyId: string): Promise<{ workspaceId: string } | null> => { + validateInputs([contactAttributeKeyId, ZId]); + try { + const contactAttributeKey = await prisma.contactAttributeKey.findUnique({ + where: { + id: contactAttributeKeyId, + }, + select: { workspaceId: true }, + }); + + return contactAttributeKey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index fdc6e6846012..9f3a796a431f 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -2281,12 +2281,12 @@ "advanced_styling_field_track_bg_description": "Färbt den ungefüllten Teil des Balkens.", "advanced_styling_field_track_height": "Spurhöhe", "advanced_styling_field_track_height_description": "Steuert die Dicke des Fortschrittsbalkens.", - "advanced_styling_field_upper_label_color": "Labelfarbe", - "advanced_styling_field_upper_label_color_description": "Färbt die kleine Beschriftung über Eingabefeldern und Skalenbeschriftungen.", - "advanced_styling_field_upper_label_size": "Label-Schriftgröße", - "advanced_styling_field_upper_label_size_description": "Skaliert die kleine Beschriftung über Eingabefeldern und Skalenbeschriftungen.", - "advanced_styling_field_upper_label_weight": "Label-Schriftstärke", - "advanced_styling_field_upper_label_weight_description": "Macht das Label leichter oder fetter.", + "advanced_styling_field_upper_label_color": "Beschriftungsfarbe", + "advanced_styling_field_upper_label_color_description": "Färbt die kleinen Beschriftungen über Eingabefeldern und Skalenbeschriftungen.", + "advanced_styling_field_upper_label_size": "Schriftgröße der Beschriftung", + "advanced_styling_field_upper_label_size_description": "Skaliert die kleinen Beschriftungen über Eingabefeldern und Skalenbeschriftungen.", + "advanced_styling_field_upper_label_weight": "Schriftstärke der Beschriftung", + "advanced_styling_field_upper_label_weight_description": "Macht die Beschriftungen leichter oder fetter.", "advanced_styling_section_buttons": "Buttons", "advanced_styling_section_headlines": "Überschriften & Beschreibungen", "advanced_styling_section_inputs": "Eingabefelder", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Du darfst dieses Verzeichnis nicht archivieren.", "are_you_sure_you_want_to_archive": "Bist du sicher, dass du dieses Verzeichnis archivieren möchtest? Workspaces haben dann keinen Zugriff mehr darauf.", "assign_workspaces_description": "Steuere, welche Workspaces auf dieses Feedback-Verzeichnis zugreifen können.", - "connectors_description": "Feedback-Quellen, die Einträge an dieses Verzeichnis senden.", "create_feedback_directory": "Feedback-Verzeichnis erstellen", "description": "Verwalte Feedback-Verzeichnisse und ihre Workspace-Zuweisungen.", "directory_archived_successfully": "Verzeichnis erfolgreich archiviert", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Verzeichnis erfolgreich wiederhergestellt", "directory_updated_successfully": "Verzeichnis erfolgreich aktualisiert", "empty_state": "Keine Feedback-Verzeichnisse gefunden. Erstelle eins, um loszulegen.", - "error_directory_has_connectors": "Ein Verzeichnis mit verknüpften Feedback-Quellen kann nicht archiviert werden. Entferne zuerst alle Feedback-Quellen.", + "error_directory_has_feedback_sources": "Ein Verzeichnis mit verknüpften Feedback-Quellen kann nicht archiviert werden. Entferne zuerst alle Feedback-Quellen.", "error_directory_name_duplicate": "Ein Feedback-Verzeichnis mit diesem Namen existiert bereits.", "error_directory_name_required": "Verzeichnisname ist erforderlich.", "error_directory_workspaces_invalid_org": "Einige der angegebenen Workspaces gehören nicht zu dieser Organisation.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Feedback-Quellen, die Einträge an dieses Verzeichnis senden.", "grant_access_confirm": "Workspace-Zugriff gewähren", "grant_workspace_access_title": "Workspace-Zugriff bestätigen", "grant_workspace_access_warning": "Alle aktuellen und zukünftigen Mitglieder der unten aufgeführten Workspaces erhalten Lesezugriff auf alle Feedback-Daten, die in \"{directoryName}\" geleitet werden – einschließlich Daten, die von Feedback-Quellen erfasst wurden, die mit anderen verknüpften Workspaces verbunden sind. Um den Zugriff später zu widerrufen, entferne den Workspace aus diesem Verzeichnis; bereits erfasste Einträge bleiben im Verzeichnis erhalten.", "nav_label": "Feedback-Verzeichnisse", "no_access": "Du hast keine Berechtigung, Feedback-Verzeichnisse zu verwalten.", - "no_connectors": "Noch keine Feedback-Quellen mit diesem Verzeichnis verknüpft.", + "no_feedback_sources": "Noch keine Feedback-Quellen mit diesem Verzeichnis verknüpft.", "no_unassigned_workspaces_description": "Jeder Workspace ist bereits mit einem aktiven Feedback-Verzeichnis verknüpft. Entferne einen Workspace aus seinem aktuellen Verzeichnis, bevor du ihn hier zuweist.", "no_unassigned_workspaces_title": "Keine nicht zugewiesenen Workspaces verfügbar", - "pause_connectors_confirmation_description": "Wenn du diese Feedback-Quellen pausierst, werden keine neuen Einträge mehr hinzugefügt.", - "pause_connectors_confirmation_title": "Verknüpfte Feedback-Quellen pausieren?", + "pause_feedback_sources_confirmation_description": "Wenn du diese Feedback-Quellen pausierst, werden keine neuen Einträge mehr hinzugefügt.", + "pause_feedback_sources_confirmation_title": "Verknüpfte Feedback-Quellen pausieren?", "select_workspaces_placeholder": "Workspaces auswählen...", "show_archived": "Archivierte anzeigen", "title": "Feedback-Verzeichnisse", @@ -3689,14 +3689,6 @@ "collected_at": "Erfasst am", "configure_import": "Import konfigurieren", "configure_mapping": "Mapping konfigurieren", - "connector_created_successfully": "Feedback-Quelle erfolgreich erstellt", - "connector_deleted_successfully": "Feedback-Quelle erfolgreich gelöscht", - "connector_duplicated_successfully": "Feedback-Quelle erfolgreich dupliziert", - "connector_name": "Quellenname", - "connector_name_hint": "So wird diese Quelle in deinem Dashboard angezeigt. Wird automatisch aus dem hochgeladenen Dateinamen übernommen – du kannst es jederzeit bearbeiten.", - "connector_status_updated_successfully": "Status der Feedback-Quelle erfolgreich aktualisiert", - "connector_updated_successfully": "Feedback-Quelle erfolgreich aktualisiert", - "connectors": "Feedback-Quellen", "create_mapping": "Zuordnung erstellen", "created_by": "Erstellt von", "csv_advanced": "Erweitert", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Diese Spalten werden von keinem Feedback-Datensatz-Feld verwendet. Sie werden beim Import ignoriert.", "custom_source_type": "Benutzerdefinierter Quelltyp", "custom_source_type_placeholder": "Geben Sie den benutzerdefinierten Quelltyp ein", - "default_connector_name_csv": "CSV-Import", - "default_connector_name_formbricks": "Formbricks-Umfragequelle", + "default_source_name_csv": "CSV-Import", + "default_source_name_formbricks": "Formbricks-Umfragequelle", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Feedback-Quelle bearbeiten", "enter_name_for_source": "Gib einen Namen für diese Quelle ein", "enum": "Aufzählung", - "error_connector_field_mapping_duplicate": "Doppelte Feldzuordnung für diese Quelle", - "error_connector_formbricks_mapping_duplicate": "Doppelte Fragenzuordnung für diese Quelle", - "error_connector_name_duplicate": "Eine Quelle mit diesem Namen existiert bereits", - "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", + "error_source_field_mapping_duplicate": "Doppelte Feldzuordnung für diese Quelle", + "error_source_formbricks_mapping_duplicate": "Doppelte Fragenzuordnung für diese Quelle", + "error_source_name_duplicate": "Eine Quelle mit diesem Namen existiert bereits", + "error_source_name_required": "Quellenname ist erforderlich", + "error_source_questions_required": "Wähle mindestens eine Frage aus", + "error_source_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_directory": "Feedback-Verzeichnis", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importiere Feedback-Einträge aus CSV-Dateien", "source_connect_feedback_record_mcp_description": "Sende Feedback-Datensätze über die MCP-Integration.", "source_connect_formbricks_description": "Sende Feedback-Einträge aus deinen Formbricks-Umfragen", + "source_created_successfully": "Feedback-Quelle erfolgreich erstellt", + "source_deleted_successfully": "Feedback-Quelle erfolgreich gelöscht", + "source_duplicated_successfully": "Feedback-Quelle erfolgreich dupliziert", "source_id": "Quell-ID", "source_name": "Quellenname", + "source_name_hint": "So wird diese Quelle in deinem Dashboard angezeigt. Wird automatisch aus dem hochgeladenen Dateinamen übernommen – du kannst es jederzeit bearbeiten.", + "source_status_updated_successfully": "Status der Feedback-Quelle erfolgreich aktualisiert", "source_type": "Quellentyp", "source_type_cannot_be_changed": "Quellentyp kann nicht geändert werden", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Feedback-Quelle erfolgreich aktualisiert", + "sources": "Feedback-Quellen", "status_error": "Fehler", "status_live_sync": "Live-Synchronisierung", "status_ready": "Bereit", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index d4a353269af4..30ebdd5e9a58 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -2573,7 +2573,6 @@ "archive_not_allowed": "You are not allowed to archive this directory.", "are_you_sure_you_want_to_archive": "Are you sure you want to archive this directory? Workspaces will no longer have access to it.", "assign_workspaces_description": "Control which workspaces can access this feedback directory.", - "connectors_description": "Feedback sources that send records to this directory.", "create_feedback_directory": "Create feedback directory", "description": "Manage feedback directories and their workspace assignments.", "directory_archived_successfully": "Directory archived successfully", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Directory unarchived successfully", "directory_updated_successfully": "Directory updated successfully", "empty_state": "No feedback directories found. Create one to get started.", - "error_directory_has_connectors": "Cannot archive a directory that has feedback sources linked to it. Remove all feedback sources first.", + "error_directory_has_feedback_sources": "Cannot archive a directory that has feedback sources linked to it. Remove all feedback sources first.", "error_directory_name_duplicate": "A feedback directory with this name already exists.", "error_directory_name_required": "Directory name is required.", "error_directory_workspaces_invalid_org": "Some specified workspaces do not belong to this organization.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Feedback sources that send records to this directory.", "grant_access_confirm": "Grant workspace access", "grant_workspace_access_title": "Confirm workspace access grant", "grant_workspace_access_warning": "All current and future members of the workspaces below will gain read access to all feedback data routed into \"{directoryName}\", including data ingested by feedback sources connected to other linked workspaces. To revoke access later, remove the workspace from this directory; already-ingested records remain in the directory.", "nav_label": "Feedback Directories", "no_access": "You do not have permission to manage feedback directories.", - "no_connectors": "No feedback sources linked to this directory yet.", + "no_feedback_sources": "No feedback sources linked to this directory yet.", "no_unassigned_workspaces_description": "Every workspace is already linked to an active feedback directory. Remove a workspace from its current directory before assigning it here.", "no_unassigned_workspaces_title": "No unassigned workspaces available", - "pause_connectors_confirmation_description": "Pausing these feedback sources will stop new records from being added.", - "pause_connectors_confirmation_title": "Pause linked feedback sources?", + "pause_feedback_sources_confirmation_description": "Pausing these feedback sources will stop new records from being added.", + "pause_feedback_sources_confirmation_title": "Pause linked feedback sources?", "select_workspaces_placeholder": "Select workspaces...", "show_archived": "Show archived", "title": "Feedback Directories", @@ -3689,14 +3689,6 @@ "collected_at": "Collected At", "configure_import": "Configure import", "configure_mapping": "Configure Mapping", - "connector_created_successfully": "Feedback source created successfully", - "connector_deleted_successfully": "Feedback source deleted successfully", - "connector_duplicated_successfully": "Feedback source duplicated successfully", - "connector_name": "Source Name", - "connector_name_hint": "How this source appears in your dashboard. Auto-filled from the uploaded filename — edit anytime.", - "connector_status_updated_successfully": "Feedback source status updated successfully", - "connector_updated_successfully": "Feedback source updated successfully", - "connectors": "Feedback sources", "create_mapping": "Create mapping", "created_by": "Created by", "csv_advanced": "Advanced", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "These columns aren't used by any Feedback Record field. They'll be ignored at import.", "custom_source_type": "Custom source type", "custom_source_type_placeholder": "Enter custom source type", - "default_connector_name_csv": "CSV Import", - "default_connector_name_formbricks": "Formbricks Survey Source", + "default_source_name_csv": "CSV Import", + "default_source_name_formbricks": "Formbricks Survey Source", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Edit feedback source", "enter_name_for_source": "Enter a name for this source", "enum": "enum", - "error_connector_field_mapping_duplicate": "Duplicate field mapping for this source", - "error_connector_formbricks_mapping_duplicate": "Duplicate question mapping for this source", - "error_connector_name_duplicate": "A source with this name already exists", - "error_connector_name_required": "Source name is required", - "error_connector_questions_required": "Select at least one question", - "error_connector_survey_required": "Select a survey", + "error_source_field_mapping_duplicate": "Duplicate field mapping for this source", + "error_source_formbricks_mapping_duplicate": "Duplicate question mapping for this source", + "error_source_name_duplicate": "A source with this name already exists", + "error_source_name_required": "Source name is required", + "error_source_questions_required": "Select at least one question", + "error_source_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_directory": "Feedback Directory", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Import feedback records from CSV files", "source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.", "source_connect_formbricks_description": "Send feedback records from your Formbricks surveys", + "source_created_successfully": "Feedback source created successfully", + "source_deleted_successfully": "Feedback source deleted successfully", + "source_duplicated_successfully": "Feedback source duplicated successfully", "source_id": "Source ID", "source_name": "Source Name", + "source_name_hint": "How this source appears in your dashboard. Auto-filled from the uploaded filename — edit anytime.", + "source_status_updated_successfully": "Feedback source status updated successfully", "source_type": "Source Type", "source_type_cannot_be_changed": "Source type cannot be changed", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Feedback source updated successfully", + "sources": "Feedback sources", "status_error": "Error", "status_live_sync": "Live sync", "status_ready": "Ready", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index 67d474041f8f..66b4b5968ba8 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -2281,11 +2281,11 @@ "advanced_styling_field_track_bg_description": "Colorea la parte no rellenada de la barra.", "advanced_styling_field_track_height": "Altura de la pista", "advanced_styling_field_track_height_description": "Controla el grosor de la barra de progreso.", - "advanced_styling_field_upper_label_color": "Color de la etiqueta", - "advanced_styling_field_upper_label_color_description": "Colorea las etiquetas pequeñas sobre los campos de entrada y las etiquetas de escala.", - "advanced_styling_field_upper_label_size": "Tamaño de fuente de la etiqueta", - "advanced_styling_field_upper_label_size_description": "Escala las etiquetas pequeñas sobre los campos de entrada y las etiquetas de escala.", - "advanced_styling_field_upper_label_weight": "Grosor de fuente de la etiqueta", + "advanced_styling_field_upper_label_color": "Color de etiqueta", + "advanced_styling_field_upper_label_color_description": "Colorea las pequeñas etiquetas sobre los campos de entrada y las etiquetas de escala.", + "advanced_styling_field_upper_label_size": "Tamaño de fuente de etiqueta", + "advanced_styling_field_upper_label_size_description": "Escala las pequeñas etiquetas sobre los campos de entrada y las etiquetas de escala.", + "advanced_styling_field_upper_label_weight": "Grosor de fuente de etiqueta", "advanced_styling_field_upper_label_weight_description": "Hace que las etiquetas sean más ligeras o más gruesas.", "advanced_styling_section_buttons": "Botones", "advanced_styling_section_headlines": "Títulos y descripciones", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "No tienes permiso para archivar este directorio.", "are_you_sure_you_want_to_archive": "¿Estás seguro de que quieres archivar este directorio? Los espacios de trabajo ya no tendrán acceso a él.", "assign_workspaces_description": "Controla qué espacios de trabajo pueden acceder a este directorio de feedback.", - "connectors_description": "Fuentes de comentarios que envían registros a este directorio.", "create_feedback_directory": "Crear directorio de comentarios", "description": "Gestiona los directorios de feedback y sus asignaciones de espacios de trabajo.", "directory_archived_successfully": "Directorio archivado correctamente", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Directorio desarchivado correctamente", "directory_updated_successfully": "Directorio actualizado correctamente", "empty_state": "No se encontraron directorios de feedback. Crea uno para empezar.", - "error_directory_has_connectors": "No se puede archivar un directorio que tiene fuentes de comentarios vinculadas. Elimina primero todas las fuentes de comentarios.", + "error_directory_has_feedback_sources": "No se puede archivar un directorio que tiene fuentes de comentarios vinculadas. Elimina primero todas las fuentes de comentarios.", "error_directory_name_duplicate": "Ya existe un directorio de feedback con este nombre.", "error_directory_name_required": "El nombre del directorio es obligatorio.", "error_directory_workspaces_invalid_org": "Algunos de los espacios de trabajo especificados no pertenecen a esta organización.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Fuentes de comentarios que envían registros a este directorio.", "grant_access_confirm": "Conceder acceso al espacio de trabajo", "grant_workspace_access_title": "Confirmar concesión de acceso al espacio de trabajo", "grant_workspace_access_warning": "Todos los miembros actuales y futuros de los espacios de trabajo indicados a continuación obtendrán acceso de lectura a todos los datos de comentarios dirigidos a \"{directoryName}\", incluidos los datos ingeridos por fuentes de comentarios conectadas a otros espacios de trabajo vinculados. Para revocar el acceso más adelante, elimina el espacio de trabajo de este directorio; los registros ya ingeridos permanecen en el directorio.", "nav_label": "Directorios de Feedback", "no_access": "No tienes permiso para gestionar directorios de feedback.", - "no_connectors": "Aún no hay fuentes de comentarios vinculadas a este directorio.", + "no_feedback_sources": "Aún no hay fuentes de comentarios vinculadas a este directorio.", "no_unassigned_workspaces_description": "Cada espacio de trabajo ya está vinculado a un directorio de feedback activo. Elimina un espacio de trabajo de su directorio actual antes de asignarlo aquí.", "no_unassigned_workspaces_title": "No hay espacios de trabajo sin asignar disponibles", - "pause_connectors_confirmation_description": "Pausar estas fuentes de comentarios detendrá la adición de nuevos registros.", - "pause_connectors_confirmation_title": "¿Pausar las fuentes de comentarios vinculadas?", + "pause_feedback_sources_confirmation_description": "Pausar estas fuentes de comentarios detendrá la adición de nuevos registros.", + "pause_feedback_sources_confirmation_title": "¿Pausar las fuentes de comentarios vinculadas?", "select_workspaces_placeholder": "Selecciona espacios de trabajo...", "show_archived": "Mostrar archivados", "title": "Directorios de feedback", @@ -3689,14 +3689,6 @@ "collected_at": "Recopilado el", "configure_import": "Configurar importación", "configure_mapping": "Configurar asignación", - "connector_created_successfully": "Fuente de comentarios creada correctamente", - "connector_deleted_successfully": "Fuente de comentarios eliminada correctamente", - "connector_duplicated_successfully": "Fuente de comentarios duplicada correctamente", - "connector_name": "Nombre de la fuente", - "connector_name_hint": "Cómo aparece esta fuente en tu panel. Se completa automáticamente desde el nombre del archivo subido — edítalo cuando quieras.", - "connector_status_updated_successfully": "Estado de la fuente de comentarios actualizado correctamente", - "connector_updated_successfully": "Fuente de comentarios actualizada correctamente", - "connectors": "Fuentes de comentarios", "create_mapping": "Crear asignación", "created_by": "Creado por", "csv_advanced": "Avanzado", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Estas columnas no están siendo usadas por ningún campo de registro de feedback. Se ignorarán durante la importación.", "custom_source_type": "Tipo de fuente personalizado", "custom_source_type_placeholder": "Ingrese el tipo de fuente personalizado", - "default_connector_name_csv": "Importación CSV", - "default_connector_name_formbricks": "Fuente de encuestas Formbricks", + "default_source_name_csv": "Importación CSV", + "default_source_name_formbricks": "Fuente de encuestas 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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Editar fuente de comentarios", "enter_name_for_source": "Introduce un nombre para este origen", "enum": "enum", - "error_connector_field_mapping_duplicate": "Mapeo de campo duplicado para esta fuente", - "error_connector_formbricks_mapping_duplicate": "Mapeo de pregunta duplicado para esta fuente", - "error_connector_name_duplicate": "Ya existe una fuente con este nombre", - "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", + "error_source_field_mapping_duplicate": "Mapeo de campo duplicado para esta fuente", + "error_source_formbricks_mapping_duplicate": "Mapeo de pregunta duplicado para esta fuente", + "error_source_name_duplicate": "Ya existe una fuente con este nombre", + "error_source_name_required": "El nombre de origen es obligatorio", + "error_source_questions_required": "Selecciona al menos una pregunta", + "error_source_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_directory": "Directorio de feedback", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importa registros de comentarios desde archivos CSV", "source_connect_feedback_record_mcp_description": "Envía registros de feedback a través de la integración MCP.", "source_connect_formbricks_description": "Envía registros de comentarios desde tus encuestas de Formbricks", + "source_created_successfully": "Fuente de comentarios creada correctamente", + "source_deleted_successfully": "Fuente de comentarios eliminada correctamente", + "source_duplicated_successfully": "Fuente de comentarios duplicada correctamente", "source_id": "ID de fuente", "source_name": "Nombre de origen", + "source_name_hint": "Cómo aparece esta fuente en tu panel. Se completa automáticamente desde el nombre del archivo subido — edítalo cuando quieras.", + "source_status_updated_successfully": "Estado de la fuente de comentarios actualizado correctamente", "source_type": "Tipo de fuente", "source_type_cannot_be_changed": "El tipo de origen no se puede cambiar", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Fuente de comentarios actualizada correctamente", + "sources": "Fuentes de comentarios", "status_error": "Error", "status_live_sync": "Sincronización en vivo", "status_ready": "Listo", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 9da29033d80f..ad47976835db 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "Hauteur de la piste", "advanced_styling_field_track_height_description": "Contrôle l'épaisseur de la barre de progression.", "advanced_styling_field_upper_label_color": "Couleur de l'étiquette", - "advanced_styling_field_upper_label_color_description": "Colore les petits libellés au-dessus des champs de saisie et les libellés d'échelle.", + "advanced_styling_field_upper_label_color_description": "Définit la couleur des petites étiquettes au-dessus des champs de saisie et des échelles.", "advanced_styling_field_upper_label_size": "Taille de police de l'étiquette", - "advanced_styling_field_upper_label_size_description": "Ajuste la taille des petits libellés au-dessus des champs de saisie et des libellés d'échelle.", + "advanced_styling_field_upper_label_size_description": "Ajuste la taille des petites étiquettes au-dessus des champs de saisie et des échelles.", "advanced_styling_field_upper_label_weight": "Graisse de police de l'étiquette", - "advanced_styling_field_upper_label_weight_description": "Rend les libellés plus légers ou plus gras.", + "advanced_styling_field_upper_label_weight_description": "Rend les étiquettes plus fines ou plus grasses.", "advanced_styling_section_buttons": "Boutons", "advanced_styling_section_headlines": "Titres et descriptions", "advanced_styling_section_inputs": "Champs de saisie", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Vous n'êtes pas autorisé à archiver ce répertoire.", "are_you_sure_you_want_to_archive": "Es-tu sûr de vouloir archiver ce répertoire ? Les espaces de travail n'y auront plus accès.", "assign_workspaces_description": "Contrôle quels espaces de travail peuvent accéder à ce répertoire de retours.", - "connectors_description": "Sources de retours qui envoient des enregistrements vers ce répertoire.", "create_feedback_directory": "Créer un répertoire de commentaires", "description": "Gère les répertoires de retours et leurs attributions d'espaces de travail.", "directory_archived_successfully": "Répertoire archivé avec succès", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Répertoire désarchivé avec succès", "directory_updated_successfully": "Répertoire mis à jour avec succès", "empty_state": "Aucun répertoire de retours trouvé. Crée-en un pour commencer.", - "error_directory_has_connectors": "Impossible d'archiver un répertoire qui a des sources de retours liées. Supprime d'abord toutes les sources de retours.", + "error_directory_has_feedback_sources": "Impossible d'archiver un répertoire qui a des sources de retours liées. Supprime d'abord toutes les sources de retours.", "error_directory_name_duplicate": "Un répertoire de retours avec ce nom existe déjà.", "error_directory_name_required": "Le nom du répertoire est requis.", "error_directory_workspaces_invalid_org": "Certains espaces de travail spécifiés n'appartiennent pas à cette organisation.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Sources de retours qui envoient des enregistrements vers ce répertoire.", "grant_access_confirm": "Accorder l'accès à l'espace de travail", "grant_workspace_access_title": "Confirmer l'octroi d'accès à l'espace de travail", "grant_workspace_access_warning": "Tous les membres actuels et futurs des espaces de travail ci-dessous obtiendront un accès en lecture à toutes les données de retours acheminées vers « {directoryName} », y compris les données ingérées par les sources de retours connectées à d'autres espaces de travail liés. Pour révoquer l'accès ultérieurement, retire l'espace de travail de ce répertoire ; les enregistrements déjà ingérés restent dans le répertoire.", "nav_label": "Répertoires de feedback", "no_access": "Tu n'as pas la permission de gérer les répertoires de retours.", - "no_connectors": "Aucune source de retours liée à ce répertoire pour le moment.", + "no_feedback_sources": "Aucune source de retours liée à ce répertoire pour le moment.", "no_unassigned_workspaces_description": "Chaque espace de travail est déjà lié à un répertoire de commentaires actif. Retirez un espace de travail de son répertoire actuel avant de l'assigner ici.", "no_unassigned_workspaces_title": "Aucun espace de travail non assigné disponible", - "pause_connectors_confirmation_description": "Mettre en pause ces sources de retours empêchera l'ajout de nouveaux enregistrements.", - "pause_connectors_confirmation_title": "Mettre en pause les sources de retours liées ?", + "pause_feedback_sources_confirmation_description": "Mettre en pause ces sources de retours empêchera l'ajout de nouveaux enregistrements.", + "pause_feedback_sources_confirmation_title": "Mettre en pause les sources de retours liées ?", "select_workspaces_placeholder": "Sélectionner des espaces de travail...", "show_archived": "Afficher les éléments archivés", "title": "Répertoires de retours", @@ -3689,14 +3689,6 @@ "collected_at": "Collecté le", "configure_import": "Configurer l'importation", "configure_mapping": "Configurer le mappage", - "connector_created_successfully": "Source de retours créée avec succès", - "connector_deleted_successfully": "Source de retours supprimée avec succès", - "connector_duplicated_successfully": "Source de retours dupliquée avec succès", - "connector_name": "Nom de la source", - "connector_name_hint": "Comment cette source apparaît dans ton tableau de bord. Pré-rempli à partir du nom du fichier téléchargé — modifiable à tout moment.", - "connector_status_updated_successfully": "Statut de la source de retours mis à jour avec succès", - "connector_updated_successfully": "Source de retours mise à jour avec succès", - "connectors": "Sources de retours", "create_mapping": "Créer un mappage", "created_by": "Créé par", "csv_advanced": "Avancé", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Ces colonnes ne sont utilisées par aucun champ d'enregistrement de retour. Elles seront ignorées lors de l'importation.", "custom_source_type": "Type de source personnalisé", "custom_source_type_placeholder": "Entrez le type de source personnalisé", - "default_connector_name_csv": "Importation CSV", - "default_connector_name_formbricks": "Source de sondage Formbricks", + "default_source_name_csv": "Importation CSV", + "default_source_name_formbricks": "Source 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é.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Modifier la source de retours", "enter_name_for_source": "Entrez un nom pour cette source", "enum": "enum", - "error_connector_field_mapping_duplicate": "Mappage de champ en double pour cette source", - "error_connector_formbricks_mapping_duplicate": "Mappage de question en double pour cette source", - "error_connector_name_duplicate": "Une source avec ce nom existe déjà", - "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", + "error_source_field_mapping_duplicate": "Mappage de champ en double pour cette source", + "error_source_formbricks_mapping_duplicate": "Mappage de question en double pour cette source", + "error_source_name_duplicate": "Une source avec ce nom existe déjà", + "error_source_name_required": "Le nom de la source est requis", + "error_source_questions_required": "Sélectionnez au moins une question", + "error_source_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_directory": "Répertoire de retours", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importer des retours clients à partir de fichiers CSV", "source_connect_feedback_record_mcp_description": "Envoyer des enregistrements de feedback via l'intégration MCP.", "source_connect_formbricks_description": "Envoyer des retours clients depuis vos sondages Formbricks", + "source_created_successfully": "Source de retours créée avec succès", + "source_deleted_successfully": "Source de retours supprimée avec succès", + "source_duplicated_successfully": "Source de retours dupliquée avec succès", "source_id": "Identifiant de la source", "source_name": "Nom de la source", + "source_name_hint": "Comment cette source apparaît dans ton tableau de bord. Pré-rempli à partir du nom du fichier téléchargé — modifiable à tout moment.", + "source_status_updated_successfully": "Statut de la source de retours mis à jour avec succès", "source_type": "Type de source", "source_type_cannot_be_changed": "Le type de source ne peut pas être modifié", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Source de retours mise à jour avec succès", + "sources": "Sources de retours", "status_error": "Erreur", "status_live_sync": "Synchronisation en direct", "status_ready": "Prêt", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index f4f12bfd3544..72e9011088c5 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "Követés magassága", "advanced_styling_field_track_height_description": "A folyamatjelző vastagságát vezérli.", "advanced_styling_field_upper_label_color": "Címke színe", - "advanced_styling_field_upper_label_color_description": "Kiszínezi a beviteli mezők fölötti kis címkéket és a skálacímkéket.", + "advanced_styling_field_upper_label_color_description": "A beviteli mezők feletti kis címkék és a skálacímkék színét állítja be.", "advanced_styling_field_upper_label_size": "Címke betűmérete", - "advanced_styling_field_upper_label_size_description": "Átméretezi a beviteli mezők fölötti kis címkéket és a skálacímkéket.", + "advanced_styling_field_upper_label_size_description": "A beviteli mezők feletti kis címkék és a skálacímkék méretét módosítja.", "advanced_styling_field_upper_label_weight": "Címke betűvastagsága", - "advanced_styling_field_upper_label_weight_description": "Vékonyabbá vagy vastagabbá teszi a címkéket.", + "advanced_styling_field_upper_label_weight_description": "A címkék betűtípusát vékonyabbá vagy vastagabbá teszi.", "advanced_styling_section_buttons": "Gombok", "advanced_styling_section_headlines": "Címsorok és leírások", "advanced_styling_section_inputs": "Beviteli mezők", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Nem rendelkezik jogosultsággal ezen könyvtár archiválásához.", "are_you_sure_you_want_to_archive": "Biztosan archiválni kívánja ezt a könyvtárat? A munkaterületek többé nem férhetnek hozzá.", "assign_workspaces_description": "Szabályozza, mely munkaterületek férhetnek hozzá ehhez a visszajelzési könyvtárhoz.", - "connectors_description": "Visszajelzési források, amelyek rekordokat küldenek ebbe a könyvtárba.", "create_feedback_directory": "Visszajelzési könyvtár létrehozása", "description": "Visszajelzési könyvtárak és munkaterület-hozzárendeléseik kezelése.", "directory_archived_successfully": "A könyvtár sikeresen archiválva", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "A könyvtár archiválása sikeresen visszavonva", "directory_updated_successfully": "A könyvtár sikeresen frissítve", "empty_state": "Nem találhatók visszajelzési könyvtárak. Hozzon létre egyet a kezdéshez.", - "error_directory_has_connectors": "Nem archiválható olyan könyvtár, amelyhez visszajelzési források vannak kapcsolva. Először távolítson el minden visszajelzési forrást.", + "error_directory_has_feedback_sources": "Nem archiválható olyan könyvtár, amelyhez visszajelzési források vannak kapcsolva. Először távolítson el minden visszajelzési forrást.", "error_directory_name_duplicate": "Már létezik visszajelzési könyvtár ezzel a névvel.", "error_directory_name_required": "A könyvtár neve kötelező megadni.", "error_directory_workspaces_invalid_org": "Egyes megadott munkaterületek nem ehhez a szervezethez tartoznak.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Visszajelzési források, amelyek rekordokat küldenek ebbe a könyvtárba.", "grant_access_confirm": "Munkaterület-hozzáférés megadása", "grant_workspace_access_title": "Munkaterület-hozzáférés megadásának megerősítése", "grant_workspace_access_warning": "Az alábbi munkaterületek összes jelenlegi és jövőbeli tagja olvasási hozzáférést kap minden visszajelzési adathoz, amely a \"{directoryName}\" könyvtárba kerül, beleértve a más kapcsolódó munkaterületekhez csatlakoztatott visszajelzési források által betöltött adatokat is. A hozzáférés később történő visszavonásához távolítsa el a munkaterületet ebből a könyvtárból; a már betöltött rekordok a könyvtárban maradnak.", "nav_label": "Visszajelzési könyvtárak", "no_access": "Önnek nincs jogosultsága a visszajelzési könyvtárak kezeléséhez.", - "no_connectors": "Még nincsenek visszajelzési források kapcsolva ehhez a könyvtárhoz.", + "no_feedback_sources": "Még nincsenek visszajelzési források kapcsolva ehhez a könyvtárhoz.", "no_unassigned_workspaces_description": "Minden munkaterület már hozzá van rendelve egy aktív visszajelzési könyvtárhoz. Távolítson el egy munkaterületet a jelenlegi könyvtárából, mielőtt ide rendelné.", "no_unassigned_workspaces_title": "Nincsenek hozzá nem rendelt munkaterületek", - "pause_connectors_confirmation_description": "Ezen visszajelzési források szüneteltetése megállítja az új rekordok hozzáadását.", - "pause_connectors_confirmation_title": "Szünetelteti a kapcsolódó visszajelzési forrásokat?", + "pause_feedback_sources_confirmation_description": "Ezen visszajelzési források szüneteltetése megállítja az új rekordok hozzáadását.", + "pause_feedback_sources_confirmation_title": "Szünetelteti a kapcsolódó visszajelzési forrásokat?", "select_workspaces_placeholder": "Munkaterületek kiválasztása...", "show_archived": "Archivált elemek megjelenítése", "title": "Visszajelzési könyvtárak", @@ -3689,14 +3689,6 @@ "collected_at": "Gyűjtve", "configure_import": "Importálás konfigurálása", "configure_mapping": "Leképezés konfigurálása", - "connector_created_successfully": "Visszajelzési forrás sikeresen létrehozva", - "connector_deleted_successfully": "Visszajelzési forrás sikeresen törölve", - "connector_duplicated_successfully": "Visszajelzési forrás sikeresen duplikálva", - "connector_name": "Forrás neve", - "connector_name_hint": "Így jelenik meg ez a forrás az Ön irányítópultján. Automatikusan kitöltődik a feltöltött fájlnévből — bármikor szerkeszthető.", - "connector_status_updated_successfully": "Visszajelzési forrás állapota sikeresen frissítve", - "connector_updated_successfully": "Visszajelzési forrás sikeresen frissítve", - "connectors": "Visszajelzési források", "create_mapping": "Leképezés létrehozása", "created_by": "Létrehozta", "csv_advanced": "Speciális", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Ezeket az oszlopokat egyetlen visszajelzési rekord mező sem használja. Az importáláskor figyelmen kívül maradnak.", "custom_source_type": "Egyéni forrástípus", "custom_source_type_placeholder": "Adja meg az egyéni forrástípust", - "default_connector_name_csv": "CSV importálás", - "default_connector_name_formbricks": "Formbricks felmérési forrás", + "default_source_name_csv": "CSV importálás", + "default_source_name_formbricks": "Formbricks felmérési forrás", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Visszajelzési forrás szerkesztése", "enter_name_for_source": "Adj nevet ennek a forrásnak", "enum": "felsorolás", - "error_connector_field_mapping_duplicate": "Duplikált mezőleképezés ehhez a forráshoz", - "error_connector_formbricks_mapping_duplicate": "Duplikált kérdésleképezés ehhez a forráshoz", - "error_connector_name_duplicate": "Már létezik forrás ezzel a névvel", - "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", + "error_source_field_mapping_duplicate": "Duplikált mezőleképezés ehhez a forráshoz", + "error_source_formbricks_mapping_duplicate": "Duplikált kérdésleképezés ehhez a forráshoz", + "error_source_name_duplicate": "Már létezik forrás ezzel a névvel", + "error_source_name_required": "A forrás neve kötelező", + "error_source_questions_required": "Válasszon ki legalább egy kérdést", + "error_source_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_directory": "Visszajelzési könyvtár", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Visszajelzési bejegyzések importálása CSV fájlokból", "source_connect_feedback_record_mcp_description": "Visszajelzési rekordok küldése az MCP integráción keresztül.", "source_connect_formbricks_description": "Visszajelzési bejegyzések küldése a Formbricks felméréseiből", + "source_created_successfully": "Visszajelzési forrás sikeresen létrehozva", + "source_deleted_successfully": "Visszajelzési forrás sikeresen törölve", + "source_duplicated_successfully": "Visszajelzési forrás sikeresen duplikálva", "source_id": "Forrásazonosító", "source_name": "Forrásnév", + "source_name_hint": "Így jelenik meg ez a forrás az Ön irányítópultján. Automatikusan kitöltődik a feltöltött fájlnévből — bármikor szerkeszthető.", + "source_status_updated_successfully": "Visszajelzési forrás állapota sikeresen frissítve", "source_type": "Forrás típus", "source_type_cannot_be_changed": "A forrástípus nem módosítható", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Visszajelzési forrás sikeresen frissítve", + "sources": "Visszajelzési források", "status_error": "Hiba", "status_live_sync": "Élő szinkronizálás", "status_ready": "Kész", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index d4ab61840b28..63d567b345f5 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "トラックの高さ", "advanced_styling_field_track_height_description": "プログレスバーの太さを調整します。", "advanced_styling_field_upper_label_color": "ラベルの色", - "advanced_styling_field_upper_label_color_description": "入力フィールド上部の小さなラベルとスケールラベルの色を設定します。", + "advanced_styling_field_upper_label_color_description": "入力欄やスケールラベルの上部にある小さなラベルの色を設定します。", "advanced_styling_field_upper_label_size": "ラベルのフォントサイズ", - "advanced_styling_field_upper_label_size_description": "入力フィールド上部の小さなラベルとスケールラベルのサイズを調整します。", - "advanced_styling_field_upper_label_weight": "ラベルのフォントの太さ", - "advanced_styling_field_upper_label_weight_description": "ラベルを細くまたは太くします。", + "advanced_styling_field_upper_label_size_description": "入力欄やスケールラベルの上部にある小さなラベルのサイズを調整します。", + "advanced_styling_field_upper_label_weight": "ラベルのフォントウェイト", + "advanced_styling_field_upper_label_weight_description": "ラベルの太さを細くしたり太くしたりします。", "advanced_styling_section_buttons": "ボタン", "advanced_styling_section_headlines": "見出しと説明", "advanced_styling_section_inputs": "入力フィールド", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "このディレクトリをアーカイブする権限がありません。", "are_you_sure_you_want_to_archive": "このディレクトリをアーカイブしてもよろしいですか?ワークスペースはアクセスできなくなります。", "assign_workspaces_description": "このフィードバックディレクトリにアクセスできるワークスペースを管理します。", - "connectors_description": "このディレクトリにレコードを送信するフィードバックソース。", "create_feedback_directory": "フィードバックディレクトリを作成", "description": "フィードバックディレクトリとワークスペースへの割り当てを管理します。", "directory_archived_successfully": "ディレクトリをアーカイブしました", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "ディレクトリのアーカイブを解除しました", "directory_updated_successfully": "ディレクトリを更新しました", "empty_state": "フィードバックディレクトリが見つかりません。作成して始めましょう。", - "error_directory_has_connectors": "フィードバックソースがリンクされているディレクトリをアーカイブすることはできません。先にすべてのフィードバックソースを削除してください。", + "error_directory_has_feedback_sources": "フィードバックソースがリンクされているディレクトリをアーカイブすることはできません。先にすべてのフィードバックソースを削除してください。", "error_directory_name_duplicate": "この名前のフィードバックディレクトリはすでに存在します。", "error_directory_name_required": "ディレクトリ名は必須です。", "error_directory_workspaces_invalid_org": "指定されたワークスペースの一部がこの組織に属していません。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "このディレクトリにレコードを送信するフィードバックソース。", "grant_access_confirm": "ワークスペースのアクセス権を付与", "grant_workspace_access_title": "ワークスペースのアクセス権付与を確認", "grant_workspace_access_warning": "以下のワークスペースの現在および将来のメンバー全員が、他のリンクされたワークスペースに接続されたフィードバックソースによって取り込まれたデータを含め、「{directoryName}」にルーティングされるすべてのフィードバックデータへの読み取りアクセスを取得します。後でアクセスを取り消すには、このディレクトリからワークスペースを削除してください。すでに取り込まれたレコードはディレクトリに残ります。", "nav_label": "フィードバックディレクトリ", "no_access": "フィードバックディレクトリを管理する権限がありません。", - "no_connectors": "このディレクトリにリンクされたフィードバックソースがまだありません。", + "no_feedback_sources": "このディレクトリにリンクされたフィードバックソースがまだありません。", "no_unassigned_workspaces_description": "すべてのワークスペースは既にアクティブなフィードバックディレクトリにリンクされています。ここに割り当てる前に、現在のディレクトリからワークスペースを削除してください。", "no_unassigned_workspaces_title": "未割り当てのワークスペースがありません", - "pause_connectors_confirmation_description": "これらのフィードバックソースを一時停止すると、新しいレコードの追加が停止されます。", - "pause_connectors_confirmation_title": "リンクされたフィードバックソースを一時停止しますか?", + "pause_feedback_sources_confirmation_description": "これらのフィードバックソースを一時停止すると、新しいレコードの追加が停止されます。", + "pause_feedback_sources_confirmation_title": "リンクされたフィードバックソースを一時停止しますか?", "select_workspaces_placeholder": "ワークスペースを選択...", "show_archived": "アーカイブ済みを表示", "title": "フィードバックディレクトリ", @@ -3689,14 +3689,6 @@ "collected_at": "収集日時", "configure_import": "インポートを設定", "configure_mapping": "マッピングを設定", - "connector_created_successfully": "フィードバックソースが正常に作成されました", - "connector_deleted_successfully": "フィードバックソースが正常に削除されました", - "connector_duplicated_successfully": "フィードバックソースが正常に複製されました", - "connector_name": "ソース名", - "connector_name_hint": "ダッシュボードに表示されるこのソースの名前です。アップロードされたファイル名から自動入力されますが、いつでも編集できます。", - "connector_status_updated_successfully": "フィードバックソースのステータスが正常に更新されました", - "connector_updated_successfully": "フィードバックソースが正常に更新されました", - "connectors": "フィードバックソース", "create_mapping": "マッピングを作成", "created_by": "作成者", "csv_advanced": "詳細設定", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "これらの列はフィードバックレコードのどのフィールドにも使用されていません。インポート時に無視されます。", "custom_source_type": "カスタムソースタイプ", "custom_source_type_placeholder": "カスタムソースタイプを入力してください", - "default_connector_name_csv": "CSVインポート", - "default_connector_name_formbricks": "Formbricksアンケートソース", + "default_source_name_csv": "CSVインポート", + "default_source_name_formbricks": "Formbricksアンケートソース", "delete_feedback_record": "フィードバック記録を削除", "delete_feedback_record_confirmation": "この操作により、フィードバック記録が完全に削除され、接続されているディレクトリから削除されます。", "delete_feedback_records_confirmation": "この操作により、{count}件のフィードバック記録が完全に削除され、接続されているディレクトリから削除されます。", @@ -3746,12 +3738,12 @@ "edit_source_connection": "フィードバックソースを編集", "enter_name_for_source": "このソースの名前を入力", "enum": "列挙型", - "error_connector_field_mapping_duplicate": "このソースのフィールドマッピングが重複しています", - "error_connector_formbricks_mapping_duplicate": "このソースの質問マッピングが重複しています", - "error_connector_name_duplicate": "この名前のソースは既に存在します", - "error_connector_name_required": "ソース名は必須です", - "error_connector_questions_required": "少なくとも1つの質問を選択してください", - "error_connector_survey_required": "アンケートを選択してください", + "error_source_field_mapping_duplicate": "このソースのフィールドマッピングが重複しています", + "error_source_formbricks_mapping_duplicate": "このソースの質問マッピングが重複しています", + "error_source_name_duplicate": "この名前のソースは既に存在します", + "error_source_name_required": "ソース名は必須です", + "error_source_questions_required": "少なくとも1つの質問を選択してください", + "error_source_survey_required": "アンケートを選択してください", "failed_to_delete_feedback_records": "フィードバックレコードの削除に失敗しました", "failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました", "feedback_directory": "フィードバックディレクトリ", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "CSVファイルからフィードバック記録をインポート", "source_connect_feedback_record_mcp_description": "MCP統合を通じてフィードバックレコードを送信します。", "source_connect_formbricks_description": "Formbricksアンケートからフィードバック記録を送信", + "source_created_successfully": "フィードバックソースが正常に作成されました", + "source_deleted_successfully": "フィードバックソースが正常に削除されました", + "source_duplicated_successfully": "フィードバックソースが正常に複製されました", "source_id": "ソースID", "source_name": "ソース名", + "source_name_hint": "ダッシュボードに表示されるこのソースの名前です。アップロードされたファイル名から自動入力されますが、いつでも編集できます。", + "source_status_updated_successfully": "フィードバックソースのステータスが正常に更新されました", "source_type": "ソースタイプ", "source_type_cannot_be_changed": "ソースタイプは変更できません", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "フィードバックソースが正常に更新されました", + "sources": "フィードバックソース", "status_error": "エラー", "status_live_sync": "リアルタイム同期", "status_ready": "準備完了", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index f0134ffb1c54..3bd1a0b4b2c5 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Je hebt geen toestemming om deze map te archiveren.", "are_you_sure_you_want_to_archive": "Weet je zeker dat je deze map wilt archiveren? Workspaces hebben er dan geen toegang meer toe.", "assign_workspaces_description": "Bepaal welke workspaces toegang hebben tot deze feedbackmap.", - "connectors_description": "Feedbackbronnen die gegevens naar deze directory sturen.", "create_feedback_directory": "Feedbackmap maken", "description": "Beheer feedbackmappen en hun workspacetoewijzingen.", "directory_archived_successfully": "Map succesvol gearchiveerd", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Map succesvol gedearchiveerd", "directory_updated_successfully": "Map succesvol bijgewerkt", "empty_state": "Geen feedbackmappen gevonden. Maak er een aan om te beginnen.", - "error_directory_has_connectors": "Je kunt geen directory archiveren die gekoppelde feedbackbronnen heeft. Verwijder eerst alle feedbackbronnen.", + "error_directory_has_feedback_sources": "Je kunt geen directory archiveren die gekoppelde feedbackbronnen heeft. Verwijder eerst alle feedbackbronnen.", "error_directory_name_duplicate": "Er bestaat al een feedbackmap met deze naam.", "error_directory_name_required": "Mapnaam is verplicht.", "error_directory_workspaces_invalid_org": "Sommige opgegeven werkruimtes behoren niet tot deze organisatie.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Feedbackbronnen die gegevens naar deze directory sturen.", "grant_access_confirm": "Werkruimtetoegang verlenen", "grant_workspace_access_title": "Bevestig verlenen van werkruimtetoegang", "grant_workspace_access_warning": "Alle huidige en toekomstige leden van onderstaande werkruimtes krijgen leestoegang tot alle feedbackgegevens die naar \"{directoryName}\" worden gerouteerd, inclusief gegevens die zijn verzameld door feedbackbronnen die verbonden zijn met andere gekoppelde werkruimtes. Om de toegang later in te trekken, verwijder je de werkruimte uit deze directory; reeds verzamelde gegevens blijven in de directory.", "nav_label": "Feedbackmappen", "no_access": "Je hebt geen toestemming om feedbackmappen te beheren.", - "no_connectors": "Nog geen feedbackbronnen gekoppeld aan deze directory.", + "no_feedback_sources": "Nog geen feedbackbronnen gekoppeld aan deze directory.", "no_unassigned_workspaces_description": "Elke workspace is al gekoppeld aan een actieve feedbackdirectory. Verwijder een workspace uit de huidige directory voordat je deze hier toewijst.", "no_unassigned_workspaces_title": "Geen niet-toegewezen workspaces beschikbaar", - "pause_connectors_confirmation_description": "Het pauzeren van deze feedbackbronnen stopt het toevoegen van nieuwe gegevens.", - "pause_connectors_confirmation_title": "Gekoppelde feedbackbronnen pauzeren?", + "pause_feedback_sources_confirmation_description": "Het pauzeren van deze feedbackbronnen stopt het toevoegen van nieuwe gegevens.", + "pause_feedback_sources_confirmation_title": "Gekoppelde feedbackbronnen pauzeren?", "select_workspaces_placeholder": "Selecteer werkruimtes...", "show_archived": "Gearchiveerde weergeven", "title": "Feedbackmappen", @@ -3689,14 +3689,6 @@ "collected_at": "Verzameld op", "configure_import": "Import configureren", "configure_mapping": "Koppeling configureren", - "connector_created_successfully": "Feedbackbron succesvol aangemaakt", - "connector_deleted_successfully": "Feedbackbron succesvol verwijderd", - "connector_duplicated_successfully": "Feedbackbron succesvol gedupliceerd", - "connector_name": "Bronnaam", - "connector_name_hint": "Hoe deze bron in je dashboard verschijnt. Automatisch ingevuld vanuit de geüploade bestandsnaam — pas aan wanneer je wilt.", - "connector_status_updated_successfully": "Status van feedbackbron succesvol bijgewerkt", - "connector_updated_successfully": "Feedbackbron succesvol bijgewerkt", - "connectors": "Feedbackbronnen", "create_mapping": "Koppeling aanmaken", "created_by": "Gemaakt door", "csv_advanced": "Geavanceerd", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Deze kolommen worden niet gebruikt door een Feedback Record-veld. Ze worden genegeerd bij het importeren.", "custom_source_type": "Aangepast brontype", "custom_source_type_placeholder": "Voer een aangepast brontype in", - "default_connector_name_csv": "CSV import", - "default_connector_name_formbricks": "Formbricks Enquêtebron", + "default_source_name_csv": "CSV import", + "default_source_name_formbricks": "Formbricks Enquêtebron", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Feedbackbron bewerken", "enter_name_for_source": "Voer een naam in voor deze bron", "enum": "enum", - "error_connector_field_mapping_duplicate": "Dubbele veldkoppeling voor deze bron", - "error_connector_formbricks_mapping_duplicate": "Dubbele vraagkoppeling voor deze bron", - "error_connector_name_duplicate": "Er bestaat al een bron met deze naam", - "error_connector_name_required": "Bronnaam is verplicht", - "error_connector_questions_required": "Selecteer minimaal één vraag", - "error_connector_survey_required": "Selecteer een enquête", + "error_source_field_mapping_duplicate": "Dubbele veldkoppeling voor deze bron", + "error_source_formbricks_mapping_duplicate": "Dubbele vraagkoppeling voor deze bron", + "error_source_name_duplicate": "Er bestaat al een bron met deze naam", + "error_source_name_required": "Bronnaam is verplicht", + "error_source_questions_required": "Selecteer minimaal één vraag", + "error_source_survey_required": "Selecteer een enquête", "failed_to_delete_feedback_records": "Feedbackgegevens verwijderen mislukt", "failed_to_load_feedback_records": "Kan feedbackrecords niet laden", "feedback_directory": "Feedbackmap", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importeer feedbackgegevens uit CSV-bestanden", "source_connect_feedback_record_mcp_description": "Verstuur feedbackrecords via de MCP-integratie.", "source_connect_formbricks_description": "Stuur feedbackgegevens van je Formbricks-enquêtes", + "source_created_successfully": "Feedbackbron succesvol aangemaakt", + "source_deleted_successfully": "Feedbackbron succesvol verwijderd", + "source_duplicated_successfully": "Feedbackbron succesvol gedupliceerd", "source_id": "Bron-ID", "source_name": "Bronnaam", + "source_name_hint": "Hoe deze bron in je dashboard verschijnt. Automatisch ingevuld vanuit de geüploade bestandsnaam — pas aan wanneer je wilt.", + "source_status_updated_successfully": "Status van feedbackbron succesvol bijgewerkt", "source_type": "Brontype", "source_type_cannot_be_changed": "Brontype kan niet worden gewijzigd", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Feedbackbron succesvol bijgewerkt", + "sources": "Feedbackbronnen", "status_error": "Fout", "status_live_sync": "Live synchronisatie", "status_ready": "Klaar", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 439fd05eff7f..7c9747c9de43 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -2281,12 +2281,12 @@ "advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.", "advanced_styling_field_track_height": "Altura da trilha", "advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.", - "advanced_styling_field_upper_label_color": "Cor do rótulo", - "advanced_styling_field_upper_label_color_description": "Colore os pequenos rótulos acima dos campos de entrada e os rótulos de escala.", - "advanced_styling_field_upper_label_size": "Tamanho da fonte do rótulo", - "advanced_styling_field_upper_label_size_description": "Ajusta o tamanho dos pequenos rótulos acima dos campos de entrada e dos rótulos de escala.", - "advanced_styling_field_upper_label_weight": "Peso da fonte do rótulo", - "advanced_styling_field_upper_label_weight_description": "Torna os rótulos mais leves ou mais negritos.", + "advanced_styling_field_upper_label_color": "Cor do Rótulo", + "advanced_styling_field_upper_label_color_description": "Colore os pequenos rótulos acima dos campos de entrada e rótulos de escala.", + "advanced_styling_field_upper_label_size": "Tamanho da Fonte do Rótulo", + "advanced_styling_field_upper_label_size_description": "Ajusta o tamanho dos pequenos rótulos acima dos campos de entrada e rótulos de escala.", + "advanced_styling_field_upper_label_weight": "Peso da Fonte do Rótulo", + "advanced_styling_field_upper_label_weight_description": "Torna os rótulos mais leves ou mais grossos.", "advanced_styling_section_buttons": "Botões", "advanced_styling_section_headlines": "Títulos e descrições", "advanced_styling_section_inputs": "Campos de entrada", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Você não tem permissão para arquivar este diretório.", "are_you_sure_you_want_to_archive": "Tem certeza de que deseja arquivar este diretório? Os espaços de trabalho não terão mais acesso a ele.", "assign_workspaces_description": "Controle quais workspaces podem acessar este diretório de feedback.", - "connectors_description": "Fontes de feedback que enviam registros para este diretório.", "create_feedback_directory": "Criar diretório de feedback", "description": "Gerencie diretórios de feedback e suas atribuições de workspace.", "directory_archived_successfully": "Diretório arquivado com sucesso", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Diretório desarquivado com sucesso", "directory_updated_successfully": "Diretório atualizado com sucesso", "empty_state": "Nenhum diretório de feedback encontrado. Crie um para começar.", - "error_directory_has_connectors": "Não é possível arquivar um diretório que tem fontes de feedback vinculadas a ele. Remova todas as fontes de feedback primeiro.", + "error_directory_has_feedback_sources": "Não é possível arquivar um diretório que tem fontes de feedback vinculadas a ele. Remova todas as fontes de feedback primeiro.", "error_directory_name_duplicate": "Já existe um diretório de feedback com este nome.", "error_directory_name_required": "O nome do diretório é obrigatório.", "error_directory_workspaces_invalid_org": "Alguns espaços de trabalho especificados não pertencem a esta organização.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Fontes de feedback que enviam registros para este diretório.", "grant_access_confirm": "Conceder acesso ao workspace", "grant_workspace_access_title": "Confirmar concessão de acesso ao workspace", "grant_workspace_access_warning": "Todos os membros atuais e futuros dos espaços de trabalho abaixo terão acesso de leitura a todos os dados de feedback direcionados para \"{directoryName}\", incluindo dados coletados por fontes de feedback conectadas a outros espaços vinculados. Para revogar o acesso depois, remova o espaço de trabalho deste diretório; registros já coletados permanecerão no diretório.", "nav_label": "Diretórios de Feedback", "no_access": "Você não tem permissão para gerenciar diretórios de feedback.", - "no_connectors": "Nenhuma fonte de feedback vinculada a este diretório ainda.", + "no_feedback_sources": "Nenhuma fonte de feedback vinculada a este diretório ainda.", "no_unassigned_workspaces_description": "Todos os workspaces já estão vinculados a um diretório de feedback ativo. Remova um workspace do seu diretório atual antes de atribuí-lo aqui.", "no_unassigned_workspaces_title": "Nenhum workspace não atribuído disponível", - "pause_connectors_confirmation_description": "Pausar essas fontes de feedback impedirá que novos registros sejam adicionados.", - "pause_connectors_confirmation_title": "Pausar fontes de feedback vinculadas?", + "pause_feedback_sources_confirmation_description": "Pausar essas fontes de feedback impedirá que novos registros sejam adicionados.", + "pause_feedback_sources_confirmation_title": "Pausar fontes de feedback vinculadas?", "select_workspaces_placeholder": "Selecionar espaços de trabalho...", "show_archived": "Mostrar arquivados", "title": "Diretórios de Feedback", @@ -3689,14 +3689,6 @@ "collected_at": "Coletado em", "configure_import": "Configurar importação", "configure_mapping": "Configurar mapeamento", - "connector_created_successfully": "Fonte de feedback criada com sucesso", - "connector_deleted_successfully": "Fonte de feedback excluída com sucesso", - "connector_duplicated_successfully": "Fonte de feedback duplicada com sucesso", - "connector_name": "Nome da Fonte", - "connector_name_hint": "Como esta fonte aparece no seu painel. Preenchido automaticamente a partir do nome do arquivo enviado — edite quando quiser.", - "connector_status_updated_successfully": "Status da fonte de feedback atualizado com sucesso", - "connector_updated_successfully": "Fonte de feedback atualizada com sucesso", - "connectors": "Fontes de feedback", "create_mapping": "Criar mapeamento", "created_by": "Criado por", "csv_advanced": "Avançado", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Essas colunas não são usadas por nenhum campo de Registro de Feedback. Elas serão ignoradas na importação.", "custom_source_type": "Tipo de origem personalizado", "custom_source_type_placeholder": "Insira o tipo de fonte personalizado", - "default_connector_name_csv": "Importação CSV", - "default_connector_name_formbricks": "Fonte de Pesquisa Formbricks", + "default_source_name_csv": "Importação CSV", + "default_source_name_formbricks": "Fonte 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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Editar fonte de feedback", "enter_name_for_source": "Digite um nome para esta origem", "enum": "enum", - "error_connector_field_mapping_duplicate": "Mapeamento de campo duplicado para esta origem", - "error_connector_formbricks_mapping_duplicate": "Mapeamento de pergunta duplicado para esta origem", - "error_connector_name_duplicate": "Uma fonte com este nome já existe", - "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", + "error_source_field_mapping_duplicate": "Mapeamento de campo duplicado para esta origem", + "error_source_formbricks_mapping_duplicate": "Mapeamento de pergunta duplicado para esta origem", + "error_source_name_duplicate": "Uma fonte com este nome já existe", + "error_source_name_required": "O nome da fonte é obrigatório", + "error_source_questions_required": "Selecione pelo menos uma pergunta", + "error_source_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_directory": "Diretório de Feedback", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importe registros de feedback a partir de arquivos CSV", "source_connect_feedback_record_mcp_description": "Envie registros de feedback através da integração MCP.", "source_connect_formbricks_description": "Envie registros de feedback das suas pesquisas do Formbricks", + "source_created_successfully": "Fonte de feedback criada com sucesso", + "source_deleted_successfully": "Fonte de feedback excluída com sucesso", + "source_duplicated_successfully": "Fonte de feedback duplicada com sucesso", "source_id": "ID da fonte", "source_name": "Nome da origem", + "source_name_hint": "Como esta fonte aparece no seu painel. Preenchido automaticamente a partir do nome do arquivo enviado — edite quando quiser.", + "source_status_updated_successfully": "Status da fonte de feedback atualizado com sucesso", "source_type": "Tipo de fonte", "source_type_cannot_be_changed": "O tipo de origem não pode ser alterado", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Fonte de feedback atualizada com sucesso", + "sources": "Fontes de feedback", "status_error": "Erro", "status_live_sync": "Sincronização ao vivo", "status_ready": "Pronto", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 253d9e145b71..3a66cf7e1138 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -2281,12 +2281,12 @@ "advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.", "advanced_styling_field_track_height": "Altura da faixa", "advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.", - "advanced_styling_field_upper_label_color": "Cor da etiqueta", - "advanced_styling_field_upper_label_color_description": "Colore as pequenas etiquetas acima dos campos de entrada e as etiquetas de escala.", - "advanced_styling_field_upper_label_size": "Tamanho da fonte da etiqueta", + "advanced_styling_field_upper_label_color": "Cor da Etiqueta", + "advanced_styling_field_upper_label_color_description": "Define a cor das pequenas etiquetas acima dos campos de entrada e das etiquetas de escala.", + "advanced_styling_field_upper_label_size": "Tamanho da Letra da Etiqueta", "advanced_styling_field_upper_label_size_description": "Ajusta o tamanho das pequenas etiquetas acima dos campos de entrada e das etiquetas de escala.", - "advanced_styling_field_upper_label_weight": "Peso da fonte da etiqueta", - "advanced_styling_field_upper_label_weight_description": "Torna as etiquetas mais leves ou mais negritas.", + "advanced_styling_field_upper_label_weight": "Espessura da Letra da Etiqueta", + "advanced_styling_field_upper_label_weight_description": "Torna as etiquetas mais leves ou mais pesadas.", "advanced_styling_section_buttons": "Botões", "advanced_styling_section_headlines": "Títulos e descrições", "advanced_styling_section_inputs": "Campos de entrada", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Não tens permissão para arquivar este diretório.", "are_you_sure_you_want_to_archive": "Tens a certeza de que queres arquivar este diretório? Os espaços de trabalho deixarão de ter acesso ao mesmo.", "assign_workspaces_description": "Controla quais workspaces podem aceder a este diretório de feedback.", - "connectors_description": "Fontes de feedback que enviam registos para este diretório.", "create_feedback_directory": "Criar diretório de feedback", "description": "Gere diretórios de feedback e as suas atribuições de workspace.", "directory_archived_successfully": "Diretório arquivado com sucesso", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Diretório desarquivado com sucesso", "directory_updated_successfully": "Diretório atualizado com sucesso", "empty_state": "Nenhum diretório de feedback encontrado. Cria um para começar.", - "error_directory_has_connectors": "Não é possível arquivar um diretório que tem fontes de feedback associadas. Remove primeiro todas as fontes de feedback.", + "error_directory_has_feedback_sources": "Não é possível arquivar um diretório que tem fontes de feedback associadas. Remove primeiro todas as fontes de feedback.", "error_directory_name_duplicate": "Já existe um diretório de feedback com este nome.", "error_directory_name_required": "O nome do diretório é obrigatório.", "error_directory_workspaces_invalid_org": "Algumas áreas de trabalho especificadas não pertencem a esta organização.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Fontes de feedback que enviam registos para este diretório.", "grant_access_confirm": "Conceder acesso ao workspace", "grant_workspace_access_title": "Confirmar concessão de acesso ao workspace", "grant_workspace_access_warning": "Todos os membros atuais e futuros dos espaços de trabalho abaixo irão obter acesso de leitura a todos os dados de feedback direcionados para \"{directoryName}\", incluindo dados ingeridos por fontes de feedback ligadas a outros espaços de trabalho associados. Para revogar o acesso mais tarde, remove o espaço de trabalho deste diretório; os registos já ingeridos permanecem no diretório.", "nav_label": "Diretórios de Feedback", "no_access": "Não tens permissão para gerir diretórios de feedback.", - "no_connectors": "Ainda sem fontes de feedback associadas a este diretório.", + "no_feedback_sources": "Ainda sem fontes de feedback associadas a este diretório.", "no_unassigned_workspaces_description": "Todos os espaços de trabalho já estão associados a um diretório de feedback ativo. Remove um espaço de trabalho do seu diretório atual antes de o atribuíres aqui.", "no_unassigned_workspaces_title": "Nenhum espaço de trabalho disponível sem atribuição", - "pause_connectors_confirmation_description": "Pausar estas fontes de feedback irá impedir a adição de novos registos.", - "pause_connectors_confirmation_title": "Pausar fontes de feedback associadas?", + "pause_feedback_sources_confirmation_description": "Pausar estas fontes de feedback irá impedir a adição de novos registos.", + "pause_feedback_sources_confirmation_title": "Pausar fontes de feedback associadas?", "select_workspaces_placeholder": "Selecionar espaços de trabalho...", "show_archived": "Mostrar arquivados", "title": "Diretórios de Feedback", @@ -3689,14 +3689,6 @@ "collected_at": "Recolhido em", "configure_import": "Configurar importação", "configure_mapping": "Configurar mapeamento", - "connector_created_successfully": "Fonte de feedback criada com sucesso", - "connector_deleted_successfully": "Fonte de feedback eliminada com sucesso", - "connector_duplicated_successfully": "Fonte de feedback duplicada com sucesso", - "connector_name": "Nome da Origem", - "connector_name_hint": "Como esta origem aparece no teu painel. Preenchido automaticamente a partir do nome do ficheiro carregado — podes editar a qualquer momento.", - "connector_status_updated_successfully": "Estado da fonte de feedback atualizado com sucesso", - "connector_updated_successfully": "Fonte de feedback atualizada com sucesso", - "connectors": "Fontes de feedback", "create_mapping": "Criar mapeamento", "created_by": "Criado por", "csv_advanced": "Avançado", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Estas colunas não são usadas por nenhum campo de Registo de Feedback. Serão ignoradas na importação.", "custom_source_type": "Tipo de origem personalizado", "custom_source_type_placeholder": "Insira o tipo de fonte personalizado", - "default_connector_name_csv": "Importação CSV", - "default_connector_name_formbricks": "Fonte de Inquéritos Formbricks", + "default_source_name_csv": "Importação CSV", + "default_source_name_formbricks": "Fonte de Inquéritos 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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Editar fonte de feedback", "enter_name_for_source": "Introduz um nome para esta origem", "enum": "enum", - "error_connector_field_mapping_duplicate": "Mapeamento de campo duplicado para esta origem", - "error_connector_formbricks_mapping_duplicate": "Mapeamento de pergunta duplicado para esta origem", - "error_connector_name_duplicate": "Já existe uma origem com este nome", - "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", + "error_source_field_mapping_duplicate": "Mapeamento de campo duplicado para esta origem", + "error_source_formbricks_mapping_duplicate": "Mapeamento de pergunta duplicado para esta origem", + "error_source_name_duplicate": "Já existe uma origem com este nome", + "error_source_name_required": "O nome da origem é obrigatório", + "error_source_questions_required": "Seleciona pelo menos uma pergunta", + "error_source_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_directory": "Diretório de Feedback", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importa registos de feedback de ficheiros CSV", "source_connect_feedback_record_mcp_description": "Envia registos de feedback através da integração MCP.", "source_connect_formbricks_description": "Envia registos de feedback dos teus inquéritos Formbricks", + "source_created_successfully": "Fonte de feedback criada com sucesso", + "source_deleted_successfully": "Fonte de feedback eliminada com sucesso", + "source_duplicated_successfully": "Fonte de feedback duplicada com sucesso", "source_id": "ID da fonte", "source_name": "Nome da fonte", + "source_name_hint": "Como esta origem aparece no teu painel. Preenchido automaticamente a partir do nome do ficheiro carregado — podes editar a qualquer momento.", + "source_status_updated_successfully": "Estado da fonte de feedback atualizado com sucesso", "source_type": "Tipo de fonte", "source_type_cannot_be_changed": "O tipo de fonte não pode ser alterado", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Fonte de feedback atualizada com sucesso", + "sources": "Fontes de feedback", "status_error": "Erro", "status_live_sync": "Sincronização em direto", "status_ready": "Pronto", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 9f4ce74b2c24..733e802b9398 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -2282,9 +2282,9 @@ "advanced_styling_field_track_height": "Înălțime track", "advanced_styling_field_track_height_description": "Controlează grosimea barei de progres.", "advanced_styling_field_upper_label_color": "Culoare etichetă", - "advanced_styling_field_upper_label_color_description": "Colorează etichetele mici de deasupra câmpurilor și etichetele de scală.", - "advanced_styling_field_upper_label_size": "Mărime font etichetă", - "advanced_styling_field_upper_label_size_description": "Redimensionează etichetele mici de deasupra câmpurilor și etichetele de scală.", + "advanced_styling_field_upper_label_color_description": "Colorează etichetele mici de deasupra câmpurilor de introducere și etichetele scalei.", + "advanced_styling_field_upper_label_size": "Dimensiune font etichetă", + "advanced_styling_field_upper_label_size_description": "Ajustează dimensiunea etichetelor mici de deasupra câmpurilor de introducere și etichetelor scalei.", "advanced_styling_field_upper_label_weight": "Grosime font etichetă", "advanced_styling_field_upper_label_weight_description": "Face etichetele mai subțiri sau mai îngroșate.", "advanced_styling_section_buttons": "Butoane", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Nu ai permisiunea să arhivezi acest director.", "are_you_sure_you_want_to_archive": "Ești sigur că vrei să arhivezi acest director? Spațiile de lucru nu vor mai avea acces la el.", "assign_workspaces_description": "Controlează ce spații de lucru pot accesa acest director de feedback.", - "connectors_description": "Surse de feedback care trimit înregistrări către acest director.", "create_feedback_directory": "Creează director de feedback", "description": "Gestionează directoarele de feedback și atribuirile lor la spații de lucru.", "directory_archived_successfully": "Directorul a fost arhivat cu succes", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Directorul a fost dezarhivat cu succes", "directory_updated_successfully": "Directorul a fost actualizat cu succes", "empty_state": "Nu au fost găsite directoare de feedback. Creează unul pentru a începe.", - "error_directory_has_connectors": "Nu poți arhiva un director care are surse de feedback conectate. Elimină mai întâi toate sursele de feedback.", + "error_directory_has_feedback_sources": "Nu poți arhiva un director care are surse de feedback conectate. Elimină mai întâi toate sursele de feedback.", "error_directory_name_duplicate": "Există deja un director de feedback cu acest nume.", "error_directory_name_required": "Numele directorului este obligatoriu.", "error_directory_workspaces_invalid_org": "Unele spații de lucru specificate nu aparțin acestei organizații.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Surse de feedback care trimit înregistrări către acest director.", "grant_access_confirm": "Acordă acces la spațiul de lucru", "grant_workspace_access_title": "Confirmă acordarea accesului la spațiul de lucru", "grant_workspace_access_warning": "Toți membrii actuali și viitori ai spațiilor de lucru de mai jos vor primi acces de citire la toate datele de feedback direcționate către \"{directoryName}\", inclusiv datele primite de sursele de feedback conectate la alte spații de lucru asociate. Pentru a revoca accesul ulterior, elimină spațiul de lucru din acest director; înregistrările deja primite rămân în director.", "nav_label": "Directoare de feedback", "no_access": "Nu ai permisiunea de a gestiona directoarele de feedback.", - "no_connectors": "Nicio sursă de feedback conectată la acest director încă.", + "no_feedback_sources": "Nicio sursă de feedback conectată la acest director încă.", "no_unassigned_workspaces_description": "Fiecare spațiu de lucru este deja conectat la un director de feedback activ. Elimină un spațiu de lucru din directorul său actual înainte de a-l atribui aici.", "no_unassigned_workspaces_title": "Niciun spațiu de lucru neatribuit disponibil", - "pause_connectors_confirmation_description": "Pauza acestor surse de feedback va opri adăugarea de noi înregistrări.", - "pause_connectors_confirmation_title": "Pui pe pauză sursele de feedback conectate?", + "pause_feedback_sources_confirmation_description": "Pauza acestor surse de feedback va opri adăugarea de noi înregistrări.", + "pause_feedback_sources_confirmation_title": "Pui pe pauză sursele de feedback conectate?", "select_workspaces_placeholder": "Selectează spații de lucru...", "show_archived": "Afișează arhivate", "title": "Directoare de Feedback", @@ -3689,14 +3689,6 @@ "collected_at": "Colectat la", "configure_import": "Configurează importul", "configure_mapping": "Configurează maparea", - "connector_created_successfully": "Sursa de feedback a fost creată cu succes", - "connector_deleted_successfully": "Sursa de feedback a fost ștearsă cu succes", - "connector_duplicated_successfully": "Sursa de feedback a fost duplicată cu succes", - "connector_name": "Numele sursei", - "connector_name_hint": "Cum apare această sursă în tabloul tău de bord. Completat automat din numele fișierului încărcat — poți modifica oricând.", - "connector_status_updated_successfully": "Starea sursei de feedback a fost actualizată cu succes", - "connector_updated_successfully": "Sursa de feedback a fost actualizată cu succes", - "connectors": "Surse de feedback", "create_mapping": "Creează mapare", "created_by": "Creat de", "csv_advanced": "Avansat", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Aceste coloane nu sunt folosite de niciun câmp din Înregistrarea de Feedback. Vor fi ignorate la import.", "custom_source_type": "Tip sursă personalizat", "custom_source_type_placeholder": "Introduceți tipul de sursă personalizat", - "default_connector_name_csv": "Import CSV", - "default_connector_name_formbricks": "Sursă de sondaje Formbricks", + "default_source_name_csv": "Import CSV", + "default_source_name_formbricks": "Sursă de sondaje 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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Editează sursa de feedback", "enter_name_for_source": "Introdu un nume pentru această sursă", "enum": "enum", - "error_connector_field_mapping_duplicate": "Mapare duplicată a câmpului pentru această sursă", - "error_connector_formbricks_mapping_duplicate": "Mapare duplicată a întrebării pentru această sursă", - "error_connector_name_duplicate": "Există deja o sursă cu acest nume", - "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", + "error_source_field_mapping_duplicate": "Mapare duplicată a câmpului pentru această sursă", + "error_source_formbricks_mapping_duplicate": "Mapare duplicată a întrebării pentru această sursă", + "error_source_name_duplicate": "Există deja o sursă cu acest nume", + "error_source_name_required": "Numele sursei este obligatoriu", + "error_source_questions_required": "Selectează cel puțin o întrebare", + "error_source_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_directory": "Director de Feedback", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importă înregistrări de feedback din fișiere CSV", "source_connect_feedback_record_mcp_description": "Trimite înregistrări de feedback prin integrarea MCP.", "source_connect_formbricks_description": "Trimite înregistrări de feedback din sondajele tale Formbricks", + "source_created_successfully": "Sursa de feedback a fost creată cu succes", + "source_deleted_successfully": "Sursa de feedback a fost ștearsă cu succes", + "source_duplicated_successfully": "Sursa de feedback a fost duplicată cu succes", "source_id": "ID sursă", "source_name": "Nume sursă", + "source_name_hint": "Cum apare această sursă în tabloul tău de bord. Completat automat din numele fișierului încărcat — poți modifica oricând.", + "source_status_updated_successfully": "Starea sursei de feedback a fost actualizată cu succes", "source_type": "Tip sursă", "source_type_cannot_be_changed": "Tipul sursei nu poate fi schimbat", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Sursa de feedback a fost actualizată cu succes", + "sources": "Surse de feedback", "status_error": "Eroare", "status_live_sync": "Sincronizare în timp real", "status_ready": "Gata", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 1a9c531826b8..2777fd1b423b 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "Высота трека", "advanced_styling_field_track_height_description": "Управляет толщиной индикатора прогресса.", "advanced_styling_field_upper_label_color": "Цвет метки", - "advanced_styling_field_upper_label_color_description": "Задаёт цвет маленьких меток над полями ввода и меток шкалы.", + "advanced_styling_field_upper_label_color_description": "Окрашивает небольшие метки над полями ввода и шкалами.", "advanced_styling_field_upper_label_size": "Размер шрифта метки", - "advanced_styling_field_upper_label_size_description": "Изменяет размер маленьких меток над полями ввода и меток шкалы.", - "advanced_styling_field_upper_label_weight": "Толщина шрифта метки", - "advanced_styling_field_upper_label_weight_description": "Делает метки тоньше или жирнее.", + "advanced_styling_field_upper_label_size_description": "Изменяет размер небольших меток над полями ввода и шкалами.", + "advanced_styling_field_upper_label_weight": "Насыщенность шрифта метки", + "advanced_styling_field_upper_label_weight_description": "Делает метки светлее или жирнее.", "advanced_styling_section_buttons": "Кнопки", "advanced_styling_section_headlines": "Заголовки и описания", "advanced_styling_section_inputs": "Поля ввода", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "У тебя нет прав для архивирования этого каталога.", "are_you_sure_you_want_to_archive": "Ты уверен, что хочешь архивировать этот каталог? Рабочие пространства больше не будут иметь к нему доступа.", "assign_workspaces_description": "Управляй, какие рабочие пространства могут получить доступ к этой директории обратной связи.", - "connectors_description": "Источники отзывов, которые отправляют записи в этот каталог.", "create_feedback_directory": "Создать директорию для отзывов", "description": "Управляй директориями обратной связи и их привязками к рабочим пространствам.", "directory_archived_successfully": "Каталог успешно архивирован", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Каталог успешно разархивирован", "directory_updated_successfully": "Каталог успешно обновлён", "empty_state": "Директории обратной связи не найдены. Создай одну, чтобы начать.", - "error_directory_has_connectors": "Невозможно архивировать каталог, к которому привязаны источники отзывов. Сначала удали все источники отзывов.", + "error_directory_has_feedback_sources": "Невозможно архивировать каталог, к которому привязаны источники отзывов. Сначала удали все источники отзывов.", "error_directory_name_duplicate": "Директория обратной связи с таким названием уже существует.", "error_directory_name_required": "Необходимо указать имя директории.", "error_directory_workspaces_invalid_org": "Некоторые указанные рабочие пространства не принадлежат этой организации.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Источники отзывов, которые отправляют записи в этот каталог.", "grant_access_confirm": "Предоставить доступ к рабочему пространству", "grant_workspace_access_title": "Подтверди предоставление доступа к рабочему пространству", "grant_workspace_access_warning": "Все текущие и будущие участники перечисленных ниже рабочих пространств получат доступ на чтение ко всем данным отзывов, которые направляются в «{directoryName}», включая данные, полученные от источников отзывов, подключённых к другим связанным рабочим пространствам. Чтобы позже отозвать доступ, удали рабочее пространство из этого каталога; уже загруженные записи останутся в каталоге.", "nav_label": "Каталоги отзывов", "no_access": "У тебя нет прав для управления директориями обратной связи.", - "no_connectors": "К этому каталогу пока не привязаны источники отзывов.", + "no_feedback_sources": "К этому каталогу пока не привязаны источники отзывов.", "no_unassigned_workspaces_description": "Каждое рабочее пространство уже связано с активным каталогом отзывов. Удалите рабочее пространство из текущего каталога, прежде чем назначить его сюда.", "no_unassigned_workspaces_title": "Нет доступных неназначенных рабочих пространств", - "pause_connectors_confirmation_description": "Приостановка этих источников отзывов остановит добавление новых записей.", - "pause_connectors_confirmation_title": "Приостановить связанные источники отзывов?", + "pause_feedback_sources_confirmation_description": "Приостановка этих источников отзывов остановит добавление новых записей.", + "pause_feedback_sources_confirmation_title": "Приостановить связанные источники отзывов?", "select_workspaces_placeholder": "Выберите рабочие области...", "show_archived": "Показать архивные", "title": "Директории обратной связи", @@ -3689,14 +3689,6 @@ "collected_at": "Собрано", "configure_import": "Настроить импорт", "configure_mapping": "Настроить сопоставление", - "connector_created_successfully": "Источник отзывов успешно создан", - "connector_deleted_successfully": "Источник отзывов успешно удалён", - "connector_duplicated_successfully": "Источник отзывов успешно продублирован", - "connector_name": "Название источника", - "connector_name_hint": "Как этот источник отображается в вашей панели управления. Автоматически заполняется из имени загруженного файла — можно изменить в любое время.", - "connector_status_updated_successfully": "Статус источника отзывов успешно обновлён", - "connector_updated_successfully": "Источник отзывов успешно обновлён", - "connectors": "Источники отзывов", "create_mapping": "Создать сопоставление", "created_by": "Создано пользователем", "csv_advanced": "Дополнительные", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Эти столбцы не используются ни одним полем записи отзывов. Они будут проигнорированы при импорте.", "custom_source_type": "Пользовательский тип источника", "custom_source_type_placeholder": "Введите собственный тип источника", - "default_connector_name_csv": "Импорт CSV", - "default_connector_name_formbricks": "Источник опросов Formbricks", + "default_source_name_csv": "Импорт CSV", + "default_source_name_formbricks": "Источник опросов Formbricks", "delete_feedback_record": "Удалить запись обратной связи", "delete_feedback_record_confirmation": "Это навсегда удалит запись обратной связи и уберёт её из подключённого каталога.", "delete_feedback_records_confirmation": "Это навсегда удалит {count} {count, plural, one {запись обратной связи} few {записи обратной связи} many {записей обратной связи} other {записей обратной связи}} и уберёт их из подключённого каталога.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Редактировать источник отзывов", "enter_name_for_source": "Введи имя для этого источника", "enum": "enum", - "error_connector_field_mapping_duplicate": "Дублирующееся сопоставление полей для этого источника", - "error_connector_formbricks_mapping_duplicate": "Дублирующееся сопоставление вопросов для этого источника", - "error_connector_name_duplicate": "Источник с таким именем уже существует", - "error_connector_name_required": "Необходимо указать название источника", - "error_connector_questions_required": "Выберите хотя бы один вопрос", - "error_connector_survey_required": "Выберите опрос", + "error_source_field_mapping_duplicate": "Дублирующееся сопоставление полей для этого источника", + "error_source_formbricks_mapping_duplicate": "Дублирующееся сопоставление вопросов для этого источника", + "error_source_name_duplicate": "Источник с таким именем уже существует", + "error_source_name_required": "Необходимо указать название источника", + "error_source_questions_required": "Выберите хотя бы один вопрос", + "error_source_survey_required": "Выберите опрос", "failed_to_delete_feedback_records": "Не удалось удалить записи обратной связи", "failed_to_load_feedback_records": "Не удалось загрузить отзывы", "feedback_directory": "Директория обратной связи", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Импортируйте записи с отзывами из CSV-файлов", "source_connect_feedback_record_mcp_description": "Отправляйте записи обратной связи через интеграцию MCP.", "source_connect_formbricks_description": "Отправляйте записи с отзывами из ваших опросов Formbricks", + "source_created_successfully": "Источник отзывов успешно создан", + "source_deleted_successfully": "Источник отзывов успешно удалён", + "source_duplicated_successfully": "Источник отзывов успешно продублирован", "source_id": "Идентификатор источника", "source_name": "Имя источника", + "source_name_hint": "Как этот источник отображается в вашей панели управления. Автоматически заполняется из имени загруженного файла — можно изменить в любое время.", + "source_status_updated_successfully": "Статус источника отзывов успешно обновлён", "source_type": "Тип источника", "source_type_cannot_be_changed": "Тип источника нельзя изменить", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Источник отзывов успешно обновлён", + "sources": "Источники отзывов", "status_error": "Ошибка", "status_live_sync": "Синхронизация в реальном времени", "status_ready": "Готово", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index a1d67e9b9664..549b1f3bc40a 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -2281,12 +2281,12 @@ "advanced_styling_field_track_bg_description": "Färgar den ofyllda delen av stapeln.", "advanced_styling_field_track_height": "Spårets höjd", "advanced_styling_field_track_height_description": "Styr tjockleken på förloppsstapeln.", - "advanced_styling_field_upper_label_color": "Etikettfärg", - "advanced_styling_field_upper_label_color_description": "Färgar de små etiketterna ovanför fälten och skaletiketter.", - "advanced_styling_field_upper_label_size": "Etikettens teckenstorlek", - "advanced_styling_field_upper_label_size_description": "Skalar storleken på de små etiketterna ovanför fälten och skaletiketter.", - "advanced_styling_field_upper_label_weight": "Etikettens teckentjocklek", - "advanced_styling_field_upper_label_weight_description": "Gör etiketterna tunnare eller fetare.", + "advanced_styling_field_upper_label_color": "Etiketfärg", + "advanced_styling_field_upper_label_color_description": "Färgsätter de små etiketterna ovanför inmatningsfält och skalrubriker.", + "advanced_styling_field_upper_label_size": "Etikettstorlek", + "advanced_styling_field_upper_label_size_description": "Skalar de små etiketterna ovanför inmatningsfält och skalrubriker.", + "advanced_styling_field_upper_label_weight": "Etikettets typsnittsvikt", + "advanced_styling_field_upper_label_weight_description": "Gör etiketterna ljusare eller fetare.", "advanced_styling_section_buttons": "Knappar", "advanced_styling_section_headlines": "Rubriker & beskrivningar", "advanced_styling_section_inputs": "Inmatningar", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Du har inte behörighet att arkivera den här katalogen.", "are_you_sure_you_want_to_archive": "Är du säker på att du vill arkivera den här katalogen? Arbetsytor kommer inte längre ha tillgång till den.", "assign_workspaces_description": "Styr vilka arbetsytor som kan komma åt denna feedback-katalog.", - "connectors_description": "Feedbackkällor som skickar poster till den här katalogen.", "create_feedback_directory": "Skapa feedbackkatalog", "description": "Hantera feedback-kataloger och deras arbetsytetilldelningar.", "directory_archived_successfully": "Katalogen arkiverades", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Katalogen återställdes från arkivet", "directory_updated_successfully": "Katalogen uppdaterades", "empty_state": "Inga feedback-kataloger hittades. Skapa en för att komma igång.", - "error_directory_has_connectors": "Kan inte arkivera en katalog som har feedbackkällor kopplade till sig. Ta bort alla feedbackkällor först.", + "error_directory_has_feedback_sources": "Kan inte arkivera en katalog som har feedbackkällor kopplade till sig. Ta bort alla feedbackkällor först.", "error_directory_name_duplicate": "En feedback-katalog med detta namn finns redan.", "error_directory_name_required": "Katalognamn krävs.", "error_directory_workspaces_invalid_org": "Vissa angivna arbetsytor tillhör inte denna organisation.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Feedbackkällor som skickar poster till den här katalogen.", "grant_access_confirm": "Bevilja arbetsyteåtkomst", "grant_workspace_access_title": "Bekräfta beviljande av arbetsyteåtkomst", "grant_workspace_access_warning": "Alla nuvarande och framtida medlemmar i arbetsytorna nedan kommer att få läsåtkomst till all feedbackdata som dirigeras till \"{directoryName}\", inklusive data som samlas in av feedbackkällor anslutna till andra länkade arbetsytor. För att återkalla åtkomst senare, ta bort arbetsytan från den här katalogen; redan insamlade poster förblir i katalogen.", "nav_label": "Feedbackkataloger", "no_access": "Du har inte behörighet att hantera feedback-kataloger.", - "no_connectors": "Inga feedbackkällor länkade till den här katalogen ännu.", + "no_feedback_sources": "Inga feedbackkällor länkade till den här katalogen ännu.", "no_unassigned_workspaces_description": "Varje arbetsyta är redan kopplad till en aktiv feedbackkatalog. Ta bort en arbetsyta från dess nuvarande katalog innan du tilldelar den här.", "no_unassigned_workspaces_title": "Inga otilldelade arbetsytor tillgängliga", - "pause_connectors_confirmation_description": "Att pausa dessa feedbackkällor kommer att stoppa nya poster från att läggas till.", - "pause_connectors_confirmation_title": "Pausa länkade feedbackkällor?", + "pause_feedback_sources_confirmation_description": "Att pausa dessa feedbackkällor kommer att stoppa nya poster från att läggas till.", + "pause_feedback_sources_confirmation_title": "Pausa länkade feedbackkällor?", "select_workspaces_placeholder": "Välj arbetsytor...", "show_archived": "Visa arkiverade", "title": "Feedback-kataloger", @@ -3689,14 +3689,6 @@ "collected_at": "Insamlad", "configure_import": "Konfigurera import", "configure_mapping": "Konfigurera mappning", - "connector_created_successfully": "Feedbackkällan skapades framgångsrikt", - "connector_deleted_successfully": "Feedbackkällan togs bort framgångsrikt", - "connector_duplicated_successfully": "Feedbackkällan duplicerades framgångsrikt", - "connector_name": "Källans namn", - "connector_name_hint": "Så här visas källan i din dashboard. Fylls i automatiskt från det uppladdade filnamnet — redigera när du vill.", - "connector_status_updated_successfully": "Feedbackkällans status uppdaterades framgångsrikt", - "connector_updated_successfully": "Feedbackkällan uppdaterades framgångsrikt", - "connectors": "Feedbackkällor", "create_mapping": "Skapa mappning", "created_by": "Skapad av", "csv_advanced": "Avancerat", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Dessa kolumner används inte av något fält i feedbackposten. De kommer att ignoreras vid import.", "custom_source_type": "Anpassad källtyp", "custom_source_type_placeholder": "Ange anpassad källtyp", - "default_connector_name_csv": "CSV-import", - "default_connector_name_formbricks": "Formbricks enkätkälla", + "default_source_name_csv": "CSV-import", + "default_source_name_formbricks": "Formbricks enkätkälla", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Redigera feedbackkälla", "enter_name_for_source": "Ange ett namn för denna källa", "enum": "enum", - "error_connector_field_mapping_duplicate": "Duplicerad fältmappning för denna källa", - "error_connector_formbricks_mapping_duplicate": "Duplicerad frågemappning för denna källa", - "error_connector_name_duplicate": "En källa med det här namnet finns redan", - "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", + "error_source_field_mapping_duplicate": "Duplicerad fältmappning för denna källa", + "error_source_formbricks_mapping_duplicate": "Duplicerad frågemappning för denna källa", + "error_source_name_duplicate": "En källa med det här namnet finns redan", + "error_source_name_required": "Källnamn krävs", + "error_source_questions_required": "Välj minst en fråga", + "error_source_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_directory": "Feedback-katalog", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "Importera feedbackposter från CSV-filer", "source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.", "source_connect_formbricks_description": "Skicka feedbackposter från dina Formbricks-enkäter", + "source_created_successfully": "Feedbackkällan skapades framgångsrikt", + "source_deleted_successfully": "Feedbackkällan togs bort framgångsrikt", + "source_duplicated_successfully": "Feedbackkällan duplicerades framgångsrikt", "source_id": "Käll-ID", "source_name": "Källnamn", + "source_name_hint": "Så här visas källan i din dashboard. Fylls i automatiskt från det uppladdade filnamnet — redigera när du vill.", + "source_status_updated_successfully": "Feedbackkällans status uppdaterades framgångsrikt", "source_type": "Källtyp", "source_type_cannot_be_changed": "Källtyp kan inte ändras", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Feedbackkällan uppdaterades framgångsrikt", + "sources": "Feedbackkällor", "status_error": "Fel", "status_live_sync": "Live sync", "status_ready": "Ready", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index 0cf1ca66045a..413bc47d11af 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -2281,12 +2281,12 @@ "advanced_styling_field_track_bg_description": "Çubuğun doldurulmamış kısmını renklendirir.", "advanced_styling_field_track_height": "İz Yüksekliği", "advanced_styling_field_track_height_description": "İlerleme çubuğu kalınlığını kontrol eder.", - "advanced_styling_field_upper_label_color": "Başlık Etiketi Rengi", - "advanced_styling_field_upper_label_color_description": "Giriş alanlarının üzerindeki küçük etiketi renklendirir.", - "advanced_styling_field_upper_label_size": "Başlık Etiketi Yazı Boyutu", - "advanced_styling_field_upper_label_size_description": "Giriş alanlarının üzerindeki küçük etiketin ölçeğini ayarlar.", - "advanced_styling_field_upper_label_weight": "Başlık Etiketi Yazı Kalınlığı", - "advanced_styling_field_upper_label_weight_description": "Etiketi daha ince veya kalın yapar.", + "advanced_styling_field_upper_label_color": "Etiket Rengi", + "advanced_styling_field_upper_label_color_description": "Girdi alanlarının ve ölçek etiketlerinin üzerindeki küçük etiketleri renklendirir.", + "advanced_styling_field_upper_label_size": "Etiket Yazı Boyutu", + "advanced_styling_field_upper_label_size_description": "Girdi alanlarının ve ölçek etiketlerinin üzerindeki küçük etiketlerin boyutunu ayarlar.", + "advanced_styling_field_upper_label_weight": "Etiket Yazı Kalınlığı", + "advanced_styling_field_upper_label_weight_description": "Etiketleri daha ince veya daha kalın yapar.", "advanced_styling_section_buttons": "Düğmeler", "advanced_styling_section_headlines": "Başlıklar ve Açıklamalar", "advanced_styling_section_inputs": "Giriş Alanları", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "Bu dizini arşivleme yetkin yok.", "are_you_sure_you_want_to_archive": "Bu dizini arşivlemek istediğinden emin misin? Çalışma alanları artık bu dizine erişemeyecek.", "assign_workspaces_description": "Hangi çalışma alanlarının bu geri bildirim dizinine erişebileceğini kontrol et.", - "connectors_description": "Bu dizine kayıt gönderen geri bildirim kaynakları.", "create_feedback_directory": "Geri bildirim dizini oluştur", "description": "Geri bildirim dizinlerini ve çalışma alanı atamalarını yönet.", "directory_archived_successfully": "Dizin başarıyla arşivlendi", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "Dizin başarıyla arşivden çıkarıldı", "directory_updated_successfully": "Dizin başarıyla güncellendi", "empty_state": "Geri bildirim dizini bulunamadı. Başlamak için bir tane oluştur.", - "error_directory_has_connectors": "Geri bildirim kaynaklarına bağlı olan bir dizini arşivleyemezsin. Önce tüm geri bildirim kaynaklarını kaldır.", + "error_directory_has_feedback_sources": "Geri bildirim kaynaklarına bağlı olan bir dizini arşivleyemezsin. Önce tüm geri bildirim kaynaklarını kaldır.", "error_directory_name_duplicate": "Bu adda bir geri bildirim dizini zaten mevcut.", "error_directory_name_required": "Dizin adı gereklidir.", "error_directory_workspaces_invalid_org": "Belirtilen çalışma alanlarından bazıları bu organizasyona ait değil.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "Bu dizine kayıt gönderen geri bildirim kaynakları.", "grant_access_confirm": "Çalışma alanı erişimi ver", "grant_workspace_access_title": "Çalışma alanı erişim iznini onayla", "grant_workspace_access_warning": "Aşağıdaki çalışma alanlarının tüm mevcut ve gelecekteki üyeleri, diğer bağlı çalışma alanlarına bağlı geri bildirim kaynakları tarafından alınan veriler de dahil olmak üzere \"{directoryName}\" dizinine yönlendirilen tüm geri bildirim verilerine okuma erişimi kazanacak. Erişimi daha sonra iptal etmek için çalışma alanını bu dizinden kaldır; zaten alınmış kayıtlar dizinde kalır.", "nav_label": "Geri Bildirim Dizinleri", "no_access": "Geri bildirim dizinlerini yönetme yetkin yok.", - "no_connectors": "Bu dizine henüz bağlı geri bildirim kaynağı yok.", + "no_feedback_sources": "Bu dizine henüz bağlı geri bildirim kaynağı yok.", "no_unassigned_workspaces_description": "Her çalışma alanı zaten aktif bir geri bildirim dizinine bağlı. Buraya atamadan önce bir çalışma alanını mevcut dizininden kaldırın.", "no_unassigned_workspaces_title": "Atanmamış çalışma alanı yok", - "pause_connectors_confirmation_description": "Bu geri bildirim kaynaklarını duraklatmak, yeni kayıtların eklenmesini durdurur.", - "pause_connectors_confirmation_title": "Bağlı geri bildirim kaynakları duraklatılsın mı?", + "pause_feedback_sources_confirmation_description": "Bu geri bildirim kaynaklarını duraklatmak, yeni kayıtların eklenmesini durdurur.", + "pause_feedback_sources_confirmation_title": "Bağlı geri bildirim kaynakları duraklatılsın mı?", "select_workspaces_placeholder": "Çalışma alanlarını seç...", "show_archived": "Arşivlenmişleri göster", "title": "Geri Bildirim Dizinleri", @@ -3689,14 +3689,6 @@ "collected_at": "Toplandığı Tarih", "configure_import": "İçe aktarmayı yapılandır", "configure_mapping": "Eşleştirmeyi yapılandır", - "connector_created_successfully": "Geri bildirim kaynağı başarıyla oluşturuldu", - "connector_deleted_successfully": "Geri bildirim kaynağı başarıyla silindi", - "connector_duplicated_successfully": "Geri bildirim kaynağı başarıyla kopyalandı", - "connector_name": "Kaynak Adı", - "connector_name_hint": "Bu kaynağın kontrol panelinizde nasıl göründüğü. Yüklenen dosya adından otomatik olarak doldurulur — istediğin zaman düzenleyebilirsin.", - "connector_status_updated_successfully": "Geri bildirim kaynağı durumu başarıyla güncellendi", - "connector_updated_successfully": "Geri bildirim kaynağı başarıyla güncellendi", - "connectors": "Geri bildirim kaynakları", "create_mapping": "Eşleştirme oluştur", "created_by": "Oluşturan", "csv_advanced": "Gelişmiş", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "Bu sütunlar herhangi bir Geri Bildirim Kaydı alanı tarafından kullanılmıyor. İçe aktarma sırasında göz ardı edilecekler.", "custom_source_type": "Özel kaynak türü", "custom_source_type_placeholder": "Özel kaynak türünü girin", - "default_connector_name_csv": "CSV İçe Aktarma", - "default_connector_name_formbricks": "Formbricks Anket Kaynağı", + "default_source_name_csv": "CSV İçe Aktarma", + "default_source_name_formbricks": "Formbricks Anket Kaynağı", "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.", @@ -3746,12 +3738,12 @@ "edit_source_connection": "Geri bildirim kaynağını düzenle", "enter_name_for_source": "Bu kaynak için bir ad girin", "enum": "enum", - "error_connector_field_mapping_duplicate": "Bu kaynak için yinelenen alan eşlemesi", - "error_connector_formbricks_mapping_duplicate": "Bu kaynak için yinelenen soru eşlemesi", - "error_connector_name_duplicate": "Bu isimde bir kaynak zaten mevcut", - "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", + "error_source_field_mapping_duplicate": "Bu kaynak için yinelenen alan eşlemesi", + "error_source_formbricks_mapping_duplicate": "Bu kaynak için yinelenen soru eşlemesi", + "error_source_name_duplicate": "Bu isimde bir kaynak zaten mevcut", + "error_source_name_required": "Kaynak adı gereklidir", + "error_source_questions_required": "En az bir soru seçin", + "error_source_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_directory": "Geri Bildirim Dizini", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "CSV dosyalarından geri bildirim kayıtlarını içe aktar", "source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.", "source_connect_formbricks_description": "Formbricks anketlerinizden geri bildirim kayıtlarını gönder", + "source_created_successfully": "Geri bildirim kaynağı başarıyla oluşturuldu", + "source_deleted_successfully": "Geri bildirim kaynağı başarıyla silindi", + "source_duplicated_successfully": "Geri bildirim kaynağı başarıyla kopyalandı", "source_id": "Kaynak kimliği", "source_name": "Kaynak Adı", + "source_name_hint": "Bu kaynağın kontrol panelinizde nasıl göründüğü. Yüklenen dosya adından otomatik olarak doldurulur — istediğin zaman düzenleyebilirsin.", + "source_status_updated_successfully": "Geri bildirim kaynağı durumu başarıyla güncellendi", "source_type": "Kaynak Türü", "source_type_cannot_be_changed": "Kaynak türü değiştirilemez", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "Geri bildirim kaynağı başarıyla güncellendi", + "sources": "Geri bildirim kaynakları", "status_error": "Hata", "status_live_sync": "Live sync", "status_ready": "Ready", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index a28332935570..58e1f41849c1 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "轨道高度", "advanced_styling_field_track_height_description": "控制进度条的粗细。", "advanced_styling_field_upper_label_color": "标签颜色", - "advanced_styling_field_upper_label_color_description": "设置输入框上方小标签和刻度标签的颜色。", + "advanced_styling_field_upper_label_color_description": "为输入框上方的小标签和刻度标签着色。", "advanced_styling_field_upper_label_size": "标签字体大小", - "advanced_styling_field_upper_label_size_description": "调整输入框上方小标签和刻度标签的大小。", + "advanced_styling_field_upper_label_size_description": "调整输入框上方的小标签和刻度标签的大小。", "advanced_styling_field_upper_label_weight": "标签字体粗细", - "advanced_styling_field_upper_label_weight_description": "设置标签文字的粗细。", + "advanced_styling_field_upper_label_weight_description": "使标签更细或更粗。", "advanced_styling_section_buttons": "按钮", "advanced_styling_section_headlines": "标题和描述", "advanced_styling_section_inputs": "输入项", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "你无权归档此目录。", "are_you_sure_you_want_to_archive": "确定要归档此目录吗?工作区将无法再访问它。", "assign_workspaces_description": "控制哪些工作区可以访问此反馈目录。", - "connectors_description": "将反馈记录发送到该目录的反馈来源。", "create_feedback_directory": "创建反馈目录", "description": "管理反馈目录及其工作区分配。", "directory_archived_successfully": "目录已成功归档", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "目录已成功取消归档", "directory_updated_successfully": "目录已成功更新", "empty_state": "未找到反馈目录。创建一个以开始使用。", - "error_directory_has_connectors": "无法归档已关联反馈来源的目录。请先移除所有反馈来源。", + "error_directory_has_feedback_sources": "无法归档已关联反馈来源的目录。请先移除所有反馈来源。", "error_directory_name_duplicate": "已存在使用此名称的反馈目录。", "error_directory_name_required": "目录名称为必填项。", "error_directory_workspaces_invalid_org": "某些指定的工作区不属于此组织。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "将反馈记录发送到该目录的反馈来源。", "grant_access_confirm": "授权工作区访问权限", "grant_workspace_access_title": "确认授权工作区访问", "grant_workspace_access_warning": "下方工作区所有当前及未来成员都将获得对所有流入“{directoryName}”的反馈数据的读取权限,包括通过其他已关联工作区的反馈来源导入的数据。如需撤销权限,请将工作区从此目录中移除;已导入的记录仍将保留在目录中。", "nav_label": "反馈目录", "no_access": "你没有管理反馈目录的权限。", - "no_connectors": "暂未关联反馈来源到此目录。", + "no_feedback_sources": "暂未关联反馈来源到此目录。", "no_unassigned_workspaces_description": "每个工作区都已链接到活跃的反馈目录。在此处分配前,请先从当前目录中移除工作区。", "no_unassigned_workspaces_title": "没有可用的未分配工作区", - "pause_connectors_confirmation_description": "暂停这些反馈来源后,将不会有新记录添加进来。", - "pause_connectors_confirmation_title": "暂停关联反馈来源?", + "pause_feedback_sources_confirmation_description": "暂停这些反馈来源后,将不会有新记录添加进来。", + "pause_feedback_sources_confirmation_title": "暂停关联反馈来源?", "select_workspaces_placeholder": "选择工作区...", "show_archived": "显示已归档", "title": "反馈目录", @@ -3689,14 +3689,6 @@ "collected_at": "收集时间", "configure_import": "配置导入", "configure_mapping": "配置映射", - "connector_created_successfully": "反馈来源创建成功", - "connector_deleted_successfully": "反馈来源删除成功", - "connector_duplicated_successfully": "反馈来源复制成功", - "connector_name": "数据源名称", - "connector_name_hint": "此数据源在仪表板中的显示名称。默认为上传文件的文件名,可随时编辑。", - "connector_status_updated_successfully": "反馈来源状态更新成功", - "connector_updated_successfully": "反馈来源更新成功", - "connectors": "反馈来源", "create_mapping": "创建映射", "created_by": "由 创建", "csv_advanced": "高级设置", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "这些列未被任何反馈记录字段使用,导入时将被忽略。", "custom_source_type": "自定义源类型", "custom_source_type_placeholder": "输入自定义来源类型", - "default_connector_name_csv": "CSV 导入", - "default_connector_name_formbricks": "Formbricks 调研来源", + "default_source_name_csv": "CSV 导入", + "default_source_name_formbricks": "Formbricks 调研来源", "delete_feedback_record": "删除反馈记录", "delete_feedback_record_confirmation": "这将永久删除该反馈记录并从关联目录中移除。", "delete_feedback_records_confirmation": "这将永久删除 {count} 条反馈记录并从关联目录中移除。", @@ -3746,12 +3738,12 @@ "edit_source_connection": "编辑反馈来源", "enter_name_for_source": "为此来源输入名称", "enum": "枚举", - "error_connector_field_mapping_duplicate": "此来源存在重复的字段映射", - "error_connector_formbricks_mapping_duplicate": "此来源存在重复的问题映射", - "error_connector_name_duplicate": "该名称的数据源已存在", - "error_connector_name_required": "数据源名称为必填项", - "error_connector_questions_required": "请至少选择一个问题", - "error_connector_survey_required": "请选择一个调查问卷", + "error_source_field_mapping_duplicate": "此来源存在重复的字段映射", + "error_source_formbricks_mapping_duplicate": "此来源存在重复的问题映射", + "error_source_name_duplicate": "该名称的数据源已存在", + "error_source_name_required": "数据源名称为必填项", + "error_source_questions_required": "请至少选择一个问题", + "error_source_survey_required": "请选择一个调查问卷", "failed_to_delete_feedback_records": "删除反馈记录失败", "failed_to_load_feedback_records": "加载反馈记录失败", "feedback_directory": "反馈目录", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "从 CSV 文件导入反馈记录", "source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.", "source_connect_formbricks_description": "从您的 Formbricks 调查发送反馈记录", + "source_created_successfully": "反馈来源创建成功", + "source_deleted_successfully": "反馈来源删除成功", + "source_duplicated_successfully": "反馈来源复制成功", "source_id": "源ID", "source_name": "来源名称", + "source_name_hint": "此数据源在仪表板中的显示名称。默认为上传文件的文件名,可随时编辑。", + "source_status_updated_successfully": "反馈来源状态更新成功", "source_type": "来源类型", "source_type_cannot_be_changed": "来源类型无法更改", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "反馈来源更新成功", + "sources": "反馈来源", "status_error": "错误", "status_live_sync": "Live sync", "status_ready": "Ready", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index fc7c01d7f8a5..eb90e555069f 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -2282,11 +2282,11 @@ "advanced_styling_field_track_height": "軌道高度", "advanced_styling_field_track_height_description": "調整進度條的厚度。", "advanced_styling_field_upper_label_color": "標籤顏色", - "advanced_styling_field_upper_label_color_description": "設定輸入框上方小標籤和刻度標籤的顏色。", - "advanced_styling_field_upper_label_size": "標籤字體大小", - "advanced_styling_field_upper_label_size_description": "調整輸入框上方小標籤和刻度標籤的大小。", - "advanced_styling_field_upper_label_weight": "標籤字體粗細", - "advanced_styling_field_upper_label_weight_description": "讓標籤字體變細或變粗。", + "advanced_styling_field_upper_label_color_description": "為輸入框上方的小標籤和量表標籤著色。", + "advanced_styling_field_upper_label_size": "標籤字型大小", + "advanced_styling_field_upper_label_size_description": "調整輸入框上方的小標籤和量表標籤的大小。", + "advanced_styling_field_upper_label_weight": "標籤字型粗細", + "advanced_styling_field_upper_label_weight_description": "使標籤更細或更粗。", "advanced_styling_section_buttons": "按鈕", "advanced_styling_section_headlines": "標題與說明", "advanced_styling_section_inputs": "輸入欄位", @@ -2573,7 +2573,6 @@ "archive_not_allowed": "您沒有權限封存此目錄。", "are_you_sure_you_want_to_archive": "確定要封存此目錄嗎?工作區將無法再存取它。", "assign_workspaces_description": "控制哪些工作區可以存取此意見回饋目錄。", - "connectors_description": "將記錄傳送到此目錄的回饋來源。", "create_feedback_directory": "建立意見回饋目錄", "description": "管理意見回饋目錄及其工作區指派。", "directory_archived_successfully": "目錄已成功封存", @@ -2585,21 +2584,22 @@ "directory_unarchived_successfully": "目錄已成功取消封存", "directory_updated_successfully": "目錄已成功更新", "empty_state": "找不到意見回饋目錄。建立一個以開始使用。", - "error_directory_has_connectors": "無法封存已連結回饋來源的目錄。請先移除所有回饋來源。", + "error_directory_has_feedback_sources": "無法封存已連結回饋來源的目錄。請先移除所有回饋來源。", "error_directory_name_duplicate": "已存在同名的意見回饋目錄。", "error_directory_name_required": "目錄名稱為必填項目。", "error_directory_workspaces_invalid_org": "部分指定的工作區不屬於此組織。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", + "feedback_sources_description": "將記錄傳送到此目錄的回饋來源。", "grant_access_confirm": "授予工作區存取權限", "grant_workspace_access_title": "確認授予工作區存取權限", "grant_workspace_access_warning": "以下工作區的所有現有成員及未來成員都將獲得讀取權限,可存取所有導入「{directoryName}」的回饋資料,包括由連結到其他工作區的回饋來源所匯入的資料。若要稍後撤銷存取權限,請從此目錄中移除該工作區;已匯入的記錄將保留在目錄中。", "nav_label": "意見回饋目錄", "no_access": "你沒有權限管理意見回饋目錄。", - "no_connectors": "此目錄尚未連結任何回饋來源。", + "no_feedback_sources": "此目錄尚未連結任何回饋來源。", "no_unassigned_workspaces_description": "每個工作區都已連結到作用中的意見回饋目錄。請先從目前目錄中移除工作區,再於此處指派。", "no_unassigned_workspaces_title": "沒有可用的未指派工作區", - "pause_connectors_confirmation_description": "暫停這些回饋來源將停止新增記錄。", - "pause_connectors_confirmation_title": "暫停已連結的回饋來源?", + "pause_feedback_sources_confirmation_description": "暫停這些回饋來源將停止新增記錄。", + "pause_feedback_sources_confirmation_title": "暫停已連結的回饋來源?", "select_workspaces_placeholder": "選擇工作區...", "show_archived": "顯示已封存", "title": "意見回饋目錄", @@ -3689,14 +3689,6 @@ "collected_at": "收集時間", "configure_import": "設定匯入", "configure_mapping": "設定對應關係", - "connector_created_successfully": "回饋來源建立成功", - "connector_deleted_successfully": "回饋來源刪除成功", - "connector_duplicated_successfully": "回饋來源複製成功", - "connector_name": "來源名稱", - "connector_name_hint": "此來源在你的儀表板中顯示的名稱。會自動從上傳的檔案名稱填入,你可以隨時編輯。", - "connector_status_updated_successfully": "回饋來源狀態更新成功", - "connector_updated_successfully": "回饋來源更新成功", - "connectors": "回饋來源", "create_mapping": "建立對應關係", "created_by": "建立者", "csv_advanced": "進階", @@ -3733,8 +3725,8 @@ "csv_unmapped_columns_explainer": "這些欄位沒有被任何意見回饋記錄欄位使用。匯入時會被忽略。", "custom_source_type": "自訂來源類型", "custom_source_type_placeholder": "輸入自訂來源類型", - "default_connector_name_csv": "CSV 匯入", - "default_connector_name_formbricks": "Formbricks 問卷來源", + "default_source_name_csv": "CSV 匯入", + "default_source_name_formbricks": "Formbricks 問卷來源", "delete_feedback_record": "刪除意見回饋記錄", "delete_feedback_record_confirmation": "這將永久刪除此意見回饋記錄,並從已連結的目錄中移除。", "delete_feedback_records_confirmation": "這將永久刪除 {count} 筆意見回饋記錄,並從已連結的目錄中移除。", @@ -3746,12 +3738,12 @@ "edit_source_connection": "編輯回饋來源", "enter_name_for_source": "請輸入此來源的名稱", "enum": "enum", - "error_connector_field_mapping_duplicate": "此來源的欄位對應重複", - "error_connector_formbricks_mapping_duplicate": "此來源的問題對應重複", - "error_connector_name_duplicate": "已存在使用此名稱的來源", - "error_connector_name_required": "來源名稱為必填項目", - "error_connector_questions_required": "請至少選擇一個問題", - "error_connector_survey_required": "請選擇一個調查問卷", + "error_source_field_mapping_duplicate": "此來源的欄位對應重複", + "error_source_formbricks_mapping_duplicate": "此來源的問題對應重複", + "error_source_name_duplicate": "已存在使用此名稱的來源", + "error_source_name_required": "來源名稱為必填項目", + "error_source_questions_required": "請至少選擇一個問題", + "error_source_survey_required": "請選擇一個調查問卷", "failed_to_delete_feedback_records": "刪除意見回饋記錄失敗", "failed_to_load_feedback_records": "載入回饋紀錄失敗", "feedback_directory": "意見回饋目錄", @@ -3840,8 +3832,13 @@ "source_connect_csv_description": "從 CSV 檔案匯入回饋記錄", "source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.", "source_connect_formbricks_description": "從您的 Formbricks 問卷傳送回饋記錄", + "source_created_successfully": "回饋來源建立成功", + "source_deleted_successfully": "回饋來源刪除成功", + "source_duplicated_successfully": "回饋來源複製成功", "source_id": "來源ID", "source_name": "來源名稱", + "source_name_hint": "此來源在你的儀表板中顯示的名稱。會自動從上傳的檔案名稱填入,你可以隨時編輯。", + "source_status_updated_successfully": "回饋來源狀態更新成功", "source_type": "來源類型", "source_type_cannot_be_changed": "來源類型無法變更", "source_type_label_feedback_form": "Feedback form", @@ -3852,6 +3849,8 @@ "source_type_label_support": "Support", "source_type_label_survey": "Survey", "source_type_label_usability_test": "Usability test", + "source_updated_successfully": "回饋來源更新成功", + "sources": "回饋來源", "status_error": "錯誤", "status_live_sync": "Live sync", "status_ready": "Ready", diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx index 3f20331616c2..33e6211b9b3b 100644 --- a/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx +++ b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx @@ -1,7 +1,7 @@ import { use } from "react"; import { getAISmartToolsUnavailableReason, getOrganizationAIConfig } from "@/lib/ai/service"; -import { getConnectorsWithMappings } from "@/lib/connector/service"; import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getFeedbackSourcesWithMappings } from "@/lib/feedback-source/service"; import { getTranslate } from "@/lingodotdev/server"; import { ChartsList } from "@/modules/ee/analysis/charts/components/charts-list"; import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button"; @@ -82,9 +82,9 @@ export async function ChartsListPage({ workspaceId }: Readonly ) : ( - 0} /> + 0} /> )} ); diff --git a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx index 0ffa1456bd72..a408b952ebcb 100644 --- a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx +++ b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx @@ -1,6 +1,6 @@ import { use } from "react"; -import { getConnectorsWithMappings } from "@/lib/connector/service"; import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getFeedbackSourcesWithMappings } from "@/lib/feedback-source/service"; import { getTranslate } from "@/lingodotdev/server"; import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout"; import { NoFeedbackRecordsState } from "@/modules/ee/analysis/components/no-feedback-records-state"; @@ -66,9 +66,9 @@ export const DashboardsListPage = async ({ workspaceId }: Readonly ) : ( - 0} /> + 0} /> )} ); diff --git a/apps/web/modules/ee/contacts/segments/actions.ts b/apps/web/modules/ee/contacts/segments/actions.ts index 4ace9586cbe5..55fcf3bc8292 100644 --- a/apps/web/modules/ee/contacts/segments/actions.ts +++ b/apps/web/modules/ee/contacts/segments/actions.ts @@ -10,9 +10,11 @@ import { loadNewSegmentInSurvey } from "@/lib/survey/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { + getOrganizationIdFromContactAttributeKeyId, getOrganizationIdFromSegmentId, getOrganizationIdFromSurveyId, getOrganizationIdFromWorkspaceId, + getWorkspaceIdFromContactAttributeKeyId, getWorkspaceIdFromSegmentId, getWorkspaceIdFromSurveyId, } from "@/lib/utils/helper"; @@ -311,17 +313,15 @@ export const resetSegmentFiltersAction = authenticatedActionClient ); const ZGetDistinctAttributeValuesAction = z.object({ - workspaceId: ZId, attributeKeyId: ZId, }); export const getDistinctAttributeValuesAction = authenticatedActionClient .inputSchema(ZGetDistinctAttributeValuesAction) .action(async ({ ctx, parsedInput }) => { - const workspaceId = parsedInput.workspaceId; await checkAuthorizationUpdated({ userId: ctx.user.id, - organizationId: await getOrganizationIdFromWorkspaceId(workspaceId), + organizationId: await getOrganizationIdFromContactAttributeKeyId(parsedInput.attributeKeyId), access: [ { type: "organization", @@ -330,7 +330,7 @@ export const getDistinctAttributeValuesAction = authenticatedActionClient { type: "workspaceTeam", minPermission: "read", - workspaceId, + workspaceId: await getWorkspaceIdFromContactAttributeKeyId(parsedInput.attributeKeyId), }, ], }); diff --git a/apps/web/modules/ee/contacts/segments/components/attribute-value-input.tsx b/apps/web/modules/ee/contacts/segments/components/attribute-value-input.tsx index 7f8052c716f4..74b61b0cc0b3 100644 --- a/apps/web/modules/ee/contacts/segments/components/attribute-value-input.tsx +++ b/apps/web/modules/ee/contacts/segments/components/attribute-value-input.tsx @@ -7,7 +7,6 @@ import { getDistinctAttributeValuesAction } from "../actions"; interface AttributeValueInputProps { attributeKeyId: string; - workspaceId: string; value: string; onChange: (value: string) => void; disabled?: boolean; @@ -17,7 +16,6 @@ interface AttributeValueInputProps { export const AttributeValueInput = ({ attributeKeyId, - workspaceId, value, onChange, disabled, @@ -40,7 +38,6 @@ export const AttributeValueInput = ({ setLoading(true); try { const result = await getDistinctAttributeValuesAction({ - workspaceId, attributeKeyId, }); @@ -68,7 +65,7 @@ export const AttributeValueInput = ({ return () => { isCancelled = true; }; - }, [workspaceId, attributeKeyId]); + }, [attributeKeyId]); const emptyDropdownText = useMemo(() => { if (loading) return "Loading values..."; diff --git a/apps/web/modules/ee/contacts/segments/components/create-segment-modal.tsx b/apps/web/modules/ee/contacts/segments/components/create-segment-modal.tsx index b6f747b4d758..87a79f0fc30f 100644 --- a/apps/web/modules/ee/contacts/segments/components/create-segment-modal.tsx +++ b/apps/web/modules/ee/contacts/segments/components/create-segment-modal.tsx @@ -209,7 +209,6 @@ export function CreateSegmentModal({ void; currentSegment: TSegmentWithSurveyRefs; @@ -33,7 +32,6 @@ const SegmentSettingsTab = ({ activitySummary, contactAttributeKeys, currentSegment, - workspaceId, isContactsEnabled, isReadOnly, segments, @@ -43,7 +41,6 @@ const SegmentSettingsTab = ({ | "activitySummary" | "contactAttributeKeys" | "currentSegment" - | "workspaceId" | "isContactsEnabled" | "isReadOnly" | "segments" @@ -57,7 +54,6 @@ const SegmentSettingsTab = ({ { @@ -167,7 +164,6 @@ export function SegmentEditor({
{ updateValueInLocalSurvey(resource.id, newValue); @@ -855,7 +853,6 @@ function DeviceFilter({ export function SegmentFilter({ resource, connector, - workspaceId, segment, segments, contactAttributeKeys, @@ -900,7 +897,6 @@ export function SegmentFilter({ void; initialSegment: TSegmentWithSurveyRefs; segments: TSegment[]; @@ -31,7 +30,6 @@ interface TSegmentSettingsTabProps { export function SegmentSettings({ activitySummary, - workspaceId, initialSegment, setOpen, contactAttributeKeys, @@ -205,7 +203,6 @@ export function SegmentSettings({ { const { i18n } = useTranslation(); - const { createdAt, workspaceId, id, surveys, title, updatedAt, description } = currentSegment; + const { createdAt, id, surveys, title, updatedAt, description } = currentSegment; const [isEditSegmentModalOpen, setIsEditSegmentModalOpen] = useState(false); const locale = i18n.resolvedLanguage ?? i18n.language ?? "en-US"; @@ -68,7 +68,6 @@ export const SegmentTableDataRow = ({ !open && setEditingSegment(null)} currentSegment={editingSegment} diff --git a/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx b/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx index 6f6a61219145..b4ca46ea90c3 100644 --- a/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx +++ b/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx @@ -39,7 +39,6 @@ import { SegmentEditor } from "./segment-editor"; interface TargetingCardProps { localSurvey: TSurvey; setLocalSurvey: React.Dispatch>; - workspaceId: string; contactAttributeKeys: TContactAttributeKey[]; segments: TSegment[]; initialSegment?: TSegment; @@ -48,7 +47,6 @@ interface TargetingCardProps { export function TargetingCard({ localSurvey, setLocalSurvey, - workspaceId, contactAttributeKeys, segments, initialSegment, @@ -222,7 +220,6 @@ export function TargetingCard({
(null); - const [connectorsToPauseCount, setConnectorsToPauseCount] = useState(0); + const [feedbackSourcesToPauseCount, setFeedbackSourcesToPauseCount] = useState(0); const [confirmAddDialogOpen, setConfirmAddDialogOpen] = useState(false); const [pendingAddData, setPendingAddData] = useState(null); @@ -148,7 +148,7 @@ export const FeedbackDirectorySettingsModal = ({ const closeModal = () => { setConfirmPauseDialogOpen(false); setPendingSubmitData(null); - setConnectorsToPauseCount(0); + setFeedbackSourcesToPauseCount(0); setConfirmAddDialogOpen(false); setPendingAddData(null); setAddedWorkspaceIds([]); @@ -158,14 +158,14 @@ export const FeedbackDirectorySettingsModal = ({ const submitDirectory = async ( data: TFeedbackDirectoryUpdateInput, - pauseConnectorsInRemovedWorkspaces: boolean + pauseFeedbackSourcesInRemovedWorkspaces: boolean ) => { const response = isEdit && directory ? await updateFeedbackDirectoryAction({ directoryId: directory.id, data: { name: data.name, workspaceIds: data.workspaceIds }, - pauseConnectorsInRemovedWorkspaces, + pauseFeedbackSourcesInRemovedWorkspaces, }) : await createFeedbackDirectoryAction({ organizationId, @@ -198,7 +198,7 @@ export const FeedbackDirectorySettingsModal = ({ if (wasSuccessful) { setConfirmPauseDialogOpen(false); setPendingSubmitData(null); - setConnectorsToPauseCount(0); + setFeedbackSourcesToPauseCount(0); } }; @@ -214,13 +214,13 @@ export const FeedbackDirectorySettingsModal = ({ ); if (removedWorkspaceIds.length > 0) { - const affectedConnectors = directory.connectors.filter((connector) => - removedWorkspaceIds.includes(connector.workspaceId) + const affectedFeedbackSources = directory.feedbackSources.filter((feedbackSource) => + removedWorkspaceIds.includes(feedbackSource.workspaceId) ); - if (affectedConnectors.length > 0) { + if (affectedFeedbackSources.length > 0) { setPendingSubmitData(data); - setConnectorsToPauseCount(affectedConnectors.length); + setFeedbackSourcesToPauseCount(affectedFeedbackSources.length); setConfirmPauseDialogOpen(true); return; } @@ -343,17 +343,17 @@ export const FeedbackDirectorySettingsModal = ({ {isEdit && (
- {t("workspace.unify.connectors")} + {t("workspace.unify.sources")} - {t("workspace.settings.feedback_directories.connectors_description")} + {t("workspace.settings.feedback_directories.feedback_sources_description")} - {directory.connectors.length === 0 ? ( + {directory.feedbackSources.length === 0 ? (

- {t("workspace.settings.feedback_directories.no_connectors")} + {t("workspace.settings.feedback_directories.no_feedback_sources")}

) : (
    - {directory.connectors.map((c) => ( + {directory.feedbackSources.map((c) => (
  • @@ -470,15 +470,18 @@ export const FeedbackDirectorySettingsModal = ({
    - {t("workspace.settings.feedback_directories.pause_connectors_confirmation_title")} + {t("workspace.settings.feedback_directories.pause_feedback_sources_confirmation_title")}

    - {t("workspace.settings.feedback_directories.pause_connectors_confirmation_description", { - count: connectorsToPauseCount, - })} + {t( + "workspace.settings.feedback_directories.pause_feedback_sources_confirmation_description", + { + count: feedbackSourcesToPauseCount, + } + )}

    @@ -487,7 +490,7 @@ export const FeedbackDirectorySettingsModal = ({ onClick={() => { setConfirmPauseDialogOpen(false); setPendingSubmitData(null); - setConnectorsToPauseCount(0); + setFeedbackSourcesToPauseCount(0); }} disabled={isSubmitting}> {t("common.cancel")} diff --git a/apps/web/modules/ee/feedback-directory/lib/feedback-directory.test.ts b/apps/web/modules/ee/feedback-directory/lib/feedback-directory.test.ts index a13f76e86456..f4bf7a31059a 100644 --- a/apps/web/modules/ee/feedback-directory/lib/feedback-directory.test.ts +++ b/apps/web/modules/ee/feedback-directory/lib/feedback-directory.test.ts @@ -34,7 +34,7 @@ vi.mock("@formbricks/database", () => { workspace: { count: vi.fn(), }, - connector: { + feedbackSource: { count: vi.fn().mockResolvedValue(0), updateMany: vi.fn().mockResolvedValue({ count: 0 }), }, @@ -52,7 +52,7 @@ const mockDirectoryDbRow = { id: mockDirectoryId, name: "Test Directory", isArchived: false, - _count: { workspaces: 2, connectors: 1 }, + _count: { workspaces: 2, feedbackSources: 1 }, }; const mockDirectoryDetailsDbRow = { @@ -64,7 +64,7 @@ const mockDirectoryDetailsDbRow = { { workspaceId: mockWorkspaceId1, workspace: { name: "Workspace A" } }, { workspaceId: mockWorkspaceId2, workspace: { name: "Workspace B" } }, ], - connectors: [], + feedbackSources: [], }; describe("FeedbackDirectory Service", () => { @@ -84,7 +84,7 @@ describe("FeedbackDirectory Service", () => { name: "Test Directory", isArchived: false, workspaceCount: 2, - connectorCount: 1, + feedbackSourceCount: 1, }, ]); expect(prisma.feedbackDirectory.findMany).toHaveBeenCalledWith({ @@ -93,7 +93,7 @@ describe("FeedbackDirectory Service", () => { id: true, name: true, isArchived: true, - _count: { select: { workspaces: true, connectors: true } }, + _count: { select: { workspaces: true, feedbackSources: true } }, }, orderBy: { createdAt: "desc" }, }); @@ -140,14 +140,14 @@ describe("FeedbackDirectory Service", () => { { workspaceId: mockWorkspaceId1, workspaceName: "Workspace A" }, { workspaceId: mockWorkspaceId2, workspaceName: "Workspace B" }, ], - connectors: [], + feedbackSources: [], }); }); - test("returns directory details with connectors", async () => { + test("returns directory details with feedbackSources", async () => { const dbRowWithConnectors = { ...mockDirectoryDetailsDbRow, - connectors: [ + feedbackSources: [ { id: "conn-1", name: "My Connector", @@ -161,7 +161,7 @@ describe("FeedbackDirectory Service", () => { const result = await getFeedbackDirectoryDetails(mockDirectoryId); - expect(result?.connectors).toEqual([ + expect(result?.feedbackSources).toEqual([ { id: "conn-1", name: "My Connector", @@ -375,8 +375,8 @@ describe("FeedbackDirectory Service", () => { }); }); - test("archives directory when no connectors linked", async () => { - vi.mocked(prisma.connector.count).mockResolvedValueOnce(0); + test("archives directory when no feedbackSources linked", async () => { + vi.mocked(prisma.feedbackSource.count).mockResolvedValueOnce(0); vi.mocked(prisma.feedbackDirectory.update).mockResolvedValueOnce({} as any); const result = await updateFeedbackDirectory(mockDirectoryId, mockOrganizationId, { @@ -384,7 +384,7 @@ describe("FeedbackDirectory Service", () => { }); expect(result).toBe(true); - expect(prisma.connector.count).toHaveBeenCalledWith({ + expect(prisma.feedbackSource.count).toHaveBeenCalledWith({ where: { feedbackDirectoryId: mockDirectoryId }, }); expect(prisma.feedbackDirectory.update).toHaveBeenCalledWith({ @@ -393,12 +393,12 @@ describe("FeedbackDirectory Service", () => { }); }); - test("throws InvalidInputError when archiving directory with connectors", async () => { - vi.mocked(prisma.connector.count).mockResolvedValueOnce(2); + test("throws InvalidInputError when archiving directory with feedbackSources", async () => { + vi.mocked(prisma.feedbackSource.count).mockResolvedValueOnce(2); await expect( updateFeedbackDirectory(mockDirectoryId, mockOrganizationId, { isArchived: true }) - ).rejects.toThrow(new InvalidInputError("DIRECTORY_HAS_CONNECTORS")); + ).rejects.toThrow(new InvalidInputError("DIRECTORY_HAS_FEEDBACK_SOURCES")); }); test("unarchives directory", async () => { @@ -473,7 +473,7 @@ describe("FeedbackDirectory Service", () => { }); }); - test("pauses connectors in removed workspaces when requested", async () => { + test("pauses feedbackSources in removed workspaces when requested", async () => { vi.mocked(prisma.feedbackDirectory.findUnique).mockResolvedValueOnce(mockDirectoryDetailsDbRow as any); vi.mocked(prisma.workspace.count).mockResolvedValueOnce(1); vi.mocked(prisma.feedbackDirectory.update).mockResolvedValueOnce({} as any); @@ -484,11 +484,11 @@ describe("FeedbackDirectory Service", () => { { workspaceIds: [mockWorkspaceId1], }, - { pauseConnectorsInRemovedWorkspaces: true } + { pauseFeedbackSourcesInRemovedWorkspaces: true } ); expect(result).toBe(true); - expect(prisma.connector.updateMany).toHaveBeenCalledWith({ + expect(prisma.feedbackSource.updateMany).toHaveBeenCalledWith({ where: { feedbackDirectoryId: mockDirectoryId, workspaceId: { in: [mockWorkspaceId2] }, diff --git a/apps/web/modules/ee/feedback-directory/lib/feedback-directory.ts b/apps/web/modules/ee/feedback-directory/lib/feedback-directory.ts index 79b266620b06..f479110e5b06 100644 --- a/apps/web/modules/ee/feedback-directory/lib/feedback-directory.ts +++ b/apps/web/modules/ee/feedback-directory/lib/feedback-directory.ts @@ -17,7 +17,7 @@ import { type FeedbackDirectoryPrismaClient = Pick< PrismaClient, - "connector" | "feedbackDirectory" | "feedbackDirectoryWorkspace" | "workspace" + "feedbackSource" | "feedbackDirectory" | "feedbackDirectoryWorkspace" | "workspace" >; /** @@ -44,7 +44,7 @@ export const getFeedbackDirectories = reactCache( _count: { select: { workspaces: true, - connectors: true, + feedbackSources: true, }, }, }, @@ -58,7 +58,7 @@ export const getFeedbackDirectories = reactCache( name: dir.name, isArchived: dir.isArchived, workspaceCount: dir._count.workspaces, - connectorCount: dir._count.connectors, + feedbackSourceCount: dir._count.feedbackSources, })); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { @@ -80,7 +80,7 @@ export const getFeedbackDirectories = reactCache( */ /** * Lists feedback directories assigned to a workspace. - * Used by connector creation to pick a feedback directory. + * Used by feedbackSource creation to pick a feedback directory. */ export const getFeedbackDirectoriesByWorkspaceId = reactCache( async (workspaceId: string): Promise<{ id: string; name: string }[]> => { @@ -197,7 +197,7 @@ const mapFeedbackDirectoryDetails = (directory: { isArchived: boolean; organizationId: string; workspaces: { workspaceId: string; workspace: { name: string } }[]; - connectors: { + feedbackSources: { id: string; name: string; type: string; @@ -213,7 +213,7 @@ const mapFeedbackDirectoryDetails = (directory: { workspaceId: dp.workspaceId, workspaceName: dp.workspace.name, })), - connectors: directory.connectors.map((c) => ({ + feedbackSources: directory.feedbackSources.map((c) => ({ id: c.id, name: c.name, type: c.type, @@ -267,7 +267,7 @@ export const getFeedbackDirectoryDetails = reactCache( }, }, }, - connectors: { + feedbackSources: { select: { id: true, name: true, @@ -407,7 +407,7 @@ const buildWorkspaceAssignmentPayload = async ( }; interface UpdateFeedbackDirectoryOptions { - pauseConnectorsInRemovedWorkspaces?: boolean; + pauseFeedbackSourcesInRemovedWorkspaces?: boolean; } const getArchiveUpdate = async ( @@ -416,11 +416,11 @@ const getArchiveUpdate = async ( isArchived: boolean | undefined ): Promise> => { if (isArchived === true) { - const connectorCount = await prismaClient.connector.count({ + const feedbackSourceCount = await prismaClient.feedbackSource.count({ where: { feedbackDirectoryId: directoryId }, }); - if (connectorCount > 0) { - throw new InvalidInputError("DIRECTORY_HAS_CONNECTORS"); + if (feedbackSourceCount > 0) { + throw new InvalidInputError("DIRECTORY_HAS_FEEDBACK_SOURCES"); } return { isArchived: true }; } @@ -477,7 +477,7 @@ const pauseConnectorsInWorkspaces = async ( return; } - await tx.connector.updateMany({ + await tx.feedbackSource.updateMany({ where: { feedbackDirectoryId: directoryId, workspaceId: { in: workspaceIds }, @@ -573,7 +573,7 @@ export const updateFeedbackDirectory = async ( data: payload, }); - if (options?.pauseConnectorsInRemovedWorkspaces) { + if (options?.pauseFeedbackSourcesInRemovedWorkspaces) { await pauseConnectorsInWorkspaces(tx, directoryId, workspaceAssignmentUpdate.removedWorkspaceIds); } }, diff --git a/apps/web/modules/ee/feedback-directory/types/feedback-directory.ts b/apps/web/modules/ee/feedback-directory/types/feedback-directory.ts index 200ee6d17bea..b278c0cb599f 100644 --- a/apps/web/modules/ee/feedback-directory/types/feedback-directory.ts +++ b/apps/web/modules/ee/feedback-directory/types/feedback-directory.ts @@ -6,7 +6,7 @@ export const ZFeedbackDirectory = z.object({ name: z.string(), isArchived: z.boolean(), workspaceCount: z.number(), - connectorCount: z.number(), + feedbackSourceCount: z.number(), }); export type TFeedbackDirectory = z.infer; @@ -22,7 +22,7 @@ export const ZFeedbackDirectoryDetails = z.object({ workspaceName: z.string(), }) ), - connectors: z.array( + feedbackSources: z.array( z.object({ id: ZId, name: z.string(), @@ -71,8 +71,8 @@ export const getTranslatedFeedbackDirectoryError = ( return t("workspace.settings.feedback_directories.error_directory_name_duplicate"); case "DIRECTORY_WORKSPACES_INVALID_ORG": return t("workspace.settings.feedback_directories.error_directory_workspaces_invalid_org"); - case "DIRECTORY_HAS_CONNECTORS": - return t("workspace.settings.feedback_directories.error_directory_has_connectors"); + case "DIRECTORY_HAS_FEEDBACK_SOURCES": + return t("workspace.settings.feedback_directories.error_directory_has_feedback_sources"); case "WORKSPACE_ALREADY_ASSIGNED_TO_DIFFERENT_DIRECTORY": return t("workspace.settings.feedback_directories.error_workspace_already_assigned"); default: diff --git a/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx b/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx index 1624b06b171a..a616cfd6becc 100644 --- a/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx +++ b/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import type { TConnectorFieldMapping } from "@formbricks/types/connector"; +import type { TFeedbackSourceFieldMapping } from "@formbricks/types/feedback-source"; import type { FeedbackRecordData } from "@/modules/hub/types"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; @@ -13,7 +13,7 @@ interface FeedbackRecordsPageClientProps { initialRecords: FeedbackRecordData[]; initialCursors: Record; frdMap: Record; - csvSources: { id: string; name: string; fieldMappings: TConnectorFieldMapping[] }[]; + csvSources: { id: string; name: string; fieldMappings: TFeedbackSourceFieldMapping[] }[]; canWrite: boolean; } diff --git a/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx b/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx index f085835712d8..8af5f4daa21b 100644 --- a/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx +++ b/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx @@ -15,8 +15,8 @@ import Link from "next/link"; import { useMemo, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import type { TConnectorFieldMapping } from "@formbricks/types/connector"; -import { listFeedbackRecordsAction } from "@/lib/connector/actions"; +import type { TFeedbackSourceFieldMapping } from "@formbricks/types/feedback-source"; +import { listFeedbackRecordsAction } from "@/lib/feedback-source/actions"; import { formatDateForDisplay, formatDateTimeForDisplay } from "@/lib/utils/datetime"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import type { FeedbackRecordData } from "@/modules/hub/types"; @@ -70,7 +70,7 @@ interface FeedbackRecordsTableProps { initialRecords: FeedbackRecordData[]; initialCursors: Record; frdMap: Record; - csvSources: { id: string; name: string; fieldMappings: TConnectorFieldMapping[] }[]; + csvSources: { id: string; name: string; fieldMappings: TFeedbackSourceFieldMapping[] }[]; canWrite: boolean; } @@ -94,7 +94,7 @@ export const FeedbackRecordsTable = ({ const [csvImportSource, setCsvImportSource] = useState<{ id: string; name: string; - fieldMappings: TConnectorFieldMapping[]; + fieldMappings: TFeedbackSourceFieldMapping[]; } | null>(null); const [selectedIds, setSelectedIds] = useState>(() => new Set()); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); @@ -465,7 +465,7 @@ export const FeedbackRecordsTable = ({ setCsvImportSource(null); } }} - connectorId={csvImportSource.id} + feedbackSourceId={csvImportSource.id} workspaceId={workspaceId} fieldMappings={csvImportSource.fieldMappings} /> diff --git a/apps/web/modules/ee/unify-feedback/page.tsx b/apps/web/modules/ee/unify-feedback/page.tsx index 429be1fe6630..93bf8f9dff73 100644 --- a/apps/web/modules/ee/unify-feedback/page.tsx +++ b/apps/web/modules/ee/unify-feedback/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; -import { getConnectorsWithMappings } from "@/lib/connector/service"; import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getFeedbackSourcesWithMappings } from "@/lib/feedback-source/service"; import { getTranslate } from "@/lingodotdev/server"; import { NoFeedbackDirectoryEmptyState } from "@/modules/ee/feedback-directory/components/no-feedback-directory-empty-state"; import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory"; @@ -64,9 +64,9 @@ export default async function UnifyFeedbackRecordsPage( ); } - const [frds, connectors] = await Promise.all([ + const [frds, feedbackSources] = await Promise.all([ getFeedbackDirectoriesByWorkspaceId(params.workspaceId), - getConnectorsWithMappings(params.workspaceId), + getFeedbackSourcesWithMappings(params.workspaceId), ]); if (frds.length === 0) { @@ -104,12 +104,12 @@ export default async function UnifyFeedbackRecordsPage( } const frdMap = Object.fromEntries(frds.map((f) => [f.id, f.name])); - const csvSources = connectors - .filter((connector) => connector.type === "csv") - .map((connector) => ({ - id: connector.id, - name: connector.name, - fieldMappings: connector.fieldMappings, + const csvSources = feedbackSources + .filter((feedbackSource) => feedbackSource.type === "csv") + .map((feedbackSource) => ({ + id: feedbackSource.id, + name: feedbackSource.name, + fieldMappings: feedbackSource.fieldMappings, })); return ( diff --git a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-data-row.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-data-row.tsx deleted file mode 100644 index 32187e40edac..000000000000 --- a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-data-row.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client"; - -import { useTranslation } from "react-i18next"; -import { TConnectorStatus, TConnectorType, TConnectorWithMappings } from "@formbricks/types/connector"; -import { Badge } from "@/modules/ui/components/badge"; -import { getConnectorIcon, getConnectorTypeLabelKey } from "./connector-display"; -import { ConnectorRowDropdown } from "./connector-row-dropdown"; - -const RELATIVE_TIME_DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [ - { amount: 60, unit: "seconds" }, - { amount: 60, unit: "minutes" }, - { amount: 24, unit: "hours" }, - { amount: 7, unit: "days" }, - { amount: 4.345, unit: "weeks" }, - { amount: 12, unit: "months" }, - { amount: Number.POSITIVE_INFINITY, unit: "years" }, -]; - -function getRelativeTime(date: Date, locale: string) { - const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); - let duration = (date.getTime() - Date.now()) / 1000; - - for (const division of RELATIVE_TIME_DIVISIONS) { - if (Math.abs(duration) < division.amount) { - return formatter.format(Math.round(duration), division.unit); - } - duration /= division.amount; - } - - return formatter.format(Math.round(duration), "years"); -} - -interface ConnectorsTableDataRowProps { - connector: TConnectorWithMappings; - onEdit: () => void; - onCsvImport?: () => void; - onDuplicate: () => Promise; - onToggleStatus: () => Promise; - onDelete: () => Promise; - isReadOnly?: boolean; -} - -const STATUS_BADGE_TYPE: Record = { - active: "success", - paused: "warning", - error: "error", -}; - -export function ConnectorsTableDataRow({ - connector, - onEdit, - onCsvImport, - onDuplicate, - onToggleStatus, - onDelete, - isReadOnly = false, -}: Readonly) { - const { t, i18n } = useTranslation(); - const handleRowClick = () => { - if (!isReadOnly && connector.type === "csv" && onCsvImport) { - onCsvImport(); - return; - } - - onEdit(); - }; - - const getStatusLabel = (s: TConnectorStatus, connectorType: TConnectorType) => { - switch (s) { - case "active": - if (connectorType === "csv") { - return t("workspace.unify.status_ready"); - } - return t("workspace.unify.status_live_sync"); - case "paused": - return t("common.disabled"); - case "error": - return t("workspace.unify.status_error"); - } - }; - - return ( -
    { - if (e.key === "Enter" || e.key === " ") { - handleRowClick(); - } - }}> -
    - {getConnectorIcon(connector.type, "h-4 w-4 text-slate-500")} -
    -
    - {connector.name} -
    -
    - -
    -
    - {getRelativeTime(connector.updatedAt, i18n.language)} -
    -
    - {connector.creatorName ?? "—"} -
    -
    - {!isReadOnly && ( - - )} -
    -
    - ); -} diff --git a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-rows-container.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-rows-container.tsx deleted file mode 100644 index d2ded4e6d85a..000000000000 --- a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-rows-container.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; -import { ConnectorsTableDataRow } from "./connectors-table-data-row"; - -interface ConnectorsTableRowsContainerProps { - connectors: TConnectorWithMappings[]; - onConnectorClick: (connector: TConnectorWithMappings) => void; - onCsvImport: (connector: TConnectorWithMappings) => void; - onDuplicate: (connector: TConnectorWithMappings) => Promise; - onToggleStatus: (connector: TConnectorWithMappings) => Promise; - onDelete: (connectorId: string) => Promise; - isReadOnly?: boolean; -} - -export const ConnectorsTableRowsContainer = ({ - connectors, - onConnectorClick, - onCsvImport, - onDuplicate, - onToggleStatus, - onDelete, - isReadOnly = false, -}: ConnectorsTableRowsContainerProps) => { - const { t } = useTranslation(); - - if (connectors.length === 0) { - return ( -
    -

    {t("workspace.unify.no_sources_connected")}

    -
    - ); - } - - return ( -
    - {connectors.map((connector) => ( - onConnectorClick(connector)} - onCsvImport={connector.type === "csv" ? () => onCsvImport(connector) : undefined} - onDuplicate={() => onDuplicate(connector)} - onToggleStatus={() => onToggleStatus(connector)} - onDelete={() => onDelete(connector.id)} - isReadOnly={isReadOnly} - /> - ))} -
    - ); -}; diff --git a/apps/web/modules/ee/unify-feedback/sources/components/create-connector-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/create-feedback-source-modal.tsx similarity index 82% rename from apps/web/modules/ee/unify-feedback/sources/components/create-connector-modal.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/create-feedback-source-modal.tsx index 35137901e304..26569bfd0ca7 100644 --- a/apps/web/modules/ee/unify-feedback/sources/components/create-connector-modal.tsx +++ b/apps/web/modules/ee/unify-feedback/sources/components/create-feedback-source-modal.tsx @@ -7,12 +7,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { TConnectorType, UNSUPPORTED_CONNECTOR_ELEMENT_TYPES } from "@formbricks/types/connector"; +import { + TFeedbackSourceType, + UNSUPPORTED_FEEDBACK_SOURCE_ELEMENT_TYPES, +} from "@formbricks/types/feedback-source"; import { getResponseCountAction, importCsvDataAction, importHistoricalResponsesAction, -} from "@/lib/connector/actions"; +} from "@/lib/feedback-source/actions"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Alert } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; @@ -46,36 +49,36 @@ import { Switch } from "@/modules/ui/components/switch"; import { CSV_HIDDEN_STATIC_MAPPINGS, CSV_PROTECTED_TARGET_IDS, - TCreateConnectorStep, + TCreateFeedbackSourceStep, TFieldMapping, - TFormbricksConnectorForm, + TFormbricksFeedbackSourceForm, TSourceField, TUnifySurvey, - ZFormbricksConnectorForm, - getTranslatedConnectorError, + ZFormbricksFeedbackSourceForm, + getTranslatedFeedbackSourceError, } from "../types"; import { - TConnectorOptionId, TEnumValidationError, + TFeedbackSourceOptionId, areAllRequiredCsvFieldsMapped, - isConnectorNameValid, + isFeedbackSourceNameValid, toggleQuestionId, validateEnumMappings, } from "../utils"; -import { ConnectorTypeSelector } from "./connector-type-selector"; -import { CsvConnectorUI } from "./csv-connector-ui"; +import { CsvFeedbackSourceUI } from "./csv-feedback-source-ui"; +import { FeedbackSourceTypeSelector } from "./feedback-source-type-selector"; import { FormbricksQuestionList } from "./formbricks-question-list"; const API_INGESTION_DOCS_URL = "https://formbricks.com/docs/unify-feedback/api/rest-api"; const FEEDBACK_RECORD_MCP_DOCS_URL = "https://formbricks.com/docs/unify-feedback/api/mcp"; -interface CreateConnectorModalProps { +interface CreateFeedbackSourceModalProps { open: boolean; onOpenChange: (open: boolean) => void; showTrigger?: boolean; - onCreateConnector: (data: { + onCreateFeedbackSource: (data: { name: string; - type: TConnectorType; + type: TFeedbackSourceType; feedbackDirectoryId: string; surveyMappings?: { surveyId: string; elementIds: string[] }[]; fieldMappings?: TFieldMapping[]; @@ -86,8 +89,8 @@ interface CreateConnectorModalProps { } const getDialogTitle = ( - step: TCreateConnectorStep, - type: TConnectorOptionId | null, + step: TCreateFeedbackSourceStep, + type: TFeedbackSourceOptionId | null, t: (key: string) => string ): string => { if (step === "selectType") return t("workspace.unify.add_feedback_source"); @@ -97,8 +100,8 @@ const getDialogTitle = ( }; const getDialogDescription = ( - step: TCreateConnectorStep, - type: TConnectorOptionId | null, + step: TCreateFeedbackSourceStep, + type: TFeedbackSourceOptionId | null, t: (key: string) => string ): string => { if (step === "selectType") return t("workspace.unify.select_source_type_description"); @@ -107,7 +110,7 @@ const getDialogDescription = ( return t("workspace.unify.configure_mapping"); }; -const getNextStepButtonLabel = (type: TConnectorOptionId | null, t: (key: string) => string): string => { +const getNextStepButtonLabel = (type: TFeedbackSourceOptionId | null, t: (key: string) => string): string => { if (type === "formbricks_survey") return t("workspace.unify.select_questions"); if (type === "csv") return t("workspace.unify.configure_import"); if (type === "api_ingestion") return t("common.learn_more"); @@ -117,34 +120,36 @@ const getNextStepButtonLabel = (type: TConnectorOptionId | null, t: (key: string const getSelectableQuestionIds = (survey: TUnifySurvey): string[] => survey.elements - .filter((element) => !(UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(element.type)) + .filter( + (element) => !(UNSUPPORTED_FEEDBACK_SOURCE_ELEMENT_TYPES as readonly string[]).includes(element.type) + ) .map((element) => element.id); type TImportState = "success" | "error" | "skipped"; -export const CreateConnectorModal = ({ +export const CreateFeedbackSourceModal = ({ open, onOpenChange, showTrigger = true, - onCreateConnector, + onCreateFeedbackSource, surveys, workspaceId, directories, -}: CreateConnectorModalProps) => { +}: CreateFeedbackSourceModalProps) => { const { t } = useTranslation(); - const defaultConnectorName = useMemo>( + const defaultFeedbackSourceName = useMemo>( () => ({ - formbricks_survey: t("workspace.unify.default_connector_name_formbricks"), - csv: t("workspace.unify.default_connector_name_csv"), + formbricks_survey: t("workspace.unify.default_source_name_formbricks"), + csv: t("workspace.unify.default_source_name_csv"), }), [t] ); - const formbricksForm = useForm({ - resolver: zodResolver(ZFormbricksConnectorForm), + const formbricksForm = useForm({ + resolver: zodResolver(ZFormbricksFeedbackSourceForm), defaultValues: { - sourceName: defaultConnectorName.formbricks_survey, + sourceName: defaultFeedbackSourceName.formbricks_survey, surveyId: "", selectedQuestionIds: [], importHistorical: true, @@ -152,18 +157,18 @@ export const CreateConnectorModal = ({ mode: "onChange", }); - const [currentStep, setCurrentStep] = useState("selectType"); - const [selectedType, setSelectedType] = useState(null); + const [currentStep, setCurrentStep] = useState("selectType"); + const [selectedType, setSelectedType] = useState(null); const [mappings, setMappings] = useState([]); const [sourceFields, setSourceFields] = useState([]); const [csvParsedData, setCsvParsedData] = useState[]>([]); const [enumValidationErrors, setEnumValidationErrors] = useState([]); - const [csvConnectorName, setCsvConnectorName] = useState(""); + const [csvFeedbackSourceName, setCsvFeedbackSourceName] = useState(""); const [responseCountBySurvey, setResponseCountBySurvey] = useState>({}); const [isImporting, setIsImporting] = useState(false); const [isCreating, setIsCreating] = useState(false); const [selectedDirectoryId, setSelectedDirectoryId] = useState(directories[0]?.id ?? null); - const userEditedConnectorNameRef = useRef(false); + const userEditedFeedbackSourceNameRef = useRef(false); const formbricksValues = formbricksForm.watch(); const selectedSurveyId = formbricksValues.surveyId; @@ -236,7 +241,7 @@ export const CreateConnectorModal = ({ setCurrentStep("selectType"); setSelectedType(null); formbricksForm.reset({ - sourceName: defaultConnectorName.formbricks_survey, + sourceName: defaultFeedbackSourceName.formbricks_survey, surveyId: "", selectedQuestionIds: [], importHistorical: true, @@ -246,8 +251,8 @@ export const CreateConnectorModal = ({ setCsvParsedData([]); setEnumValidationErrors([]); setResponseCountBySurvey({}); - setCsvConnectorName(""); - userEditedConnectorNameRef.current = false; + setCsvFeedbackSourceName(""); + userEditedFeedbackSourceNameRef.current = false; setIsImporting(false); setIsCreating(false); setSelectedDirectoryId(directories[0]?.id ?? null); @@ -274,7 +279,7 @@ export const CreateConnectorModal = ({ if (selectedType === "formbricks_survey") { formbricksForm.reset({ - sourceName: defaultConnectorName.formbricks_survey, + sourceName: defaultFeedbackSourceName.formbricks_survey, surveyId: "", selectedQuestionIds: [], importHistorical: true, @@ -282,7 +287,7 @@ export const CreateConnectorModal = ({ } if (selectedType === "csv") { - setCsvConnectorName(defaultConnectorName.csv); + setCsvFeedbackSourceName(defaultFeedbackSourceName.csv); } setCurrentStep("mapping"); @@ -297,12 +302,15 @@ export const CreateConnectorModal = ({ } }; - const handleHistoricalImport = async (connectorId: string, surveyId: string): Promise => { + const handleHistoricalImport = async ( + feedbackSourceId: string, + surveyId: string + ): Promise => { const responseCount = responseCountBySurvey[surveyId] ?? 0; if (responseCount <= 0) return "skipped"; setIsImporting(true); const importResult = await importHistoricalResponsesAction({ - connectorId, + feedbackSourceId, workspaceId, surveyId, }); @@ -323,10 +331,10 @@ export const CreateConnectorModal = ({ } }; - const handleCsvImport = async (connectorId: string): Promise => { + const handleCsvImport = async (feedbackSourceId: string): Promise => { setIsImporting(true); const importResult = await importCsvDataAction({ - connectorId, + feedbackSourceId, workspaceId, csvData: csvParsedData, }); @@ -342,7 +350,7 @@ export const CreateConnectorModal = ({ ); return "success"; } else { - toast.error(getTranslatedConnectorError(getFormattedErrorMessage(importResult), t)); + toast.error(getTranslatedFeedbackSourceError(getFormattedErrorMessage(importResult), t)); return "error"; } }; @@ -355,27 +363,27 @@ export const CreateConnectorModal = ({ }); }; - const handleCreateFormbricksConnector = async (values: TFormbricksConnectorForm) => { + const handleCreateFormbricksFeedbackSource = async (values: TFormbricksFeedbackSourceForm) => { if (!selectedDirectoryId) return; setIsCreating(true); - const connectorId = await onCreateConnector({ + const feedbackSourceId = await onCreateFeedbackSource({ name: values.sourceName.trim(), type: "formbricks_survey", feedbackDirectoryId: selectedDirectoryId, surveyMappings: [{ surveyId: values.surveyId, elementIds: values.selectedQuestionIds }], }); - if (!connectorId) { + if (!feedbackSourceId) { setIsCreating(false); return; } const importState = values.importHistorical - ? await handleHistoricalImport(connectorId, values.surveyId) + ? await handleHistoricalImport(feedbackSourceId, values.surveyId) : "skipped"; if (importState === "skipped") { - showFeedbackRecordsSuccessToast(t("workspace.unify.connector_created_successfully")); + showFeedbackRecordsSuccessToast(t("workspace.unify.source_created_successfully")); } setIsCreating(false); @@ -383,8 +391,8 @@ export const CreateConnectorModal = ({ onOpenChange(false); }; - const handleCreateCsvConnector = async () => { - if (!selectedDirectoryId || !isConnectorNameValid(csvConnectorName)) return; + const handleCreateCsvFeedbackSource = async () => { + if (!selectedDirectoryId || !isFeedbackSourceNameValid(csvFeedbackSourceName)) return; const requiredCheck = areAllRequiredCsvFieldsMapped(mappings); if (!requiredCheck.valid) { @@ -411,21 +419,21 @@ export const CreateConnectorModal = ({ ); const fieldMappings = [...userMappings, ...CSV_HIDDEN_STATIC_MAPPINGS]; - const connectorId = await onCreateConnector({ - name: csvConnectorName.trim(), + const feedbackSourceId = await onCreateFeedbackSource({ + name: csvFeedbackSourceName.trim(), type: "csv", feedbackDirectoryId: selectedDirectoryId, fieldMappings, }); - if (!connectorId) { + if (!feedbackSourceId) { setIsCreating(false); return; } - const importState = csvParsedData.length > 0 ? await handleCsvImport(connectorId) : "skipped"; + const importState = csvParsedData.length > 0 ? await handleCsvImport(feedbackSourceId) : "skipped"; if (importState === "skipped") { - showFeedbackRecordsSuccessToast(t("workspace.unify.connector_created_successfully")); + showFeedbackRecordsSuccessToast(t("workspace.unify.source_created_successfully")); } setIsCreating(false); @@ -436,14 +444,14 @@ export const CreateConnectorModal = ({ const isCsvValid = selectedType === "csv" && sourceFields.length > 0; const areCsvRequiredFieldsMapped = areAllRequiredCsvFieldsMapped(mappings).valid; - const handleSuggestConnectorName = (name: string) => { - if (userEditedConnectorNameRef.current) return; - setCsvConnectorName(name); + const handleSuggestFeedbackSourceName = (name: string) => { + if (userEditedFeedbackSourceNameRef.current) return; + setCsvFeedbackSourceName(name); }; - const handleCsvConnectorNameChange = (value: string) => { - userEditedConnectorNameRef.current = true; - setCsvConnectorName(value); + const handleCsvFeedbackSourceNameChange = (value: string) => { + userEditedFeedbackSourceNameRef.current = true; + setCsvFeedbackSourceName(value); }; return ( @@ -475,7 +483,7 @@ export const CreateConnectorModal = ({ {currentStep === "selectType" && ( -
    + onSubmit={formbricksForm.handleSubmit(handleCreateFormbricksFeedbackSource)}> {error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -530,7 +538,7 @@ export const CreateConnectorModal = ({ {error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -552,7 +560,7 @@ export const CreateConnectorModal = ({
{error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -584,20 +592,20 @@ export const CreateConnectorModal = ({ {currentStep === "mapping" && selectedType === "csv" && (
- + handleCsvConnectorNameChange(event.target.value)} + id="feedbackSourceName" + value={csvFeedbackSourceName} + onChange={(event) => handleCsvFeedbackSourceNameChange(event.target.value)} placeholder={t("workspace.unify.enter_name_for_source")} /> -

{t("workspace.unify.connector_name_hint")}

+

{t("workspace.unify.source_name_hint")}

{directories.length === 0 && }
- { @@ -606,7 +614,7 @@ export const CreateConnectorModal = ({ }} onSourceFieldsChange={setSourceFields} onParsedDataChange={setCsvParsedData} - onSuggestConnectorName={handleSuggestConnectorName} + onSuggestFeedbackSourceName={handleSuggestFeedbackSourceName} />
@@ -659,18 +667,20 @@ export const CreateConnectorModal = ({ {previewOpen && ( - <> -
- - - - {csvPreview[0]?.map((header, i) => ( - +
+
- {header} -
+ + + {csvPreview[0]?.map((header, i) => ( + + ))} + + + + {csvPreview.slice(1, 4).map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + ))} - - - {csvPreview.slice(1, 4).map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - ))} - - ))} - -
+ {header} +
+ {cell || } +
- {cell || } -
-
- + ))} + + +
)}
)} @@ -288,7 +281,7 @@ export function CsvConnectorUI({ sourceFields={sourceFields} mappings={mappings} onMappingsChange={handleUserMappingsChange} - connectorType="csv" + feedbackSourceType="csv" confidenceByTargetId={confidenceByTargetId} sampleRow={sampleRow} /> diff --git a/apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx index 7d15da566dd6..153388f645a6 100644 --- a/apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx +++ b/apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx @@ -5,13 +5,13 @@ import { ArrowUpFromLineIcon, Loader2Icon } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import type { TConnectorFieldMapping } from "@formbricks/types/connector"; -import { importCsvDataAction } from "@/lib/connector/actions"; +import type { TFeedbackSourceFieldMapping } from "@formbricks/types/feedback-source"; +import { importCsvDataAction } from "@/lib/feedback-source/actions"; import { formatCsvMissingMappedSourceColumns, getMissingCsvMappedSourceColumns, getMissingRequiredCsvSourceColumns, -} from "@/lib/connector/utils"; +} from "@/lib/feedback-source/utils"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Alert } from "@/modules/ui/components/alert"; import { Badge } from "@/modules/ui/components/badge"; @@ -24,25 +24,25 @@ import { DialogHeader, DialogTitle, } from "@/modules/ui/components/dialog"; -import { createFeedbackCSVDataSchema, getTranslatedConnectorError } from "../types"; +import { createFeedbackCSVDataSchema, getTranslatedFeedbackSourceError } from "../types"; import { validateCsvFile } from "../utils"; interface CsvImportModalProps { open: boolean; onOpenChange: (open: boolean) => void; - connectorId: string; + feedbackSourceId: string; workspaceId: string; - fieldMappings: TConnectorFieldMapping[]; - onOpenEditConnector?: () => void; + fieldMappings: TFeedbackSourceFieldMapping[]; + onOpenEditFeedbackSource?: () => void; } export function CsvImportModal({ open, onOpenChange, - connectorId, + feedbackSourceId, workspaceId, fieldMappings, - onOpenEditConnector, + onOpenEditFeedbackSource, }: CsvImportModalProps) { const { t } = useTranslation(); const [csvFile, setCsvFile] = useState(null); @@ -126,7 +126,7 @@ export function CsvImportModal({ if (parsedData.length === 0) return; setIsImporting(true); - const result = await importCsvDataAction({ connectorId, workspaceId, csvData: parsedData }); + const result = await importCsvDataAction({ feedbackSourceId, workspaceId, csvData: parsedData }); setIsImporting(false); if (result?.data) { @@ -142,7 +142,7 @@ export function CsvImportModal({ setRowCount(0); onOpenChange(false); } else { - toast.error(getTranslatedConnectorError(getFormattedErrorMessage(result), t)); + toast.error(getTranslatedFeedbackSourceError(getFormattedErrorMessage(result), t)); } }; @@ -208,12 +208,12 @@ export function CsvImportModal({
- {onOpenEditConnector && ( + {onOpenEditFeedbackSource && ( diff --git a/apps/web/modules/ee/unify-feedback/sources/components/edit-connector-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/edit-feedback-source-modal.tsx similarity index 75% rename from apps/web/modules/ee/unify-feedback/sources/components/edit-connector-modal.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/edit-feedback-source-modal.tsx index 9a90b8250bf1..d471c9bc2073 100644 --- a/apps/web/modules/ee/unify-feedback/sources/components/edit-connector-modal.tsx +++ b/apps/web/modules/ee/unify-feedback/sources/components/edit-feedback-source-modal.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; import { Button } from "@/modules/ui/components/button"; import { Dialog, @@ -37,28 +37,28 @@ import { CSV_PROTECTED_TARGET_IDS, SAMPLE_CSV_COLUMNS, TFieldMapping, - TFormbricksConnectorForm, + TFormbricksFeedbackSourceForm, TSourceField, TUnifySurvey, - ZFormbricksConnectorForm, - getTranslatedConnectorError, + ZFormbricksFeedbackSourceForm, + getTranslatedFeedbackSourceError, } from "../types"; import { areAllRequiredCsvFieldsMapped, - isConnectorNameValid, + isFeedbackSourceNameValid, parseCSVColumnsToFields, toggleQuestionId, } from "../utils"; -import { getConnectorIcon, getConnectorTypeLabelKey } from "./connector-display"; +import { getFeedbackSourceIcon, getFeedbackSourceTypeLabelKey } from "./feedback-source-display"; import { FormbricksQuestionList } from "./formbricks-question-list"; import { MappingUI } from "./mapping-ui"; -interface EditConnectorModalProps { - connector: TConnectorWithMappings | null; +interface EditFeedbackSourceModalProps { + feedbackSource: TFeedbackSourceWithMappings | null; open: boolean; onOpenChange: (open: boolean) => void; - onUpdateConnector: (data: { - connectorId: string; + onUpdateFeedbackSource: (data: { + feedbackSourceId: string; workspaceId: string; name: string; surveyMappings?: { surveyId: string; elementIds: string[] }[]; @@ -69,23 +69,23 @@ interface EditConnectorModalProps { isReadOnly?: boolean; } -export const EditConnectorModal = ({ - connector, +export const EditFeedbackSourceModal = ({ + feedbackSource, open, onOpenChange, - onUpdateConnector, + onUpdateFeedbackSource, surveys, onOpenCsvImport, isReadOnly = false, -}: EditConnectorModalProps) => { +}: EditFeedbackSourceModalProps) => { const { t } = useTranslation(); - const [csvConnectorName, setCsvConnectorName] = useState(""); + const [csvFeedbackSourceName, setCsvFeedbackSourceName] = useState(""); const [mappings, setMappings] = useState([]); const [sourceFields, setSourceFields] = useState([]); const [isUpdating, setIsUpdating] = useState(false); - const formbricksForm = useForm({ - resolver: zodResolver(ZFormbricksConnectorForm), + const formbricksForm = useForm({ + resolver: zodResolver(ZFormbricksFeedbackSourceForm), defaultValues: { sourceName: "", surveyId: "", @@ -104,26 +104,26 @@ export const EditConnectorModal = ({ ); useEffect(() => { - if (connector) { - if (connector.type === "formbricks_survey") { - const mappedSurveyId = connector.formbricksMappings[0]?.surveyId ?? ""; - const mappedQuestionIds = connector.formbricksMappings + if (feedbackSource) { + if (feedbackSource.type === "formbricks_survey") { + const mappedSurveyId = feedbackSource.formbricksMappings[0]?.surveyId ?? ""; + const mappedQuestionIds = feedbackSource.formbricksMappings .filter((mapping) => mapping.surveyId === mappedSurveyId) .map((mapping) => mapping.elementId); formbricksForm.reset({ - sourceName: connector.name, + sourceName: feedbackSource.name, surveyId: mappedSurveyId, selectedQuestionIds: mappedQuestionIds, importHistorical: true, }); - setCsvConnectorName(""); + setCsvFeedbackSourceName(""); setSourceFields([]); setMappings([]); - } else if (connector.type === "csv") { - setCsvConnectorName(connector.name); + } else if (feedbackSource.type === "csv") { + setCsvFeedbackSourceName(feedbackSource.name); const columnsFromMappings = [ - ...new Set(connector.fieldMappings.map((m) => m.sourceFieldId).filter(Boolean)), + ...new Set(feedbackSource.fieldMappings.map((m) => m.sourceFieldId).filter(Boolean)), ]; setSourceFields( columnsFromMappings.length > 0 @@ -131,7 +131,7 @@ export const EditConnectorModal = ({ : parseCSVColumnsToFields(SAMPLE_CSV_COLUMNS, { includeSampleValues: false }) ); setMappings( - connector.fieldMappings.map((m) => ({ + feedbackSource.fieldMappings.map((m) => ({ sourceFieldId: m.sourceFieldId, targetFieldId: m.targetFieldId, staticValue: m.staticValue ?? undefined, @@ -144,7 +144,7 @@ export const EditConnectorModal = ({ importHistorical: true, }); } else { - setCsvConnectorName(""); + setCsvFeedbackSourceName(""); setSourceFields([]); setMappings([]); formbricksForm.reset({ @@ -155,10 +155,10 @@ export const EditConnectorModal = ({ }); } } - }, [connector, formbricksForm]); + }, [feedbackSource, formbricksForm]); const resetForm = () => { - setCsvConnectorName(""); + setCsvFeedbackSourceName(""); setMappings([]); setSourceFields([]); formbricksForm.reset({ @@ -177,12 +177,12 @@ export const EditConnectorModal = ({ onOpenChange(newOpen); }; - const handleUpdateFormbricksConnector = async (values: TFormbricksConnectorForm) => { - if (connector?.type !== "formbricks_survey") return; + const handleUpdateFormbricksFeedbackSource = async (values: TFormbricksFeedbackSourceForm) => { + if (feedbackSource?.type !== "formbricks_survey") return; setIsUpdating(true); - const success = await onUpdateConnector({ - connectorId: connector.id, - workspaceId: connector.workspaceId, + const success = await onUpdateFeedbackSource({ + feedbackSourceId: feedbackSource.id, + workspaceId: feedbackSource.workspaceId, name: values.sourceName.trim(), surveyMappings: [{ surveyId: values.surveyId, elementIds: values.selectedQuestionIds }], fieldMappings: undefined, @@ -193,8 +193,8 @@ export const EditConnectorModal = ({ } }; - const handleUpdateCsvConnector = async () => { - if (connector?.type !== "csv" || !isConnectorNameValid(csvConnectorName)) return; + const handleUpdateCsvFeedbackSource = async () => { + if (feedbackSource?.type !== "csv" || !isFeedbackSourceNameValid(csvFeedbackSourceName)) return; const requiredCheck = areAllRequiredCsvFieldsMapped(mappings); if (!requiredCheck.valid) { @@ -210,10 +210,10 @@ export const EditConnectorModal = ({ ); const fieldMappings = [...userMappings, ...CSV_HIDDEN_STATIC_MAPPINGS]; - const success = await onUpdateConnector({ - connectorId: connector.id, - workspaceId: connector.workspaceId, - name: csvConnectorName.trim(), + const success = await onUpdateFeedbackSource({ + feedbackSourceId: feedbackSource.id, + workspaceId: feedbackSource.workspaceId, + name: csvFeedbackSourceName.trim(), surveyMappings: undefined, fieldMappings, }); @@ -232,25 +232,27 @@ export const EditConnectorModal = ({ }; const saveChangesDisabled = useMemo(() => { - if (!connector) return true; + if (!feedbackSource) return true; if (isUpdating) return true; - if (connector.type === "formbricks_survey") { + if (feedbackSource.type === "formbricks_survey") { return ( - !isConnectorNameValid(formbricksValues.sourceName ?? "") || + !isFeedbackSourceNameValid(formbricksValues.sourceName ?? "") || !formbricksValues.surveyId || !formbricksValues.selectedQuestionIds?.length ); } - if (connector.type === "csv") { - return !isConnectorNameValid(csvConnectorName) || !areAllRequiredCsvFieldsMapped(mappings).valid; + if (feedbackSource.type === "csv") { + return ( + !isFeedbackSourceNameValid(csvFeedbackSourceName) || !areAllRequiredCsvFieldsMapped(mappings).valid + ); } return true; - }, [connector, csvConnectorName, formbricksValues, isUpdating, mappings]); + }, [feedbackSource, csvFeedbackSourceName, formbricksValues, isUpdating, mappings]); - if (!connector) return null; + if (!feedbackSource) return null; return ( @@ -261,11 +263,11 @@ export const EditConnectorModal = ({
- {connector.type === "formbricks_survey" ? ( + {feedbackSource.type === "formbricks_survey" ? ( + onSubmit={formbricksForm.handleSubmit(handleUpdateFormbricksFeedbackSource)}> {error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -311,7 +313,7 @@ export const EditConnectorModal = ({ {error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -333,7 +335,7 @@ export const EditConnectorModal = ({ {error?.message && ( - {getTranslatedConnectorError(error.message, t)} + {getTranslatedFeedbackSourceError(error.message, t)} )} )} @@ -343,10 +345,10 @@ export const EditConnectorModal = ({ ) : ( <>
- {getConnectorIcon(connector.type, "h-5 w-5 text-slate-500")} + {getFeedbackSourceIcon(feedbackSource.type, "h-5 w-5 text-slate-500")}

- {t(getConnectorTypeLabelKey(connector.type))} + {t(getFeedbackSourceTypeLabelKey(feedbackSource.type))}

{t("workspace.unify.source_type_cannot_be_changed")} @@ -355,11 +357,11 @@ export const EditConnectorModal = ({

- + setCsvConnectorName(event.target.value)} + id="editFeedbackSourceName" + value={csvFeedbackSourceName} + onChange={(event) => setCsvFeedbackSourceName(event.target.value)} placeholder={t("workspace.unify.enter_name_for_source")} disabled={isReadOnly} /> @@ -374,7 +376,7 @@ export const EditConnectorModal = ({ sourceFields={sourceFields} mappings={mappings} onMappingsChange={setMappings} - connectorType={connector.type} + feedbackSourceType={feedbackSource.type} /> @@ -388,7 +390,7 @@ export const EditConnectorModal = ({ ) : ( <> - {connector.type === "csv" && ( + {feedbackSource.type === "csv" && ( +
+ {!isReadOnly && ( + + )} +
+
+ ); +} diff --git a/apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table-rows-container.tsx b/apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table-rows-container.tsx new file mode 100644 index 000000000000..98f6a740709d --- /dev/null +++ b/apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table-rows-container.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from "react-i18next"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; +import { FeedbackSourcesTableDataRow } from "./feedback-sources-table-data-row"; + +interface FeedbackSourcesTableRowsContainerProps { + feedbackSources: TFeedbackSourceWithMappings[]; + onFeedbackSourceClick: (feedbackSource: TFeedbackSourceWithMappings) => void; + onCsvImport: (feedbackSource: TFeedbackSourceWithMappings) => void; + onDuplicate: (feedbackSource: TFeedbackSourceWithMappings) => Promise; + onToggleStatus: (feedbackSource: TFeedbackSourceWithMappings) => Promise; + onDelete: (feedbackSourceId: string) => Promise; + isReadOnly?: boolean; +} + +export const FeedbackSourcesTableRowsContainer = ({ + feedbackSources, + onFeedbackSourceClick, + onCsvImport, + onDuplicate, + onToggleStatus, + onDelete, + isReadOnly = false, +}: FeedbackSourcesTableRowsContainerProps) => { + const { t } = useTranslation(); + + if (feedbackSources.length === 0) { + return ( +
+

{t("workspace.unify.no_sources_connected")}

+
+ ); + } + + return ( +
+ {feedbackSources.map((feedbackSource) => ( + onFeedbackSourceClick(feedbackSource)} + onCsvImport={feedbackSource.type === "csv" ? () => onCsvImport(feedbackSource) : undefined} + onDuplicate={() => onDuplicate(feedbackSource)} + onToggleStatus={() => onToggleStatus(feedbackSource)} + onDelete={() => onDelete(feedbackSource.id)} + isReadOnly={isReadOnly} + /> + ))} +
+ ); +}; diff --git a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table.tsx b/apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table.tsx similarity index 61% rename from apps/web/modules/ee/unify-feedback/sources/components/connectors-table.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table.tsx index bd224f0d302e..7ddd376fc475 100644 --- a/apps/web/modules/ee/unify-feedback/sources/components/connectors-table.tsx +++ b/apps/web/modules/ee/unify-feedback/sources/components/feedback-sources-table.tsx @@ -2,30 +2,30 @@ import { Loader2Icon } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { TConnectorWithMappings } from "@formbricks/types/connector"; -import { ConnectorsTableRowsContainer } from "./connectors-table-rows-container"; +import { TFeedbackSourceWithMappings } from "@formbricks/types/feedback-source"; +import { FeedbackSourcesTableRowsContainer } from "./feedback-sources-table-rows-container"; -interface ConnectorsTableProps { - connectors: TConnectorWithMappings[]; - onConnectorClick: (connector: TConnectorWithMappings) => void; - onCsvImport: (connector: TConnectorWithMappings) => void; - onDuplicate: (connector: TConnectorWithMappings) => Promise; - onToggleStatus: (connector: TConnectorWithMappings) => Promise; - onDelete: (connectorId: string) => Promise; +interface FeedbackSourcesTableProps { + feedbackSources: TFeedbackSourceWithMappings[]; + onFeedbackSourceClick: (feedbackSource: TFeedbackSourceWithMappings) => void; + onCsvImport: (feedbackSource: TFeedbackSourceWithMappings) => void; + onDuplicate: (feedbackSource: TFeedbackSourceWithMappings) => Promise; + onToggleStatus: (feedbackSource: TFeedbackSourceWithMappings) => Promise; + onDelete: (feedbackSourceId: string) => Promise; isLoading?: boolean; isReadOnly?: boolean; } -export function ConnectorsTable({ - connectors, - onConnectorClick, +export function FeedbackSourcesTable({ + feedbackSources, + onFeedbackSourceClick, onCsvImport, onDuplicate, onToggleStatus, onDelete, isLoading = false, isReadOnly = false, -}: Readonly) { +}: Readonly) { const { t } = useTranslation(); return ( @@ -43,9 +43,9 @@ export function ConnectorsTable({
) : ( - - (UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(type); + (UNSUPPORTED_FEEDBACK_SOURCE_ELEMENT_TYPES as readonly string[]).includes(type); export const FormbricksQuestionList = ({ survey, @@ -45,7 +45,7 @@ export const FormbricksQuestionList = ({ const unsupported = isUnsupportedElementType(element.type); const isChecked = selectedQuestionIds.includes(element.id); const elementTypeLabel = getTSurveyElementTypeEnumName(element.type, t) ?? element.type; - const inputId = `connector-question-${element.id}`; + const inputId = `feedbackSource-question-${element.id}`; return (
void; - connectorType: TConnectorType; + feedbackSourceType: TFeedbackSourceType; confidenceByTargetId?: Record; sampleRow?: Record; } @@ -27,11 +27,11 @@ export function MappingUI({ sourceFields, mappings, onMappingsChange, - connectorType, + feedbackSourceType, confidenceByTargetId, sampleRow, }: MappingUIProps) { - switch (connectorType) { + switch (feedbackSourceType) { case "csv": return ( export type TFeedbackCSVData = z.infer>; -export type TCreateConnectorStep = "selectType" | "mapping"; +export type TCreateFeedbackSourceStep = "selectType" | "mapping"; -export const ZFormbricksConnectorForm = z.object({ - sourceName: z.string().trim().min(1, "CONNECTOR_NAME_REQUIRED"), - surveyId: z.string().min(1, "CONNECTOR_SURVEY_REQUIRED"), - selectedQuestionIds: z.array(z.string()).min(1, "CONNECTOR_QUESTIONS_REQUIRED"), +export const ZFormbricksFeedbackSourceForm = z.object({ + sourceName: z.string().trim().min(1, "FEEDBACK_SOURCE_NAME_REQUIRED"), + surveyId: z.string().min(1, "FEEDBACK_SOURCE_SURVEY_REQUIRED"), + selectedQuestionIds: z.array(z.string()).min(1, "FEEDBACK_SOURCE_QUESTIONS_REQUIRED"), importHistorical: z.boolean(), }); -export type TFormbricksConnectorForm = z.infer; +export type TFormbricksFeedbackSourceForm = z.infer; -export const getTranslatedConnectorError = (errorCode: string, t: TFunction): string => { +export const getTranslatedFeedbackSourceError = (errorCode: string, t: TFunction): string => { switch (errorCode) { - case "CONNECTOR_NAME_DUPLICATE": - return t("workspace.unify.error_connector_name_duplicate"); - case "CONNECTOR_FORMBRICKS_MAPPING_DUPLICATE": - return t("workspace.unify.error_connector_formbricks_mapping_duplicate"); - case "CONNECTOR_FIELD_MAPPING_DUPLICATE": - return t("workspace.unify.error_connector_field_mapping_duplicate"); + case "FEEDBACK_SOURCE_NAME_DUPLICATE": + return t("workspace.unify.error_source_name_duplicate"); + case "FEEDBACK_SOURCE_FORMBRICKS_MAPPING_DUPLICATE": + return t("workspace.unify.error_source_formbricks_mapping_duplicate"); + case "FEEDBACK_SOURCE_FIELD_MAPPING_DUPLICATE": + return t("workspace.unify.error_source_field_mapping_duplicate"); case CSV_IMPORT_MISSING_COLUMNS_ERROR_CODE: return t("workspace.unify.csv_saved_mapping_missing_columns"); - case "CONNECTOR_NAME_REQUIRED": - return t("workspace.unify.error_connector_name_required"); - case "CONNECTOR_SURVEY_REQUIRED": - return t("workspace.unify.error_connector_survey_required"); - case "CONNECTOR_QUESTIONS_REQUIRED": - return t("workspace.unify.error_connector_questions_required"); + case "FEEDBACK_SOURCE_NAME_REQUIRED": + return t("workspace.unify.error_source_name_required"); + case "FEEDBACK_SOURCE_SURVEY_REQUIRED": + return t("workspace.unify.error_source_survey_required"); + case "FEEDBACK_SOURCE_QUESTIONS_REQUIRED": + return t("workspace.unify.error_source_questions_required"); default: return errorCode; } diff --git a/apps/web/modules/ee/unify-feedback/sources/utils.test.ts b/apps/web/modules/ee/unify-feedback/sources/utils.test.ts index c3cb69ce05f7..4f85fd4090a5 100644 --- a/apps/web/modules/ee/unify-feedback/sources/utils.test.ts +++ b/apps/web/modules/ee/unify-feedback/sources/utils.test.ts @@ -3,9 +3,9 @@ import { CSV_HIDDEN_STATIC_MAPPINGS, MAX_CSV_VALUES, TFieldMapping, TSourceField import { areAllRequiredCsvFieldsMapped, autoMapCsvSourceFields, - getConnectorOptions, + getFeedbackSourceOptions, inferFieldType, - isConnectorNameValid, + isFeedbackSourceNameValid, parseCSVColumnsToFields, titleizeFromFileName, toggleQuestionId, @@ -14,9 +14,9 @@ import { const mockT = (key: string) => key; -describe("getConnectorOptions", () => { +describe("getFeedbackSourceOptions", () => { test("returns formbricks, csv, api ingestion, and mcp options", () => { - const options = getConnectorOptions(mockT as never); + const options = getFeedbackSourceOptions(mockT as never); expect(options).toHaveLength(4); expect(options[0].id).toBe("formbricks_survey"); expect(options[1].id).toBe("csv"); @@ -25,12 +25,12 @@ describe("getConnectorOptions", () => { }); test("both options are enabled by default", () => { - const options = getConnectorOptions(mockT as never); + const options = getFeedbackSourceOptions(mockT as never); expect(options.every((o) => !o.disabled)).toBe(true); }); test("uses translation keys for name and description", () => { - const options = getConnectorOptions(mockT as never); + const options = getFeedbackSourceOptions(mockT as never); expect(options[0].name).toBe("workspace.unify.formbricks_surveys"); expect(options[0].description).toBe("workspace.unify.source_connect_formbricks_description"); expect(options[1].name).toBe("workspace.unify.csv_import"); @@ -126,26 +126,26 @@ describe("validateCsvFile", () => { }); }); -describe("isConnectorNameValid", () => { +describe("isFeedbackSourceNameValid", () => { test("returns true for non-empty name", () => { - expect(isConnectorNameValid("My Connector")).toBe(true); + expect(isFeedbackSourceNameValid("My FeedbackSource")).toBe(true); }); test("returns false for empty string", () => { - expect(isConnectorNameValid("")).toBe(false); + expect(isFeedbackSourceNameValid("")).toBe(false); }); test("returns false for whitespace-only string", () => { - expect(isConnectorNameValid(" ")).toBe(false); - expect(isConnectorNameValid("\t\n ")).toBe(false); + expect(isFeedbackSourceNameValid(" ")).toBe(false); + expect(isFeedbackSourceNameValid("\t\n ")).toBe(false); }); test("returns true for name with surrounding whitespace", () => { - expect(isConnectorNameValid(" name ")).toBe(true); + expect(isFeedbackSourceNameValid(" name ")).toBe(true); }); test("returns true for single character", () => { - expect(isConnectorNameValid("a")).toBe(true); + expect(isFeedbackSourceNameValid("a")).toBe(true); }); }); diff --git a/apps/web/modules/ee/unify-feedback/sources/utils.ts b/apps/web/modules/ee/unify-feedback/sources/utils.ts index bf18e4e9d02e..3edd3fac4ff2 100644 --- a/apps/web/modules/ee/unify-feedback/sources/utils.ts +++ b/apps/web/modules/ee/unify-feedback/sources/utils.ts @@ -1,5 +1,5 @@ import { TFunction } from "i18next"; -import { TConnectorType, THubFieldType, ZHubFieldType } from "@formbricks/types/connector"; +import { TFeedbackSourceType, THubFieldType, ZHubFieldType } from "@formbricks/types/feedback-source"; import { CSV_REQUIRED_UI_FIELDS, CSV_TARGET_FIELDS, @@ -9,17 +9,17 @@ import { TSourceField, } from "./types"; -export type TConnectorOptionId = TConnectorType | "api_ingestion" | "feedback_record_mcp"; +export type TFeedbackSourceOptionId = TFeedbackSourceType | "api_ingestion" | "feedback_record_mcp"; -export interface TConnectorOption { - id: TConnectorOptionId; +export interface TFeedbackSourceOption { + id: TFeedbackSourceOptionId; name: string; description: string; disabled: boolean; badge?: { text: string; type: "success" | "gray" | "warning" }; } -export const getConnectorOptions = (t: TFunction): TConnectorOption[] => [ +export const getFeedbackSourceOptions = (t: TFunction): TFeedbackSourceOption[] => [ { id: "formbricks_survey", name: t("workspace.unify.formbricks_surveys"), @@ -101,7 +101,7 @@ export const validateEnumMappings = ( return errors; }; -export const isConnectorNameValid = (name: string): boolean => name.trim().length > 0; +export const isFeedbackSourceNameValid = (name: string): boolean => name.trim().length > 0; export const toggleQuestionId = (currentSelection: string[], questionId: string): string[] => { return currentSelection.includes(questionId) diff --git a/apps/web/modules/response-pipeline/lib/process-response-pipeline-job.ts b/apps/web/modules/response-pipeline/lib/process-response-pipeline-job.ts index 6afbd4e8f540..112334b84f9f 100644 --- a/apps/web/modules/response-pipeline/lib/process-response-pipeline-job.ts +++ b/apps/web/modules/response-pipeline/lib/process-response-pipeline-job.ts @@ -6,9 +6,9 @@ import { type JobHandler, type TResponsePipelineJobData, UnrecoverableError } fr import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { type TUserLocale, ZUserLocale } from "@formbricks/types/user"; -import { handleConnectorPipeline } from "@/lib/connector/pipeline-handler"; import { DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS, POSTHOG_KEY } from "@/lib/constants"; import { generateStandardWebhookSignature } from "@/lib/crypto"; +import { handleFeedbackSourcePipeline } from "@/lib/feedback-source/pipeline-handler"; import { getIntegrations } from "@/lib/integration/service"; import { getResponseCountBySurveyId } from "@/lib/response/service"; import { createPinnedDispatcher, validateAndResolveWebhookUrl } from "@/lib/utils/validate-webhook-url"; @@ -656,14 +656,14 @@ const runResponseFinishedSideEffects = async ({ } try { - await handleConnectorPipeline(data.response, survey, workspaceId); + await handleFeedbackSourcePipeline(data.response, survey, workspaceId); } catch (error) { logger.error( { ...logContext, err: error, }, - "Response pipeline connector handling failed" + "Response pipeline feedbackSource handling failed" ); } diff --git a/apps/web/modules/survey/editor/components/settings-view.tsx b/apps/web/modules/survey/editor/components/settings-view.tsx index 5f51cb785701..b95450af329f 100644 --- a/apps/web/modules/survey/editor/components/settings-view.tsx +++ b/apps/web/modules/survey/editor/components/settings-view.tsx @@ -79,7 +79,6 @@ export const SettingsView = ({ key={localSurvey.segment?.id} localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} - workspaceId={localSurvey.workspaceId} contactAttributeKeys={contactAttributeKeys} segments={segments} initialSegment={segments.find((segment) => segment.id === localSurvey.segment?.id)} diff --git a/docs/development/standards/practices/naming-conventions.mdx b/docs/development/standards/practices/naming-conventions.mdx new file mode 100644 index 000000000000..572133ce236f --- /dev/null +++ b/docs/development/standards/practices/naming-conventions.mdx @@ -0,0 +1,45 @@ +--- +title: Naming conventions +description: Domain-specific naming rules used across the Formbricks codebase. +icon: book-open +--- + +This page documents naming choices that intentionally split similar-sounding terms +across different domains. Always check this list before introducing a name that +overlaps with one of these reserved terms. + +## `FeedbackSource` vs `Connector` + +These two words sound interchangeable in English but refer to **two different +domains in the codebase**. Use them precisely. + +| Term | Domain | Meaning | Used in | +| --- | --- | --- | --- | +| `FeedbackSource` | **Unify Feedback** | A configured integration (Formbricks survey, CSV import, etc.) that streams records into the Hub. | `apps/web/lib/feedback-source/`, `apps/web/modules/ee/unify-feedback/`, Prisma models `FeedbackSource`, `FeedbackSourceFormbricksMapping`, `FeedbackSourceFieldMapping`, enums `FeedbackSourceType`/`FeedbackSourceStatus`, types `T/ZFeedbackSource*`, i18n keys under `workspace.unify.source_*` and `workspace.settings.feedback_directories.feedback_sources_*`. | +| `Connector` | **Workflows** *(upcoming)* and **logical condition combinator** | (a) The logical AND/OR connector used by survey logic, segment filters, and quotas (`ZConnector = z.enum(["and", "or"])`). (b) Reserved for the upcoming Workflows feature; do not use for new Unify Feedback work. | `packages/types/surveys/logic.ts` (`ZConnector`), segment / survey-logic / quota code. | + +## Rules + +- **Never** introduce a new type, file, identifier, Prisma field, or i18n key that uses `Connector`/`connector` inside the Unify Feedback domain. Use `FeedbackSource`/`feedbackSource` instead. +- **Never** rename existing AND/OR `connector` usages in survey logic, segment filters, or quotas. They are not Unify Feedback concepts. +- When adding a new Workflows component, use `Connector` / `connector` freely — that is the reserved name for that domain. +- User-facing copy should say "feedback source" (lowercase) or "Feedback source" (sentence case). Avoid showing "connector" to end users in Unify Feedback UI. + +## i18n key prefix conventions + +Within `workspace.unify.*`: + +- `source_*` — single feedback source actions, errors, labels (e.g., `source_created_successfully`, `source_name`, `default_source_name_csv`) +- `sources` — collection label + +Within `workspace.settings.feedback_directories.*`: + +- `feedback_sources_*` — references to feedback sources attached to a directory (e.g., `feedback_sources_description`, `no_feedback_sources`, `pause_feedback_sources_confirmation_title`) + +## Migration history + +The Unify Feedback domain was originally implemented under `Connector` naming. +The rename was performed in migration `20260528120000_rename_connector_to_feedback_source`, +which renamed three tables, two enums, foreign-key columns (`connectorId` → `feedback_source_id`), +indexes, and constraints in place — preserving all data. See that migration's SQL for the +full mapping if you ever need to reconcile pre-rename SQL dumps. diff --git a/packages/database/migration/20260528120000_rename_connector_to_feedback_source/migration.sql b/packages/database/migration/20260528120000_rename_connector_to_feedback_source/migration.sql new file mode 100644 index 000000000000..011b5009c947 --- /dev/null +++ b/packages/database/migration/20260528120000_rename_connector_to_feedback_source/migration.sql @@ -0,0 +1,80 @@ +-- Rename Connector domain to FeedbackSource (Unify Feedback). The word +-- "Connector" is reserved for the upcoming Workflows domain. +-- +-- This migration renames the three tables, their two enums, foreign-key +-- columns (connectorId -> feedback_source_id), indexes, constraints, and +-- foreign-key relationships in place. Data is preserved. + +-- Rename enums +ALTER TYPE "ConnectorType" RENAME TO "FeedbackSourceType"; +ALTER TYPE "ConnectorStatus" RENAME TO "FeedbackSourceStatus"; + +-- Rename tables +ALTER TABLE "Connector" RENAME TO "FeedbackSource"; +ALTER TABLE "ConnectorFormbricksMapping" RENAME TO "FeedbackSourceFormbricksMapping"; +ALTER TABLE "ConnectorFieldMapping" RENAME TO "FeedbackSourceFieldMapping"; + +-- Rename foreign-key columns (connectorId -> feedback_source_id) +ALTER TABLE "FeedbackSourceFormbricksMapping" + RENAME COLUMN "connectorId" TO "feedback_source_id"; +ALTER TABLE "FeedbackSourceFieldMapping" + RENAME COLUMN "connectorId" TO "feedback_source_id"; + +-- Rename the primary-key constraint on each renamed table +ALTER INDEX "Connector_pkey" RENAME TO "FeedbackSource_pkey"; +ALTER INDEX "ConnectorFormbricksMapping_pkey" RENAME TO "FeedbackSourceFormbricksMapping_pkey"; +ALTER INDEX "ConnectorFieldMapping_pkey" RENAME TO "FeedbackSourceFieldMapping_pkey"; + +-- Rename indexes on the FeedbackSource table +ALTER INDEX "Connector_id_workspaceId_key" RENAME TO "FeedbackSource_id_workspaceId_key"; +ALTER INDEX "Connector_workspaceId_name_key" RENAME TO "FeedbackSource_workspaceId_name_key"; +ALTER INDEX "Connector_type_idx" RENAME TO "FeedbackSource_type_idx"; + +-- Rename indexes on FeedbackSourceFormbricksMapping +ALTER INDEX "ConnectorFormbricksMapping_workspaceId_connectorId_surveyId_elementId_key" + RENAME TO "FeedbackSourceFormbricksMapping_workspaceId_feedbackSourceId_surveyId_elementId_key"; +ALTER INDEX "ConnectorFormbricksMapping_workspaceId_surveyId_idx" + RENAME TO "FeedbackSourceFormbricksMapping_workspaceId_surveyId_idx"; +ALTER INDEX "ConnectorFormbricksMapping_surveyId_idx" + RENAME TO "FeedbackSourceFormbricksMapping_surveyId_idx"; + +-- Rename indexes on FeedbackSourceFieldMapping +ALTER INDEX "ConnectorFieldMapping_workspaceId_connectorId_sourceFieldId_targetFieldId_key" + RENAME TO "FeedbackSourceFieldMapping_workspaceId_feedbackSourceId_sourceFieldId_targetFieldId_key"; + +-- Rename foreign-key constraints on FeedbackSource itself +ALTER TABLE "FeedbackSource" + RENAME CONSTRAINT "Connector_workspaceId_fkey" TO "FeedbackSource_workspaceId_fkey"; +ALTER TABLE "FeedbackSource" + RENAME CONSTRAINT "Connector_created_by_fkey" TO "FeedbackSource_created_by_fkey"; + +-- Rename foreign-key constraints on FeedbackSourceFormbricksMapping +ALTER TABLE "FeedbackSourceFormbricksMapping" + RENAME CONSTRAINT "ConnectorFormbricksMapping_connectorId_workspaceId_fkey" + TO "FeedbackSourceFormbricksMapping_feedbackSourceId_workspaceId_fkey"; +ALTER TABLE "FeedbackSourceFormbricksMapping" + RENAME CONSTRAINT "ConnectorFormbricksMapping_surveyId_workspaceId_fkey" + TO "FeedbackSourceFormbricksMapping_surveyId_workspaceId_fkey"; + +-- Rename foreign-key constraint on FeedbackSourceFieldMapping +ALTER TABLE "FeedbackSourceFieldMapping" + RENAME CONSTRAINT "ConnectorFieldMapping_connectorId_workspaceId_fkey" + TO "FeedbackSourceFieldMapping_feedbackSourceId_workspaceId_fkey"; + +-- FeedbackDirectory FK (added in a later migration than the original connector model) +-- exists if the connector→directory link was added later; rename if present. +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'Connector_feedbackDirectoryId_fkey' + ) THEN + EXECUTE 'ALTER TABLE "FeedbackSource" RENAME CONSTRAINT "Connector_feedbackDirectoryId_fkey" TO "FeedbackSource_feedbackDirectoryId_fkey"'; + END IF; + IF EXISTS ( + SELECT 1 FROM pg_indexes + WHERE schemaname = 'public' AND indexname = 'Connector_feedbackDirectoryId_idx' + ) THEN + EXECUTE 'ALTER INDEX "Connector_feedbackDirectoryId_idx" RENAME TO "FeedbackSource_feedbackDirectoryId_idx"'; + END IF; +END$$; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index f98da4810a44..d3091fd9745a 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -395,21 +395,21 @@ model Survey { /// [SurveySingleUse] singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}") - isVerifyEmailEnabled Boolean @default(false) - isSingleResponsePerEmailEnabled Boolean @default(false) - isBackButtonHidden Boolean @default(false) - isAutoProgressingEnabled Boolean @default(false) - isCaptureIpEnabled Boolean @default(false) + isVerifyEmailEnabled Boolean @default(false) + isSingleResponsePerEmailEnabled Boolean @default(false) + isBackButtonHidden Boolean @default(false) + isAutoProgressingEnabled Boolean @default(false) + isCaptureIpEnabled Boolean @default(false) pin String? displayPercentage Decimal? languages SurveyLanguage[] showLanguageSwitch Boolean? followUps SurveyFollowUp[] /// [SurveyRecaptcha] - recaptcha Json? @default("{\"enabled\": false, \"threshold\":0.1}") + recaptcha Json? @default("{\"enabled\": false, \"threshold\":0.1}") /// [SurveyLinkMetadata] - metadata Json @default("{}") - connectorMappings ConnectorFormbricksMapping[] + metadata Json @default("{}") + feedbackSourceMappings FeedbackSourceFormbricksMapping[] slug String? @unique @@ -632,7 +632,7 @@ model Workspace { segments Segment[] integrations Integration[] apiKeyWorkspaces ApiKeyWorkspace[] - connectors Connector[] + feedbackSources FeedbackSource[] @@unique([organizationId, name]) } @@ -917,7 +917,7 @@ model User { surveys Survey[] charts Chart[] @relation("chartCreatedBy") dashboards Dashboard[] @relation("dashboardCreatedBy") - connectors Connector[] + feedbackSources FeedbackSource[] teamUsers TeamUser[] lastLoginAt DateTime? isActive Boolean @default(true) @@ -1146,12 +1146,12 @@ model DashboardWidget { @@index([dashboardId, order]) } -enum ConnectorType { +enum FeedbackSourceType { formbricks_survey csv } -enum ConnectorStatus { +enum FeedbackSourceStatus { active paused error @@ -1169,33 +1169,34 @@ enum HubFieldType { date } -/// Base connector for all integration types. -/// Connects external data sources to the Hub for feedback record creation. +/// A feedback source — connects external data into Unify Feedback for record creation. +/// Naming note: "FeedbackSource" is the Unify Feedback domain term; the word "Connector" +/// is reserved for the Workflows domain (see docs/development/standards/practices/naming-conventions.mdx). /// -/// @property id - Unique identifier for the connector -/// @property name - Display name for the connector -/// @property type - Type of connector (formbricks_survey, webhook, csv, email, slack) -/// @property status - Current state of the connector (active, paused) -/// @property environment - The environment this connector belongs to -/// @property config - Type-specific configuration (e.g., webhook secret, S3 config) -/// @property formbricksMappings - Element mappings for Formbricks connectors -/// @property fieldMappings - Field mappings for other connector types -model Connector { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @updatedAt @map(name: "updated_at") +/// @property id - Unique identifier for the feedback source +/// @property name - Display name for the feedback source +/// @property type - Type of feedback source (formbricks_survey, csv, …) +/// @property status - Current state of the feedback source (active, paused, error) +/// @property workspace - The workspace this feedback source belongs to +/// @property feedbackDirectory - Hub directory the feedback source writes into +/// @property formbricksMappings - Element mappings for Formbricks feedback sources +/// @property fieldMappings - Field mappings for other feedback source types +model FeedbackSource { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") name String - type ConnectorType - status ConnectorStatus @default(active) + type FeedbackSourceType + status FeedbackSourceStatus @default(active) workspaceId String - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) feedbackDirectoryId String - feedbackDirectory FeedbackDirectory @relation(fields: [feedbackDirectoryId], references: [id], onDelete: Cascade) - formbricksMappings ConnectorFormbricksMapping[] - fieldMappings ConnectorFieldMapping[] - lastSyncAt DateTime? @map(name: "last_sync_at") - createdBy String? @map(name: "created_by") - creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) + feedbackDirectory FeedbackDirectory @relation(fields: [feedbackDirectoryId], references: [id], onDelete: Cascade) + formbricksMappings FeedbackSourceFormbricksMapping[] + fieldMappings FeedbackSourceFieldMapping[] + lastSyncAt DateTime? @map(name: "last_sync_at") + createdBy String? @map(name: "created_by") + creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) @@unique([id, workspaceId]) @@unique([workspaceId, name]) @@ -1203,51 +1204,51 @@ model Connector { @@index([feedbackDirectoryId]) } -/// Maps survey elements to Hub FeedbackRecords for Formbricks connectors. +/// Maps survey elements to Hub FeedbackRecords for Formbricks feedback sources. /// Each row represents one element that will create FeedbackRecords when answered. /// /// @property id - Unique identifier for the mapping -/// @property connector - The parent connector +/// @property feedbackSource - The parent feedback source /// @property survey - The survey containing the element /// @property elementId - The element ID within the survey (from blocks[].elements[].id) /// @property hubFieldType - The field_type to use in Hub (text, nps, rating, etc.) /// @property customFieldLabel - Optional override for the element headline as field_label in Hub -model ConnectorFormbricksMapping { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - connectorId String +model FeedbackSourceFormbricksMapping { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + feedbackSourceId String @map(name: "feedback_source_id") workspaceId String - connector Connector @relation(fields: [connectorId, workspaceId], references: [id, workspaceId], onDelete: Cascade) + feedbackSource FeedbackSource @relation(fields: [feedbackSourceId, workspaceId], references: [id, workspaceId], onDelete: Cascade) surveyId String - survey Survey @relation(fields: [surveyId, workspaceId], references: [id, workspaceId], onDelete: Cascade) + survey Survey @relation(fields: [surveyId, workspaceId], references: [id, workspaceId], onDelete: Cascade) elementId String hubFieldType HubFieldType - customFieldLabel String? @map(name: "custom_field_label") + customFieldLabel String? @map(name: "custom_field_label") - @@unique([workspaceId, connectorId, surveyId, elementId]) + @@unique([workspaceId, feedbackSourceId, surveyId, elementId]) @@index([workspaceId, surveyId]) @@index([surveyId]) } -/// Generic field mapping for Webhook, CSV, Email, Slack connectors. +/// Generic field mapping for non-Formbricks feedback sources (CSV today; webhook/email/slack later). /// Maps source fields to Hub FeedbackRecord fields. /// /// @property id - Unique identifier for the mapping -/// @property connector - The parent connector +/// @property feedbackSource - The parent feedback source /// @property sourceFieldId - Field path for webhook (e.g., "user.id"), column name for CSV /// @property targetFieldId - Hub field (collected_at, field_id, value_text, etc.) /// @property staticValue - If set, use this value instead of reading from sourceFieldId -model ConnectorFieldMapping { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - connectorId String - workspaceId String - connector Connector @relation(fields: [connectorId, workspaceId], references: [id, workspaceId], onDelete: Cascade) - sourceFieldId String @map(name: "source_field_id") - targetFieldId String @map(name: "target_field_id") - staticValue String? @map(name: "static_value") +model FeedbackSourceFieldMapping { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + feedbackSourceId String @map(name: "feedback_source_id") + workspaceId String + feedbackSource FeedbackSource @relation(fields: [feedbackSourceId, workspaceId], references: [id, workspaceId], onDelete: Cascade) + sourceFieldId String @map(name: "source_field_id") + targetFieldId String @map(name: "target_field_id") + staticValue String? @map(name: "static_value") - @@unique([workspaceId, connectorId, sourceFieldId, targetFieldId]) + @@unique([workspaceId, feedbackSourceId, sourceFieldId, targetFieldId]) } /// Represents a feedback directory (Hub tenant) owned by an organization. @@ -1259,16 +1260,16 @@ model ConnectorFieldMapping { /// @property organization - The parent organization /// @property workspaces - Workspaces assigned to this directory model FeedbackDirectory { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @updatedAt @map(name: "updated_at") - name String - isArchived Boolean @default(false) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - workspaces FeedbackDirectoryWorkspace[] - connectors Connector[] - charts Chart[] + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + isArchived Boolean @default(false) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + workspaces FeedbackDirectoryWorkspace[] + feedbackSources FeedbackSource[] + charts Chart[] @@unique([organizationId, name]) } diff --git a/packages/types/connector.ts b/packages/types/feedback-source.ts similarity index 58% rename from packages/types/connector.ts rename to packages/types/feedback-source.ts index ed4cb60c0948..b250462e4bb8 100644 --- a/packages/types/connector.ts +++ b/packages/types/feedback-source.ts @@ -1,13 +1,13 @@ import { z } from "zod"; import { TSurveyElementTypeEnum } from "./surveys/constants"; -// Connector type enum -export const ZConnectorType = z.enum(["formbricks_survey", "csv"]); -export type TConnectorType = z.infer; +// Feedback source type enum +export const ZFeedbackSourceType = z.enum(["formbricks_survey", "csv"]); +export type TFeedbackSourceType = z.infer; -// Connector status enum -export const ZConnectorStatus = z.enum(["active", "paused", "error"]); -export type TConnectorStatus = z.infer; +// Feedback source status enum +export const ZFeedbackSourceStatus = z.enum(["active", "paused", "error"]); +export type TFeedbackSourceStatus = z.infer; // Hub field types (from Hub OpenAPI spec) export const ZHubFieldType = z.enum([ @@ -24,7 +24,7 @@ export const ZHubFieldType = z.enum([ export type THubFieldType = z.infer; // Hub target fields for mapping. -// `response_value` is a CSV-only synthetic id stored in ConnectorFieldMapping; csv-transform.ts +// `response_value` is a CSV-only synthetic id stored in FeedbackSourceFieldMapping; csv-transform.ts // resolves it to the appropriate value_* target before any Hub write — the Hub never sees it. export const ZHubTargetField = z.enum([ "collected_at", @@ -49,88 +49,90 @@ export const ZHubTargetField = z.enum([ ]); export type THubTargetField = z.infer; -// Base connector schema -export const ZConnector = z.object({ +// Base feedback source schema +export const ZFeedbackSource = z.object({ id: z.cuid2(), createdAt: z.date(), updatedAt: z.date(), name: z.string().min(1), - type: ZConnectorType, - status: ZConnectorStatus, + type: ZFeedbackSourceType, + status: ZFeedbackSourceStatus, workspaceId: z.cuid2(), feedbackDirectoryId: z.cuid2(), lastSyncAt: z.date().nullable(), createdBy: z.string().nullable(), }); -export type TConnector = z.infer; +export type TFeedbackSource = z.infer; // Formbricks element mapping -export const ZConnectorFormbricksMapping = z.object({ +export const ZFeedbackSourceFormbricksMapping = z.object({ id: z.cuid2(), createdAt: z.date(), - connectorId: z.cuid2(), + feedbackSourceId: z.cuid2(), workspaceId: z.cuid2(), surveyId: z.cuid2(), elementId: z.string(), hubFieldType: ZHubFieldType, customFieldLabel: z.string().nullable(), }); -export type TConnectorFormbricksMapping = z.infer; +export type TFeedbackSourceFormbricksMapping = z.infer; -export const ZConnectorFieldMapping = z.object({ +export const ZFeedbackSourceFieldMapping = z.object({ id: z.cuid2(), createdAt: z.date(), - connectorId: z.cuid2(), + feedbackSourceId: z.cuid2(), workspaceId: z.cuid2(), sourceFieldId: z.string(), targetFieldId: ZHubTargetField, staticValue: z.string().nullable(), }); -export type TConnectorFieldMapping = z.infer; +export type TFeedbackSourceFieldMapping = z.infer; -export const ZConnectorWithMappings = ZConnector.extend({ - formbricksMappings: z.array(ZConnectorFormbricksMapping), - fieldMappings: z.array(ZConnectorFieldMapping), +export const ZFeedbackSourceWithMappings = ZFeedbackSource.extend({ + formbricksMappings: z.array(ZFeedbackSourceFormbricksMapping), + fieldMappings: z.array(ZFeedbackSourceFieldMapping), creatorName: z.string().nullable().optional(), }); -export type TConnectorWithMappings = z.infer; +export type TFeedbackSourceWithMappings = z.infer; // Create input schemas -export const ZConnectorCreateInput = z.object({ +export const ZFeedbackSourceCreateInput = z.object({ name: z.string().min(1), - type: ZConnectorType, + type: ZFeedbackSourceType, feedbackDirectoryId: z.cuid2(), createdBy: z.cuid2().optional(), }); -export type TConnectorCreateInput = z.infer; +export type TFeedbackSourceCreateInput = z.infer; // Create Formbricks mapping input -export const ZConnectorFormbricksMappingCreateInput = z.object({ +export const ZFeedbackSourceFormbricksMappingCreateInput = z.object({ surveyId: z.cuid2(), elementId: z.string(), hubFieldType: ZHubFieldType, customFieldLabel: z.string().optional(), }); -export type TConnectorFormbricksMappingCreateInput = z.infer; +export type TFeedbackSourceFormbricksMappingCreateInput = z.infer< + typeof ZFeedbackSourceFormbricksMappingCreateInput +>; // Create field mapping input -export const ZConnectorFieldMappingCreateInput = z.object({ +export const ZFeedbackSourceFieldMappingCreateInput = z.object({ sourceFieldId: z.string(), targetFieldId: ZHubTargetField, staticValue: z.string().optional(), }); -export type TConnectorFieldMappingCreateInput = z.infer; +export type TFeedbackSourceFieldMappingCreateInput = z.infer; -// Update connector input -export const ZConnectorUpdateInput = z.object({ +// Update feedback source input +export const ZFeedbackSourceUpdateInput = z.object({ name: z.string().min(1).optional(), - status: ZConnectorStatus.optional(), + status: ZFeedbackSourceStatus.optional(), lastSyncAt: z.date().nullable().optional(), }); -export type TConnectorUpdateInput = z.infer; +export type TFeedbackSourceUpdateInput = z.infer; // Element types that cannot be mapped to Hub fields -export const UNSUPPORTED_CONNECTOR_ELEMENT_TYPES: readonly TSurveyElementTypeEnum[] = [ +export const UNSUPPORTED_FEEDBACK_SOURCE_ELEMENT_TYPES: readonly TSurveyElementTypeEnum[] = [ TSurveyElementTypeEnum.ContactInfo, TSurveyElementTypeEnum.Address, TSurveyElementTypeEnum.Cal,