mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks into feat/attribute-type-date
This commit is contained in:
53
.github/workflows/e2e.yml
vendored
53
.github/workflows/e2e.yml
vendored
@@ -3,14 +3,10 @@ name: E2E Tests
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
AZURE_CLIENT_ID:
|
||||
required: false
|
||||
AZURE_TENANT_ID:
|
||||
required: false
|
||||
AZURE_SUBSCRIPTION_ID:
|
||||
required: false
|
||||
PLAYWRIGHT_SERVICE_URL:
|
||||
required: false
|
||||
PLAYWRIGHT_SERVICE_ACCESS_TOKEN:
|
||||
required: false
|
||||
ENTERPRISE_LICENSE_KEY:
|
||||
required: true
|
||||
# Add other secrets if necessary
|
||||
@@ -21,7 +17,6 @@ env:
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
@@ -114,7 +109,7 @@ jobs:
|
||||
- name: Start MinIO Server
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
# Start MinIO server in background
|
||||
docker run -d \
|
||||
--name minio-server \
|
||||
@@ -124,7 +119,7 @@ jobs:
|
||||
-e MINIO_ROOT_PASSWORD=devminio123 \
|
||||
minio/minio:RELEASE.2025-09-07T16-13-09Z \
|
||||
server /data --console-address :9001
|
||||
|
||||
|
||||
echo "MinIO server started"
|
||||
|
||||
- name: Wait for MinIO and create S3 bucket
|
||||
@@ -207,32 +202,30 @@ jobs:
|
||||
- name: Install Playwright
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Set Azure Secret Variables
|
||||
run: |
|
||||
if [[ -n "${{ secrets.AZURE_CLIENT_ID }}" && -n "${{ secrets.AZURE_TENANT_ID }}" && -n "${{ secrets.AZURE_SUBSCRIPTION_ID }}" ]]; then
|
||||
echo "AZURE_ENABLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "AZURE_ENABLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Azure login
|
||||
if: env.AZURE_ENABLED == 'true'
|
||||
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Run E2E Tests (Azure)
|
||||
if: env.AZURE_ENABLED == 'true'
|
||||
- name: Determine Playwright execution mode
|
||||
shell: bash
|
||||
env:
|
||||
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
|
||||
CI: true
|
||||
PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }}
|
||||
run: |
|
||||
pnpm test-e2e:azure
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "${PLAYWRIGHT_SERVICE_URL}" && -n "${PLAYWRIGHT_SERVICE_ACCESS_TOKEN}" ]]; then
|
||||
echo "PW_MODE=service" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "PW_MODE=local" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Run E2E Tests (Playwright Service)
|
||||
if: env.PW_MODE == 'service'
|
||||
env:
|
||||
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
|
||||
PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }}
|
||||
CI: true
|
||||
run: pnpm test-e2e:azure
|
||||
|
||||
- name: Run E2E Tests (Local)
|
||||
if: env.AZURE_ENABLED == 'false'
|
||||
if: env.PW_MODE == 'local'
|
||||
env:
|
||||
CI: true
|
||||
run: |
|
||||
|
||||
@@ -135,7 +135,7 @@ export const OrganizationBreadcrumb = ({
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.teams"),
|
||||
label: t("common.members_and_teams"),
|
||||
href: `/environments/${currentEnvironmentId}/settings/teams`,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ export const OrganizationSettingsNavbar = ({
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.teams"),
|
||||
label: t("common.members_and_teams"),
|
||||
href: `/environments/${environmentId}/settings/teams`,
|
||||
current: pathname?.includes("/teams"),
|
||||
},
|
||||
|
||||
@@ -234,6 +234,7 @@ checksums:
|
||||
common/maximum: 4c07541dd1f093775bdc61b559cca6c8
|
||||
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||
common/metadata: 695d4f7da261ba76e3be4de495491028
|
||||
common/minimum: d9759235086d0169928b3c1401115e22
|
||||
@@ -313,6 +314,7 @@ checksums:
|
||||
common/read_docs: 426ba960bfedf186a878b7467867f9d2
|
||||
common/recipients: f90e7f266be3f5a724858f21a9fd855e
|
||||
common/remove: dba2fe5fe9f83f8078c687f28cba4b52
|
||||
common/remove_from_team: 69bcc7a1001c3017f9de578ee22cffd6
|
||||
common/reorder_and_hide_columns: a5e3d7c0c7ef879211d05a37be1c5069
|
||||
common/report_survey: 147dd05db52e35f5d1f837460fb720f5
|
||||
common/request_pricing: 58eb24af4f098632709cb7482b70a1cb
|
||||
@@ -380,7 +382,8 @@ checksums:
|
||||
common/team_access: 45c6232c71b760eaa33b932dabab4c1c
|
||||
common/team_id: 134e32d6f7184577a46b2fd83e85e532
|
||||
common/team_name: 549d949de4b9adad4afd6427a60a329e
|
||||
common/teams: a2fbdec69342366a2b6033d119aa279a
|
||||
common/team_role: 66db395781aef64ef3791417b3b67c0b
|
||||
common/teams: b63448c05270497973ac4407047dae02
|
||||
common/teams_not_found: 02f333a64a83c1c014d8900ec9666345
|
||||
common/text: 4ddccc1974775ed7357f9beaf9361cec
|
||||
common/time: b504a03d52e8001bfdc5cb6205364f42
|
||||
@@ -840,7 +843,6 @@ checksums:
|
||||
environments/project/tags/tags_merged: 544471de666f93fbb0ab600321d1e553
|
||||
environments/project/teams/manage_teams: d7b5f26335cea450c333832adbe0b6ad
|
||||
environments/project/teams/no_teams_found: fb6680d4b5b73731697b100713afb50d
|
||||
environments/project/teams/only_organization_owners_and_managers_can_manage_teams: 179056fade669d34f63fb1ee965b8024
|
||||
environments/project/teams/permission: cc2ed7274bd8267f9e0a10b079584d8b
|
||||
environments/project/teams/team_name: d1a5f99dbf503ca53f06b3a98b511d02
|
||||
environments/project/teams/team_settings_description: da32d77993f5c5c7547cdf3e1d3fc7d5
|
||||
@@ -1086,13 +1088,17 @@ checksums:
|
||||
environments/settings/teams/manage_team: 4c52e636cfd1451a08179fb7a68042ab
|
||||
environments/settings/teams/manage_team_disabled: 2aaa0557b403a5bc657ec9e8b19ac5ac
|
||||
environments/settings/teams/manager_role_description: 39846863fa85ff8b1c6e4f354eb5018f
|
||||
environments/settings/teams/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
environments/settings/teams/member_role_description: 1c5deaece65798b74cc0d34525506c18
|
||||
environments/settings/teams/member_role_info_message: 0a276eef3c3b907d6f396ebfdc693b12
|
||||
environments/settings/teams/organization_role: 979b75fcc3696952e5922d659c839c10
|
||||
environments/settings/teams/owner_role_description: 8f577e6f9d1368fed4eba5a91ffc8cbf
|
||||
environments/settings/teams/please_fill_all_member_fields: 60e38d9906ec9a02a44d16c736bd9fe9
|
||||
environments/settings/teams/please_fill_all_project_fields: 6712059df63c432ecd31f3c52b8e4d87
|
||||
environments/settings/teams/read: 2494ca23d10e5b6381eb271aceeb5270
|
||||
environments/settings/teams/read_write: 278a90dade128198d4c93ac00c345320
|
||||
environments/settings/teams/select_member: 7f4a38312aabbbe3fe92756b57bd5d75
|
||||
environments/settings/teams/select_project: 6e4f4a24178660851d9ae0874706be9f
|
||||
environments/settings/teams/team_admin: 5df68214685738029af678ae1d5912bb
|
||||
environments/settings/teams/team_created_successfully: 45f83048fcabf466551144858a761eca
|
||||
environments/settings/teams/team_deleted_successfully: 972c86b0abe87f229f7bf1a691c0a253
|
||||
@@ -1176,6 +1182,10 @@ checksums:
|
||||
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
||||
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
||||
environments/surveys/edit/brightness: 45425b6db1872225bfff71cf619d0e64
|
||||
environments/surveys/edit/bulk_edit: 59bd1a55587c8cbad716afbf2509e5bb
|
||||
environments/surveys/edit/bulk_edit_description: 9b5b2c6183c6c51689e16d7ba02ec9bb
|
||||
environments/surveys/edit/bulk_edit_options: 74ebec7c53be729f33e38d7605b25815
|
||||
environments/surveys/edit/bulk_edit_options_for: 986af3a8286f34c9e4ad7c74d3c65ada
|
||||
environments/surveys/edit/button_external: d2de24e06574622baf1c0cdd1b718b1a
|
||||
environments/surveys/edit/button_external_description: cbd10d494a70b362bfee811e012c45b1
|
||||
environments/surveys/edit/button_label: db3cd7c74f393187bd780c5c3d8b9b4f
|
||||
@@ -1422,6 +1432,7 @@ checksums:
|
||||
environments/surveys/edit/option_used_in_logic_error: c682ac2cfd286c3cc07dd21ac863dd4c
|
||||
environments/surveys/edit/optional: 396fb9a0472daf401c392bdc3e248943
|
||||
environments/surveys/edit/options: 59156082418d80acb211f973b1218f11
|
||||
environments/surveys/edit/options_used_in_logic_bulk_error: 1720e7a01a0bcb67c152cfe6a68c5355
|
||||
environments/surveys/edit/override_theme_with_individual_styles_for_this_survey: edffc97f5d3372419fe0444de0a5aa3f
|
||||
environments/surveys/edit/overwrite_global_waiting_time: 7bc23bd502b6bd048356b67acd956d9d
|
||||
environments/surveys/edit/overwrite_global_waiting_time_description: 795cf6e93d4c01d2e43aa0ebab601c6e
|
||||
@@ -1569,6 +1580,7 @@ checksums:
|
||||
environments/surveys/edit/unsaved_changes_warning: a164f276c9f7344022aa4640b32abcf9
|
||||
environments/surveys/edit/until_they_submit_a_response: 2a0fd5dcc6cc40a72ed9b974f22eaf68
|
||||
environments/surveys/edit/untitled_block: fdaa045139deff5cc65fa027df0cc22e
|
||||
environments/surveys/edit/update_options: 3499161b010acdefba2d878daa5fb6fa
|
||||
environments/surveys/edit/upgrade_notice_description: 32b66a4f257ad8d38bc38dcc95fe23c4
|
||||
environments/surveys/edit/upgrade_notice_title: 40866066ebc558ad0c92a4f19f12090c
|
||||
environments/surveys/edit/upload: 4a6c84aa16db0f4e5697f49b45257bc7
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Maximal",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"members_and_teams": "Mitglieder & Teams",
|
||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||
"metadata": "Metadaten",
|
||||
"minimum": "Minimum",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"recipients": "Empfänger",
|
||||
"remove": "Entfernen",
|
||||
"remove_from_team": "Aus Team entfernen",
|
||||
"reorder_and_hide_columns": "Spalten neu anordnen und ausblenden",
|
||||
"report_survey": "Umfrage melden",
|
||||
"request_pricing": "Preise anfragen",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Teamzugriff",
|
||||
"team_id": "Team-ID",
|
||||
"team_name": "Teamname",
|
||||
"teams": "Zugriffskontrolle",
|
||||
"team_role": "Team-Rolle",
|
||||
"teams": "Teams",
|
||||
"teams_not_found": "Teams nicht gefunden",
|
||||
"text": "Text",
|
||||
"time": "Zeit",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Teams verwalten",
|
||||
"no_teams_found": "Keine Teams gefunden",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Nur Organisationsinhaber und -manager können Teams verwalten.",
|
||||
"permission": "Berechtigung",
|
||||
"team_name": "Teamname",
|
||||
"team_settings_description": "Teams und ihre Mitglieder können auf dieses Projekt und seine Umfragen zugreifen. Organisationsbesitzer und Manager können diesen Zugriff gewähren."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Team verwalten",
|
||||
"manage_team_disabled": "Nur Organisationsbesitzer, Manager und Team-Admins können Teams verwalten.",
|
||||
"manager_role_description": "Manager können auf alle Projekte zugreifen und Mitglieder hinzufügen und entfernen.",
|
||||
"member": "Mitglied",
|
||||
"member_role_description": "Mitglieder können in ausgewählten Projekten arbeiten.",
|
||||
"member_role_info_message": "Um neuen Mitgliedern Zugriff auf ein Projekt zu geben, füge sie bitte unten einem Team hinzu. Mit Teams kannst du steuern, wer auf welches Projekt zugreifen kann.",
|
||||
"organization_role": "Organisationsrolle",
|
||||
"owner_role_description": "Besitzer haben die volle Kontrolle über die Organisation.",
|
||||
"please_fill_all_member_fields": "Bitte fülle alle Felder aus, um ein neues Mitglied hinzuzufügen.",
|
||||
"please_fill_all_project_fields": "Bitte fülle alle Felder aus, um ein neues Projekt hinzuzufügen.",
|
||||
"read": "Lesen",
|
||||
"read_write": "Lesen & Schreiben",
|
||||
"select_member": "Mitglied auswählen",
|
||||
"select_project": "Projekt auswählen",
|
||||
"team_admin": "Team-Admin",
|
||||
"team_created_successfully": "Team erfolgreich erstellt.",
|
||||
"team_deleted_successfully": "Team erfolgreich gelöscht.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Fett",
|
||||
"brand_color": "Markenfarbe",
|
||||
"brightness": "Helligkeit",
|
||||
"bulk_edit": "Massenbearbeitung",
|
||||
"bulk_edit_description": "Bearbeiten Sie alle Optionen unten, eine pro Zeile. Leere Zeilen werden übersprungen und Duplikate entfernt.",
|
||||
"bulk_edit_options": "Optionen massenbearbeiten",
|
||||
"bulk_edit_options_for": "Optionen massenbearbeiten für {language}",
|
||||
"button_external": "Externen Link aktivieren",
|
||||
"button_external_description": "Fügen Sie eine Schaltfläche hinzu, die eine externe URL in einem neuen Tab öffnet",
|
||||
"button_label": "Beschriftung",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Diese Option wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"optional": "Optional",
|
||||
"options": "Optionen",
|
||||
"options_used_in_logic_bulk_error": "Die folgenden Optionen werden in der Logik verwendet: {questionIndexes}. Bitte entferne sie zuerst aus der Logik.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Styling für diese Umfrage überschreiben.",
|
||||
"overwrite_global_waiting_time": "Benutzerdefinierte Wartezeit festlegen",
|
||||
"overwrite_global_waiting_time_description": "Die Projektkonfiguration nur für diese Umfrage überschreiben.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Du hast ungespeicherte Änderungen in deiner Umfrage. Möchtest Du sie speichern, bevor Du gehst?",
|
||||
"until_they_submit_a_response": "Fragen, bis sie eine Antwort abgeben",
|
||||
"untitled_block": "Unbenannter Block",
|
||||
"update_options": "Optionen aktualisieren",
|
||||
"upgrade_notice_description": "Erstelle mehrsprachige Umfragen und entdecke viele weitere Funktionen",
|
||||
"upgrade_notice_title": "Schalte mehrsprachige Umfragen mit einem höheren Plan frei",
|
||||
"upload": "Hochladen",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Maximum",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"members_and_teams": "Members & Teams",
|
||||
"membership_not_found": "Membership not found",
|
||||
"metadata": "Metadata",
|
||||
"minimum": "Minimum",
|
||||
@@ -337,9 +338,10 @@
|
||||
"quota": "Quota",
|
||||
"quotas": "Quotas",
|
||||
"quotas_description": "Limit the amount of responses you receive from participants who meet certain criteria.",
|
||||
"read_docs": "Read Docs",
|
||||
"read_docs": "Read docs",
|
||||
"recipients": "Recipients",
|
||||
"remove": "Remove",
|
||||
"remove_from_team": "Remove from team",
|
||||
"reorder_and_hide_columns": "Reorder and hide columns",
|
||||
"report_survey": "Report Survey",
|
||||
"request_pricing": "Request Pricing",
|
||||
@@ -349,7 +351,6 @@
|
||||
"responses": "Responses",
|
||||
"restart": "Restart",
|
||||
"role": "Role",
|
||||
"role_organization": "Role (Organization)",
|
||||
"saas": "SaaS",
|
||||
"sales": "Sales",
|
||||
"save": "Save",
|
||||
@@ -407,7 +408,8 @@
|
||||
"team_access": "Team Access",
|
||||
"team_id": "Team ID",
|
||||
"team_name": "Team name",
|
||||
"teams": "Access Control",
|
||||
"team_role": "Team role",
|
||||
"teams": "Teams",
|
||||
"teams_not_found": "Teams not found",
|
||||
"text": "Text",
|
||||
"time": "Time",
|
||||
@@ -903,7 +905,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Manage teams",
|
||||
"no_teams_found": "No teams found",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Only organization owners and managers can manage teams.",
|
||||
"permission": "Permission",
|
||||
"team_name": "Team Name",
|
||||
"team_settings_description": "See which teams can access this project."
|
||||
@@ -1167,13 +1168,17 @@
|
||||
"manage_team": "Manage team",
|
||||
"manage_team_disabled": "Only organization owners, managers and team admins can manage teams.",
|
||||
"manager_role_description": "Managers can access all projects and add and remove members.",
|
||||
"member": "Member",
|
||||
"member_role_description": "Members can work in selected projects.",
|
||||
"member_role_info_message": "To give new members access to a project, please add them to a Team below. With Teams you can manage who has access to which project.",
|
||||
"organization_role": "Organization role",
|
||||
"owner_role_description": "Owners have full control over the organization.",
|
||||
"please_fill_all_member_fields": "Please fill all the fields to add a new member.",
|
||||
"please_fill_all_project_fields": "Please fill all the fields to add a new project.",
|
||||
"read": "Read",
|
||||
"read_write": "Read & Write",
|
||||
"select_member": "Select member",
|
||||
"select_project": "Select project",
|
||||
"team_admin": "Team Admin",
|
||||
"team_created_successfully": "Team created successfully.",
|
||||
"team_deleted_successfully": "Team deleted successfully.",
|
||||
@@ -1261,6 +1266,10 @@
|
||||
"bold": "Bold",
|
||||
"brand_color": "Brand color",
|
||||
"brightness": "Brightness",
|
||||
"bulk_edit": "Bulk edit",
|
||||
"bulk_edit_description": "Edit all options below, one per line. Empty lines will be skipped and duplicates removed.",
|
||||
"bulk_edit_options": "Bulk edit options",
|
||||
"bulk_edit_options_for": "Bulk edit options for {language}",
|
||||
"button_external": "Enable External Link",
|
||||
"button_external_description": "Add a button that opens an external URL in a new tab",
|
||||
"button_label": "Button Label",
|
||||
@@ -1507,6 +1516,7 @@
|
||||
"option_used_in_logic_error": "This option is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"optional": "Optional",
|
||||
"options": "Options",
|
||||
"options_used_in_logic_bulk_error": "The following options are used in logic: {questionIndexes}. Please remove them from logic first.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Set custom waiting time",
|
||||
"overwrite_global_waiting_time_description": "Override the project configuration for this survey only.",
|
||||
@@ -1656,6 +1666,7 @@
|
||||
"unsaved_changes_warning": "You have unsaved changes in your survey. Would you like to save them before leaving?",
|
||||
"until_they_submit_a_response": "Ask until they submit a response",
|
||||
"untitled_block": "Untitled Block",
|
||||
"update_options": "Update options",
|
||||
"upgrade_notice_description": "Create multilingual surveys and unlock many more features",
|
||||
"upgrade_notice_title": "Unlock multi-language surveys with a higher plan",
|
||||
"upload": "Upload",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Máximo",
|
||||
"member": "Miembro",
|
||||
"members": "Miembros",
|
||||
"members_and_teams": "Miembros y equipos",
|
||||
"membership_not_found": "Membresía no encontrada",
|
||||
"metadata": "Metadatos",
|
||||
"minimum": "Mínimo",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Leer documentación",
|
||||
"recipients": "Destinatarios",
|
||||
"remove": "Eliminar",
|
||||
"remove_from_team": "Eliminar del equipo",
|
||||
"reorder_and_hide_columns": "Reordenar y ocultar columnas",
|
||||
"report_survey": "Reportar encuesta",
|
||||
"request_pricing": "Solicitar precios",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Acceso de equipo",
|
||||
"team_id": "ID de equipo",
|
||||
"team_name": "Nombre del equipo",
|
||||
"teams": "Control de acceso",
|
||||
"team_role": "Rol del equipo",
|
||||
"teams": "Equipos",
|
||||
"teams_not_found": "Equipos no encontrados",
|
||||
"text": "Texto",
|
||||
"time": "Hora",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Gestionar equipos",
|
||||
"no_teams_found": "No se han encontrado equipos",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Solo los propietarios y gestores de la organización pueden gestionar equipos.",
|
||||
"permission": "Permiso",
|
||||
"team_name": "Nombre del equipo",
|
||||
"team_settings_description": "Consulta qué equipos pueden acceder a este proyecto."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Gestionar equipo",
|
||||
"manage_team_disabled": "Solo los propietarios de la organización, gestores y administradores de equipo pueden gestionar equipos.",
|
||||
"manager_role_description": "Los gestores pueden acceder a todos los proyectos y añadir y eliminar miembros.",
|
||||
"member": "Miembro",
|
||||
"member_role_description": "Los miembros pueden trabajar en proyectos seleccionados.",
|
||||
"member_role_info_message": "Para dar a los nuevos miembros acceso a un proyecto, por favor añádelos a un equipo a continuación. Con los equipos puedes gestionar quién tiene acceso a qué proyecto.",
|
||||
"organization_role": "Rol en la organización",
|
||||
"owner_role_description": "Los propietarios tienen control total sobre la organización.",
|
||||
"please_fill_all_member_fields": "Por favor, rellena todos los campos para añadir un nuevo miembro.",
|
||||
"please_fill_all_project_fields": "Por favor, rellena todos los campos para añadir un nuevo proyecto.",
|
||||
"read": "Lectura",
|
||||
"read_write": "Lectura y escritura",
|
||||
"select_member": "Seleccionar miembro",
|
||||
"select_project": "Seleccionar proyecto",
|
||||
"team_admin": "Administrador de equipo",
|
||||
"team_created_successfully": "Equipo creado con éxito.",
|
||||
"team_deleted_successfully": "Equipo eliminado correctamente.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Negrita",
|
||||
"brand_color": "Color de marca",
|
||||
"brightness": "Brillo",
|
||||
"bulk_edit": "Edición masiva",
|
||||
"bulk_edit_description": "Edita todas las opciones a continuación, una por línea. Las líneas vacías se omitirán y los duplicados se eliminarán.",
|
||||
"bulk_edit_options": "Edición masiva de opciones",
|
||||
"bulk_edit_options_for": "Edición masiva de opciones para {language}",
|
||||
"button_external": "Habilitar enlace externo",
|
||||
"button_external_description": "Añadir un botón que abre una URL externa en una nueva pestaña",
|
||||
"button_label": "Etiqueta del botón",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Esta opción se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opciones",
|
||||
"options_used_in_logic_bulk_error": "Las siguientes opciones se utilizan en la lógica: {questionIndexes}. Por favor, elimínalas de la lógica primero.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Anular el tema con estilos individuales para esta encuesta.",
|
||||
"overwrite_global_waiting_time": "Establecer tiempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Anular la configuración del proyecto solo para esta encuesta.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Tienes cambios sin guardar en tu encuesta. ¿Quieres guardarlos antes de salir?",
|
||||
"until_they_submit_a_response": "Preguntar hasta que envíen una respuesta",
|
||||
"untitled_block": "Bloque sin título",
|
||||
"update_options": "Actualizar opciones",
|
||||
"upgrade_notice_description": "Crea encuestas multilingües y desbloquea muchas más funciones",
|
||||
"upgrade_notice_title": "Desbloquea encuestas multilingües con un plan superior",
|
||||
"upload": "Subir",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Max",
|
||||
"member": "Membre",
|
||||
"members": "Membres",
|
||||
"members_and_teams": "Membres & Équipes",
|
||||
"membership_not_found": "Abonnement non trouvé",
|
||||
"metadata": "Métadonnées",
|
||||
"minimum": "Min",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Lire les documents",
|
||||
"recipients": "Destinataires",
|
||||
"remove": "Retirer",
|
||||
"remove_from_team": "Retirer de l'équipe",
|
||||
"reorder_and_hide_columns": "Réorganiser et masquer des colonnes",
|
||||
"report_survey": "Rapport d'enquête",
|
||||
"request_pricing": "Connaître le tarif",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Accès",
|
||||
"team_id": "Identifiant de l'équipe",
|
||||
"team_name": "Nom de l'équipe",
|
||||
"teams": "Contrôle d'accès",
|
||||
"team_role": "Rôle dans l'équipe",
|
||||
"teams": "Équipes",
|
||||
"teams_not_found": "Équipes non trouvées",
|
||||
"text": "Texte",
|
||||
"time": "Temps",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Gérer les équipes",
|
||||
"no_teams_found": "Aucune équipe trouvée",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Seuls les propriétaires et les gestionnaires de l'organisation peuvent gérer les équipes.",
|
||||
"permission": "Permission",
|
||||
"team_name": "Nom de l'équipe",
|
||||
"team_settings_description": "Vous pouvez consulter la liste des équipes qui ont accès à ce projet."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Gérer l'équipe",
|
||||
"manage_team_disabled": "Seuls les propriétaires de l'organisation, les gestionnaires et les administrateurs d'équipe peuvent gérer les équipes.",
|
||||
"manager_role_description": "Les gestionnaires peuvent accéder à tous les projets et ajouter et supprimer des membres.",
|
||||
"member": "Membre",
|
||||
"member_role_description": "Les membres peuvent travailler sur des projets sélectionnés.",
|
||||
"member_role_info_message": "Pour donner accès à un projet aux nouveaux membres, veuillez les ajouter à une équipe ci-dessous. Avec les équipes, vous pouvez gérer qui a accès à quel projet.",
|
||||
"organization_role": "Rôle dans l'organisation",
|
||||
"owner_role_description": "Les propriétaires ont un contrôle total sur l'organisation.",
|
||||
"please_fill_all_member_fields": "Veuillez remplir tous les champs pour ajouter un nouveau membre.",
|
||||
"please_fill_all_project_fields": "Veuillez remplir tous les champs pour ajouter un nouveau projet.",
|
||||
"read": "Lire",
|
||||
"read_write": "Lire et Écrire",
|
||||
"select_member": "Sélectionner membre",
|
||||
"select_project": "Sélectionner projet",
|
||||
"team_admin": "Administrateur d'équipe",
|
||||
"team_created_successfully": "Équipe créée avec succès.",
|
||||
"team_deleted_successfully": "Équipe supprimée avec succès.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Gras",
|
||||
"brand_color": "Couleur de marque",
|
||||
"brightness": "Luminosité",
|
||||
"bulk_edit": "Modification en masse",
|
||||
"bulk_edit_description": "Modifiez toutes les options ci-dessous, une par ligne. Les lignes vides seront ignorées et les doublons supprimés.",
|
||||
"bulk_edit_options": "Modifier les options en masse",
|
||||
"bulk_edit_options_for": "Modifier les options en masse pour {language}",
|
||||
"button_external": "Activer le lien externe",
|
||||
"button_external_description": "Ajouter un bouton qui ouvre une URL externe dans un nouvel onglet",
|
||||
"button_label": "Label du bouton",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Cette option est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"optional": "Optionnel",
|
||||
"options": "Options",
|
||||
"options_used_in_logic_bulk_error": "Les options suivantes sont utilisées dans la logique : {questionIndexes}. Veuillez d'abord les supprimer de la logique.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Définir un temps d'attente personnalisé",
|
||||
"overwrite_global_waiting_time_description": "Remplacer la configuration du projet pour cette enquête uniquement.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Vous avez des modifications non enregistrées dans votre enquête. Souhaitez-vous les enregistrer avant de partir ?",
|
||||
"until_they_submit_a_response": "Demander jusqu'à ce qu'ils soumettent une réponse",
|
||||
"untitled_block": "Bloc sans titre",
|
||||
"update_options": "Mettre à jour les options",
|
||||
"upgrade_notice_description": "Créez des sondages multilingues et débloquez de nombreuses autres fonctionnalités",
|
||||
"upgrade_notice_title": "Débloquez les sondages multilingues avec un plan supérieur",
|
||||
"upload": "Télécharger",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "最大",
|
||||
"member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"members_and_teams": "メンバー&チーム",
|
||||
"membership_not_found": "メンバーシップが見つかりません",
|
||||
"metadata": "メタデータ",
|
||||
"minimum": "最小",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "ドキュメントを読む",
|
||||
"recipients": "受信者",
|
||||
"remove": "削除",
|
||||
"remove_from_team": "チームから削除",
|
||||
"reorder_and_hide_columns": "列の並び替えと非表示",
|
||||
"report_survey": "フォームを報告",
|
||||
"request_pricing": "料金を問い合わせる",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "チームアクセス",
|
||||
"team_id": "チームID",
|
||||
"team_name": "チーム名",
|
||||
"teams": "アクセス制御",
|
||||
"team_role": "チームの役割",
|
||||
"teams": "チーム",
|
||||
"teams_not_found": "チームが見つかりません",
|
||||
"text": "テキスト",
|
||||
"time": "時間",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "チームを管理",
|
||||
"no_teams_found": "チームが見つかりません",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "組織のオーナーまたは管理者のみがチームを管理できます。",
|
||||
"permission": "権限",
|
||||
"team_name": "チーム名",
|
||||
"team_settings_description": "このプロジェクトにアクセスできるチームを確認します。"
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "チームを管理",
|
||||
"manage_team_disabled": "組織のオーナー、管理者、チーム管理者のみがチームを管理できます。",
|
||||
"manager_role_description": "管理者はすべてのプロジェクトにアクセスでき、メンバーを追加および削除できます。",
|
||||
"member": "メンバー",
|
||||
"member_role_description": "メンバーは選択されたプロジェクトで作業できます。",
|
||||
"member_role_info_message": "新しいメンバーにプロジェクトへのアクセス権を付与するには、以下のチームに追加してください。チームを使用すると、誰がどのプロジェクトにアクセスできるかを管理できます。",
|
||||
"organization_role": "組織の役割",
|
||||
"owner_role_description": "オーナーは組織を完全に制御できます。",
|
||||
"please_fill_all_member_fields": "新しいメンバーを追加するには、すべてのフィールドを記入してください。",
|
||||
"please_fill_all_project_fields": "新しいプロジェクトを追加するには、すべてのフィールドを記入してください。",
|
||||
"read": "読み取り",
|
||||
"read_write": "読み書き",
|
||||
"select_member": "メンバーを選択",
|
||||
"select_project": "プロジェクトを選択",
|
||||
"team_admin": "チーム管理者",
|
||||
"team_created_successfully": "チームを正常に作成しました。",
|
||||
"team_deleted_successfully": "チームを正常に削除しました。",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "太字",
|
||||
"brand_color": "ブランドカラー",
|
||||
"brightness": "明るさ",
|
||||
"bulk_edit": "一括編集",
|
||||
"bulk_edit_description": "以下のオプションを1行ずつ編集してください。空の行はスキップされ、重複は削除されます。",
|
||||
"bulk_edit_options": "オプションの一括編集",
|
||||
"bulk_edit_options_for": "{language}のオプションを一括編集",
|
||||
"button_external": "外部リンクを有効にする",
|
||||
"button_external_description": "新しいタブで外部URLを開くボタンを追加する",
|
||||
"button_label": "ボタンのラベル",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "このオプションは質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"optional": "オプション",
|
||||
"options": "オプション",
|
||||
"options_used_in_logic_bulk_error": "以下のオプションはロジックで使用されています:{questionIndexes}。まず、ロジックから削除してください。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "このフォームの個別のスタイルでテーマを上書きします。",
|
||||
"overwrite_global_waiting_time": "カスタム待機時間を設定する",
|
||||
"overwrite_global_waiting_time_description": "このフォームのみプロジェクト設定を上書きします。",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "フォームに未保存の変更があります。離れる前に保存しますか?",
|
||||
"until_they_submit_a_response": "回答が提出されるまで質問する",
|
||||
"untitled_block": "無題のブロック",
|
||||
"update_options": "オプションを更新",
|
||||
"upgrade_notice_description": "多言語フォームを作成し、さらに多くの機能をアンロック",
|
||||
"upgrade_notice_title": "上位プランで多言語フォームをアンロック",
|
||||
"upload": "アップロード",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Maximaal",
|
||||
"member": "Lid",
|
||||
"members": "Leden",
|
||||
"members_and_teams": "Leden & teams",
|
||||
"membership_not_found": "Lidmaatschap niet gevonden",
|
||||
"metadata": "Metagegevens",
|
||||
"minimum": "Minimum",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Lees Documenten",
|
||||
"recipients": "Ontvangers",
|
||||
"remove": "Verwijderen",
|
||||
"remove_from_team": "Verwijderen uit team",
|
||||
"reorder_and_hide_columns": "Kolommen opnieuw rangschikken en verbergen",
|
||||
"report_survey": "Verslag enquête",
|
||||
"request_pricing": "Vraag prijzen aan",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Teamtoegang",
|
||||
"team_id": "Team-ID",
|
||||
"team_name": "Teamnaam",
|
||||
"teams": "Toegangscontrole",
|
||||
"team_role": "Teamrol",
|
||||
"teams": "Teams",
|
||||
"teams_not_found": "Teams niet gevonden",
|
||||
"text": "Tekst",
|
||||
"time": "Tijd",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Beheer teams",
|
||||
"no_teams_found": "Geen teams gevonden",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Alleen eigenaren en managers van organisaties kunnen teams beheren.",
|
||||
"permission": "Toestemming",
|
||||
"team_name": "Teamnaam",
|
||||
"team_settings_description": "Bekijk welke teams toegang hebben tot dit project."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Beheer team",
|
||||
"manage_team_disabled": "Alleen organisatie-eigenaren, managers en teambeheerders kunnen teams beheren.",
|
||||
"manager_role_description": "Managers hebben toegang tot alle projecten en kunnen leden toevoegen en verwijderen.",
|
||||
"member": "Lid",
|
||||
"member_role_description": "Leden kunnen in geselecteerde projecten werken.",
|
||||
"member_role_info_message": "Om nieuwe leden toegang te geven tot een project, voegt u ze hieronder toe aan een team. Met Teams kun je beheren wie toegang heeft tot welk project.",
|
||||
"organization_role": "Organisatierol",
|
||||
"owner_role_description": "Eigenaars hebben volledige controle over de organisatie.",
|
||||
"please_fill_all_member_fields": "Vul alle velden in om een nieuw lid toe te voegen.",
|
||||
"please_fill_all_project_fields": "Vul alle velden in om een nieuw project toe te voegen.",
|
||||
"read": "Lezen",
|
||||
"read_write": "Lezen en schrijven",
|
||||
"select_member": "Selecteer lid",
|
||||
"select_project": "Selecteer project",
|
||||
"team_admin": "Teambeheerder",
|
||||
"team_created_successfully": "Team succesvol aangemaakt.",
|
||||
"team_deleted_successfully": "Team succesvol verwijderd.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Vetgedrukt",
|
||||
"brand_color": "Merk kleur",
|
||||
"brightness": "Helderheid",
|
||||
"bulk_edit": "Bulkbewerking",
|
||||
"bulk_edit_description": "Bewerk alle opties hieronder, één per regel. Lege regels worden overgeslagen en duplicaten verwijderd.",
|
||||
"bulk_edit_options": "Opties bulkbewerken",
|
||||
"bulk_edit_options_for": "Opties bulkbewerken voor {language}",
|
||||
"button_external": "Externe link inschakelen",
|
||||
"button_external_description": "Voeg een knop toe die een externe URL in een nieuw tabblad opent",
|
||||
"button_label": "Knoplabel",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Deze optie wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||
"optional": "Optioneel",
|
||||
"options": "Opties",
|
||||
"options_used_in_logic_bulk_error": "De volgende opties worden gebruikt in logica: {questionIndexes}. Verwijder ze eerst uit de logica.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Overschrijf het thema met individuele stijlen voor deze enquête.",
|
||||
"overwrite_global_waiting_time": "Stel aangepaste wachttijd in",
|
||||
"overwrite_global_waiting_time_description": "Overschrijf de projectconfiguratie alleen voor deze enquête.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Er zijn niet-opgeslagen wijzigingen in uw enquête. Wilt u ze bewaren voordat u vertrekt?",
|
||||
"until_they_submit_a_response": "Vraag totdat ze een reactie indienen",
|
||||
"untitled_block": "Naamloos blok",
|
||||
"update_options": "Opties bijwerken",
|
||||
"upgrade_notice_description": "Creëer meertalige enquêtes en ontgrendel nog veel meer functies",
|
||||
"upgrade_notice_title": "Ontgrendel meertalige enquêtes met een hoger plan",
|
||||
"upload": "Uploaden",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Máximo",
|
||||
"member": "Membros",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipes",
|
||||
"membership_not_found": "Assinatura não encontrada",
|
||||
"metadata": "metadados",
|
||||
"minimum": "Mínimo",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Ler Documentação",
|
||||
"recipients": "Destinatários",
|
||||
"remove": "remover",
|
||||
"remove_from_team": "Remover da equipe",
|
||||
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
|
||||
"report_survey": "Relatório de Pesquisa",
|
||||
"request_pricing": "Solicitar Preços",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Acesso da equipe",
|
||||
"team_id": "ID da Equipe",
|
||||
"team_name": "Nome da equipe",
|
||||
"teams": "Controle de Acesso",
|
||||
"team_role": "Função na equipe",
|
||||
"teams": "Equipes",
|
||||
"teams_not_found": "Equipes não encontradas",
|
||||
"text": "Texto",
|
||||
"time": "tempo",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Gerenciar Equipes",
|
||||
"no_teams_found": "Nenhuma equipe encontrada",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Apenas proprietários e gerentes da organização podem gerenciar equipes.",
|
||||
"permission": "Permissão",
|
||||
"team_name": "Nome da equipe",
|
||||
"team_settings_description": "As equipes e seus membros podem acessar este projeto e suas pesquisas. Proprietários e gerentes da organização podem conceder esse acesso."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Gerenciar equipe",
|
||||
"manage_team_disabled": "Apenas proprietários da organização, gerentes e administradores da equipe podem gerenciar equipes.",
|
||||
"manager_role_description": "Os gerentes podem acessar todos os projetos e adicionar e remover membros.",
|
||||
"member": "Membro",
|
||||
"member_role_description": "Os membros podem trabalhar em projetos selecionados.",
|
||||
"member_role_info_message": "Para dar acesso a novos membros a um projeto, por favor, adicione-os a uma equipe abaixo. Com equipes, você pode gerenciar quem tem acesso a qual projeto.",
|
||||
"organization_role": "Função na organização",
|
||||
"owner_role_description": "Os proprietários têm controle total sobre a organização.",
|
||||
"please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.",
|
||||
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
|
||||
"read": "Leitura",
|
||||
"read_write": "Leitura & Escrita",
|
||||
"select_member": "Selecionar membro",
|
||||
"select_project": "Selecionar projeto",
|
||||
"team_admin": "Administrador da equipe",
|
||||
"team_created_successfully": "Equipe criada com sucesso.",
|
||||
"team_deleted_successfully": "Equipe excluída com sucesso.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brightness": "brilho",
|
||||
"bulk_edit": "Edição em massa",
|
||||
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicatas removidas.",
|
||||
"bulk_edit_options": "Editar opções em massa",
|
||||
"bulk_edit_options_for": "Editar opções em massa para {language}",
|
||||
"button_external": "Habilitar link externo",
|
||||
"button_external_description": "Adicionar um botão que abre uma URL externa em uma nova aba",
|
||||
"button_label": "Rótulo do Botão",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substitua o tema com estilos individuais para essa pesquisa.",
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para esta pesquisa.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Você tem alterações não salvas na sua pesquisa. Quer salvar antes de sair?",
|
||||
"until_they_submit_a_response": "Perguntar até que enviem uma resposta",
|
||||
"untitled_block": "Bloco sem título",
|
||||
"update_options": "Atualizar opções",
|
||||
"upgrade_notice_description": "Crie pesquisas multilíngues e desbloqueie muitas outras funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie pesquisas multilíngues com um plano superior",
|
||||
"upload": "Enviar",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Máximo",
|
||||
"member": "Membro",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipas",
|
||||
"membership_not_found": "Associação não encontrada",
|
||||
"metadata": "Metadados",
|
||||
"minimum": "Mínimo",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Ler Documentos",
|
||||
"recipients": "Destinatários",
|
||||
"remove": "Remover",
|
||||
"remove_from_team": "Remover da equipa",
|
||||
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
|
||||
"report_survey": "Relatório de Inquérito",
|
||||
"request_pricing": "Pedido de Preços",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Acesso da Equipa",
|
||||
"team_id": "ID da Equipa",
|
||||
"team_name": "Nome da equipa",
|
||||
"teams": "Controlo de Acesso",
|
||||
"team_role": "Função na equipa",
|
||||
"teams": "Equipas",
|
||||
"teams_not_found": "Equipas não encontradas",
|
||||
"text": "Texto",
|
||||
"time": "Tempo",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Gerir equipas",
|
||||
"no_teams_found": "Nenhuma equipa encontrada",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Apenas os proprietários e gestores da organização podem gerir equipas.",
|
||||
"permission": "Permissão",
|
||||
"team_name": "Nome da Equipa",
|
||||
"team_settings_description": "Veja quais equipas podem aceder a este projeto."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Gerir equipa",
|
||||
"manage_team_disabled": "Apenas os proprietários da organização, gestores e administradores de equipa podem gerir equipas.",
|
||||
"manager_role_description": "Os gestores podem aceder a todos os projetos e adicionar e remover membros.",
|
||||
"member": "Membro",
|
||||
"member_role_description": "Os membros podem trabalhar em projetos selecionados.",
|
||||
"member_role_info_message": "Adicione os membros que deseja a uma Equipa abaixo. Nesta secção, pode gerir quem tem acesso a cada projeto.",
|
||||
"organization_role": "Função na organização",
|
||||
"owner_role_description": "Os proprietários têm controlo total sobre a organização.",
|
||||
"please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.",
|
||||
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
|
||||
"read": "Ler",
|
||||
"read_write": "Ler e Escrever",
|
||||
"select_member": "Selecionar membro",
|
||||
"select_project": "Selecionar projeto",
|
||||
"team_admin": "Administrador da Equipa",
|
||||
"team_created_successfully": "Equipa criada com sucesso.",
|
||||
"team_deleted_successfully": "Equipa eliminada com sucesso.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brightness": "Brilho",
|
||||
"bulk_edit": "Edição em massa",
|
||||
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicados removidos.",
|
||||
"bulk_edit_options": "Editar opções em massa",
|
||||
"bulk_edit_options_for": "Editar opções em massa para {language}",
|
||||
"button_external": "Ativar link externo",
|
||||
"button_external_description": "Adicionar um botão que abre um URL externo num novo separador",
|
||||
"button_label": "Rótulo do botão",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substituir o tema com estilos individuais para este inquérito.",
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para este inquérito.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Tem alterações não guardadas no seu inquérito. Gostaria de as guardar antes de sair?",
|
||||
"until_they_submit_a_response": "Perguntar até que submetam uma resposta",
|
||||
"untitled_block": "Bloco sem título",
|
||||
"update_options": "Atualizar opções",
|
||||
"upgrade_notice_description": "Crie inquéritos multilingues e desbloqueie muitas mais funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie inquéritos multilingues com um plano superior",
|
||||
"upload": "Carregar",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Maximum",
|
||||
"member": "Membru",
|
||||
"members": "Membri",
|
||||
"members_and_teams": "Membri și echipe",
|
||||
"membership_not_found": "Apartenența nu a fost găsită",
|
||||
"metadata": "Metadate",
|
||||
"minimum": "Minim",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Citește documentația",
|
||||
"recipients": "Destinatari",
|
||||
"remove": "Șterge",
|
||||
"remove_from_team": "Elimină din echipă",
|
||||
"reorder_and_hide_columns": "Reordonați și ascundeți coloanele",
|
||||
"report_survey": "Raportează chestionarul",
|
||||
"request_pricing": "Solicită Prețuri",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "Acces echipă",
|
||||
"team_id": "ID echipă",
|
||||
"team_name": "Nume echipă",
|
||||
"teams": "Control acces",
|
||||
"team_role": "Rol în echipă",
|
||||
"teams": "Echipe",
|
||||
"teams_not_found": "Echipele nu au fost găsite",
|
||||
"text": "Text",
|
||||
"time": "Timp",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Gestionați echipele",
|
||||
"no_teams_found": "Nicio echipă găsită",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Doar proprietarii de organizație și managerii pot gestiona echipele.",
|
||||
"permission": "Permisiune",
|
||||
"team_name": "Nume echipă",
|
||||
"team_settings_description": "Vezi care echipe pot accesa acest proiect."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Gestionați echipa",
|
||||
"manage_team_disabled": "Doar proprietarii de organizații, managerii și administratorii de echipă pot gestiona echipele.",
|
||||
"manager_role_description": "Managerii pot accesa toate proiectele și pot adăuga sau elimina membri.",
|
||||
"member": "Membru",
|
||||
"member_role_description": "Membrii pot lucra în proiectele selectate.",
|
||||
"member_role_info_message": "Pentru a oferi membrilor noi acces la un proiect, vă rugăm să-i adăugați la o Echipă mai jos. Cu Echipe puteți gestiona cine are acces la ce proiect.",
|
||||
"organization_role": "Rol în organizație",
|
||||
"owner_role_description": "Proprietarii au control total asupra organizației.",
|
||||
"please_fill_all_member_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un nou membru.",
|
||||
"please_fill_all_project_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un proiect nou.",
|
||||
"read": "Citește",
|
||||
"read_write": "Citire & Scriere",
|
||||
"select_member": "Selectează membrul",
|
||||
"select_project": "Selectează proiectul",
|
||||
"team_admin": "Administrator Echipe",
|
||||
"team_created_successfully": "Echipă creată cu succes",
|
||||
"team_deleted_successfully": "Echipă ștearsă cu succes.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Îngroșat",
|
||||
"brand_color": "Culoarea brandului",
|
||||
"brightness": "Luminozitate",
|
||||
"bulk_edit": "Editare în bloc",
|
||||
"bulk_edit_description": "Editați toate opțiunile de mai jos, câte una pe linie. Liniile goale vor fi omise, iar duplicatele vor fi eliminate.",
|
||||
"bulk_edit_options": "Opțiuni de editare în bloc",
|
||||
"bulk_edit_options_for": "Editare în bloc a opțiunilor pentru {language}",
|
||||
"button_external": "Activează link extern",
|
||||
"button_external_description": "Adaugă un buton care deschide un URL extern într-o filă nouă",
|
||||
"button_label": "Etichetă buton",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Această opțiune este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"optional": "Opțional",
|
||||
"options": "Opțiuni",
|
||||
"options_used_in_logic_bulk_error": "Următoarele opțiuni sunt folosite în logică: {questionIndexes}. Vă rugăm să le eliminați din logică mai întâi.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Suprascrie tema cu stiluri individuale pentru acest sondaj.",
|
||||
"overwrite_global_waiting_time": "Setează un timp de așteptare personalizat",
|
||||
"overwrite_global_waiting_time_description": "Suprascrie configurația proiectului doar pentru acest sondaj.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Aveți modificări nesalvate în sondajul dumneavoastră. Doriți să le salvați înainte de a pleca?",
|
||||
"until_they_submit_a_response": "Întreabă până când trimit un răspuns",
|
||||
"untitled_block": "Bloc fără titlu",
|
||||
"update_options": "Actualizați opțiunile",
|
||||
"upgrade_notice_description": "Creați sondaje multilingve și deblocați multe alte caracteristici",
|
||||
"upgrade_notice_title": "Deblocați sondajele multilingve cu un plan superior",
|
||||
"upload": "Încărcați",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "Maximum",
|
||||
"member": "Medlem",
|
||||
"members": "Medlemmar",
|
||||
"members_and_teams": "Medlemmar och team",
|
||||
"membership_not_found": "Medlemskap hittades inte",
|
||||
"metadata": "Metadata",
|
||||
"minimum": "Minimum",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "Läs dokumentation",
|
||||
"recipients": "Mottagare",
|
||||
"remove": "Ta bort",
|
||||
"remove_from_team": "Ta bort från teamet",
|
||||
"reorder_and_hide_columns": "Ordna om och dölj kolumner",
|
||||
"report_survey": "Rapportera enkät",
|
||||
"request_pricing": "Begär prissättning",
|
||||
@@ -407,6 +409,7 @@
|
||||
"team_access": "Teamåtkomst",
|
||||
"team_id": "Team-ID",
|
||||
"team_name": "Teamnamn",
|
||||
"team_role": "Teamroll",
|
||||
"teams": "Åtkomstkontroll",
|
||||
"teams_not_found": "Team hittades inte",
|
||||
"text": "Text",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "Hantera team",
|
||||
"no_teams_found": "Inga team hittades",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "Endast organisationsägare och administratörer kan hantera team.",
|
||||
"permission": "Behörighet",
|
||||
"team_name": "Teamnamn",
|
||||
"team_settings_description": "Se vilka team som kan komma åt detta projekt."
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "Hantera team",
|
||||
"manage_team_disabled": "Endast organisationsägare, administratörer och teamadministratörer kan hantera team.",
|
||||
"manager_role_description": "Administratörer kan komma åt alla projekt och lägga till och ta bort medlemmar.",
|
||||
"member": "Medlem",
|
||||
"member_role_description": "Medlemmar kan arbeta i valda projekt.",
|
||||
"member_role_info_message": "För att ge nya medlemmar åtkomst till ett projekt, vänligen lägg till dem i ett team nedan. Med team kan du hantera vem som har åtkomst till vilket projekt.",
|
||||
"organization_role": "Organisationsroll",
|
||||
"owner_role_description": "Ägare har full kontroll över organisationen.",
|
||||
"please_fill_all_member_fields": "Vänligen fyll i alla fält för att lägga till en ny medlem.",
|
||||
"please_fill_all_project_fields": "Vänligen fyll i alla fält för att lägga till ett nytt projekt.",
|
||||
"read": "Läs",
|
||||
"read_write": "Läs och skriv",
|
||||
"select_member": "Välj medlem",
|
||||
"select_project": "Välj projekt",
|
||||
"team_admin": "Teamadministratör",
|
||||
"team_created_successfully": "Team skapat.",
|
||||
"team_deleted_successfully": "Team borttaget.",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "Fet",
|
||||
"brand_color": "Varumärkesfärg",
|
||||
"brightness": "Ljusstyrka",
|
||||
"bulk_edit": "Massredigera",
|
||||
"bulk_edit_description": "Redigera alla alternativ nedan, ett per rad. Tomma rader kommer att hoppas över och dubbletter tas bort.",
|
||||
"bulk_edit_options": "Massredigera alternativ",
|
||||
"bulk_edit_options_for": "Massredigera alternativ för {language}",
|
||||
"button_external": "Aktivera extern länk",
|
||||
"button_external_description": "Lägg till en knapp som öppnar en extern URL i en ny flik",
|
||||
"button_label": "Knappetikett",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "Detta alternativ används i logiken för fråga {questionIndex}. Vänligen ta bort det från logiken först.",
|
||||
"optional": "Valfritt",
|
||||
"options": "Alternativ",
|
||||
"options_used_in_logic_bulk_error": "Följande alternativ används i logiken: {questionIndexes}. Vänligen ta bort dem från logiken först.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Åsidosätt temat med individuella stilar för denna enkät.",
|
||||
"overwrite_global_waiting_time": "Ställ in anpassad väntetid",
|
||||
"overwrite_global_waiting_time_description": "Åsidosätt projektkonfigurationen endast för denna enkät.",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "Du har osparade ändringar i din enkät. Vill du spara dem innan du lämnar?",
|
||||
"until_they_submit_a_response": "Fråga tills de skickar in ett svar",
|
||||
"untitled_block": "Namnlöst block",
|
||||
"update_options": "Uppdatera alternativ",
|
||||
"upgrade_notice_description": "Skapa flerspråkiga enkäter och lås upp många fler funktioner",
|
||||
"upgrade_notice_title": "Lås upp flerspråkiga enkäter med en högre plan",
|
||||
"upload": "Ladda upp",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "最大值",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"members_and_teams": "成员和团队",
|
||||
"membership_not_found": "未找到会员资格",
|
||||
"metadata": "元数据",
|
||||
"minimum": "最低",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "阅读 文档",
|
||||
"recipients": "收件人",
|
||||
"remove": "移除",
|
||||
"remove_from_team": "从团队中移除",
|
||||
"reorder_and_hide_columns": "重新排序和隐藏列",
|
||||
"report_survey": "报告调查",
|
||||
"request_pricing": "请求 定价",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "团队 访问",
|
||||
"team_id": "团队 ID",
|
||||
"team_name": "团队 名称",
|
||||
"teams": "访问控制",
|
||||
"team_role": "团队角色",
|
||||
"teams": "团队",
|
||||
"teams_not_found": "未找到 团队",
|
||||
"text": "文本",
|
||||
"time": "时间",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "管理 团队",
|
||||
"no_teams_found": "未找到 团队",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "只有 组织 拥有者 和 经理 可以 管理 团队。",
|
||||
"permission": "权限",
|
||||
"team_name": "团队名称",
|
||||
"team_settings_description": "查看 哪些 团队 可以 访问 该 项目。"
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "管理团队",
|
||||
"manage_team_disabled": "只有 组织 拥有者、经理 和 团队 管理员 可以 管理 团队。",
|
||||
"manager_role_description": "经理 可以 访问 所有 项目 并 添加 移除 成员。",
|
||||
"member": "成员",
|
||||
"member_role_description": "成员 可以 在 选定 项目 中 工作。",
|
||||
"member_role_info_message": "要 给 新 成员 访问 项目 ,请 将 他们 添加 到 下方 的 团队 。通过 团队 你 可以 管理 谁 可以 访问 哪个 项目 。",
|
||||
"organization_role": "组织角色",
|
||||
"owner_role_description": "所有者拥有对组织的完全控制权。",
|
||||
"please_fill_all_member_fields": "请 填写 所有 字段 以 添加 新 成员。",
|
||||
"please_fill_all_project_fields": "请 填写 所有 字段 以 添加 新 项目。",
|
||||
"read": "阅读",
|
||||
"read_write": "读 & 写",
|
||||
"select_member": "选择成员",
|
||||
"select_project": "选择项目",
|
||||
"team_admin": "团队管理员",
|
||||
"team_created_successfully": "团队 创建 成功",
|
||||
"team_deleted_successfully": "团队 删除 成功",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "粗体",
|
||||
"brand_color": "品牌 颜色",
|
||||
"brightness": "亮度",
|
||||
"bulk_edit": "批量编辑",
|
||||
"bulk_edit_description": "编辑以下所有选项,每行一个。空行将被跳过,重复项将被移除。",
|
||||
"bulk_edit_options": "批量编辑选项",
|
||||
"bulk_edit_options_for": "为 {language} 批量编辑选项",
|
||||
"button_external": "启用外部链接",
|
||||
"button_external_description": "添加一个按钮,在新标签页中打开外部URL",
|
||||
"button_label": "按钮标签",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "\"这个 选项 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"optional": "可选",
|
||||
"options": "选项",
|
||||
"options_used_in_logic_bulk_error": "以下选项在逻辑中被使用:{questionIndexes}。请先从逻辑中删除它们。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用 个性化 样式 替代 这份 问卷 的 主题。",
|
||||
"overwrite_global_waiting_time": "设置自定义等待时间",
|
||||
"overwrite_global_waiting_time_description": "仅为此调查覆盖项目配置。",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "您在调查中有未保存的更改。离开前是否要保存?",
|
||||
"until_they_submit_a_response": "持续显示直到提交回应",
|
||||
"untitled_block": "未命名区块",
|
||||
"update_options": "更新选项",
|
||||
"upgrade_notice_description": "创建 多语言 调查 并 解锁 更多 功能",
|
||||
"upgrade_notice_title": "解锁 更高 计划 中 的 多语言 调查",
|
||||
"upload": "上传",
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
"maximum": "最大值",
|
||||
"member": "成員",
|
||||
"members": "成員",
|
||||
"members_and_teams": "成員與團隊",
|
||||
"membership_not_found": "找不到成員資格",
|
||||
"metadata": "元數據",
|
||||
"minimum": "最小值",
|
||||
@@ -340,6 +341,7 @@
|
||||
"read_docs": "閱讀文件",
|
||||
"recipients": "收件者",
|
||||
"remove": "移除",
|
||||
"remove_from_team": "從團隊中移除",
|
||||
"reorder_and_hide_columns": "重新排序和隱藏欄位",
|
||||
"report_survey": "報告問卷",
|
||||
"request_pricing": "請求定價",
|
||||
@@ -407,7 +409,8 @@
|
||||
"team_access": "團隊存取權限",
|
||||
"team_id": "團隊 ID",
|
||||
"team_name": "團隊名稱",
|
||||
"teams": "存取控制",
|
||||
"team_role": "團隊角色",
|
||||
"teams": "團隊",
|
||||
"teams_not_found": "找不到團隊",
|
||||
"text": "文字",
|
||||
"time": "時間",
|
||||
@@ -903,7 +906,6 @@
|
||||
"teams": {
|
||||
"manage_teams": "管理團隊",
|
||||
"no_teams_found": "找不到團隊",
|
||||
"only_organization_owners_and_managers_can_manage_teams": "只有組織擁有者和管理員才能管理團隊。",
|
||||
"permission": "權限",
|
||||
"team_name": "團隊名稱",
|
||||
"team_settings_description": "查看哪些團隊可以存取此專案。"
|
||||
@@ -1167,13 +1169,17 @@
|
||||
"manage_team": "管理團隊",
|
||||
"manage_team_disabled": "只有組織擁有者、管理員和團隊管理員才能管理團隊。",
|
||||
"manager_role_description": "管理員可以存取所有專案,並新增和移除成員。",
|
||||
"member": "成員",
|
||||
"member_role_description": "成員可以在選定的專案中工作。",
|
||||
"member_role_info_message": "若要授予新成員存取專案的權限,請將他們新增至下方的團隊。藉由團隊,您可以管理誰可以存取哪些專案。",
|
||||
"organization_role": "組織角色",
|
||||
"owner_role_description": "擁有者對組織具有完全控制權。",
|
||||
"please_fill_all_member_fields": "請填寫所有欄位以新增新成員。",
|
||||
"please_fill_all_project_fields": "請填寫所有欄位以新增新專案。",
|
||||
"read": "讀取",
|
||||
"read_write": "讀取和寫入",
|
||||
"select_member": "選擇成員",
|
||||
"select_project": "選擇專案",
|
||||
"team_admin": "團隊管理員",
|
||||
"team_created_successfully": "團隊已成功建立。",
|
||||
"team_deleted_successfully": "團隊已成功刪除。",
|
||||
@@ -1261,6 +1267,10 @@
|
||||
"bold": "粗體",
|
||||
"brand_color": "品牌顏色",
|
||||
"brightness": "亮度",
|
||||
"bulk_edit": "批次編輯",
|
||||
"bulk_edit_description": "在下方逐行編輯所有選項。空白行將被略過,重複項目將被移除。",
|
||||
"bulk_edit_options": "批次編輯選項",
|
||||
"bulk_edit_options_for": "為 {language} 批次編輯選項",
|
||||
"button_external": "啟用外部連結",
|
||||
"button_external_description": "新增一個按鈕,在新分頁中開啟外部網址",
|
||||
"button_label": "按鈕標籤",
|
||||
@@ -1507,6 +1517,7 @@
|
||||
"option_used_in_logic_error": "此選項用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"optional": "選填",
|
||||
"options": "選項",
|
||||
"options_used_in_logic_bulk_error": "以下選項已用於邏輯中:{questionIndexes}。請先從邏輯中移除它們。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。",
|
||||
"overwrite_global_waiting_time": "設定自訂等待時間",
|
||||
"overwrite_global_waiting_time_description": "僅覆蓋此問卷的專案設定。",
|
||||
@@ -1656,6 +1667,7 @@
|
||||
"unsaved_changes_warning": "您的問卷中有未儲存的變更。您要先儲存它們再離開嗎?",
|
||||
"until_they_submit_a_response": "持續詢問直到提交回應",
|
||||
"untitled_block": "未命名區塊",
|
||||
"update_options": "更新選項",
|
||||
"upgrade_notice_description": "建立多語言問卷並解鎖更多功能",
|
||||
"upgrade_notice_title": "使用更高等級的方案解鎖多語言問卷",
|
||||
"upload": "上傳",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { OrganizationRole } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { CreateMembershipInvite } from "@/modules/auth/signup/types/invites";
|
||||
import { createTeamMembership } from "../team";
|
||||
import { createTeamMembership, getTeamProjectIds } from "../team";
|
||||
|
||||
// Setup all mocks
|
||||
const setupMocks = () => {
|
||||
@@ -31,6 +31,7 @@ const setupMocks = () => {
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -55,7 +56,7 @@ describe("Team Management", () => {
|
||||
describe("createTeamMembership", () => {
|
||||
describe("when user is an admin", () => {
|
||||
test("creates a team membership with admin role", async () => {
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
|
||||
vi.mocked(prisma.teamUser.create).mockResolvedValue(MOCK_TEAM_USER);
|
||||
|
||||
await createTeamMembership(MOCK_INVITE, MOCK_IDS.userId);
|
||||
@@ -90,7 +91,7 @@ describe("Team Management", () => {
|
||||
role: "member" as OrganizationRole,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
|
||||
vi.mocked(prisma.teamUser.create).mockResolvedValue({
|
||||
...MOCK_TEAM_USER,
|
||||
role: "contributor",
|
||||
@@ -110,11 +111,68 @@ describe("Team Management", () => {
|
||||
|
||||
describe("error handling", () => {
|
||||
test("throws error when database operation fails", async () => {
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
|
||||
vi.mocked(prisma.teamUser.create).mockRejectedValue(new Error("Database error"));
|
||||
|
||||
await expect(createTeamMembership(MOCK_INVITE, MOCK_IDS.userId)).rejects.toThrow("Database error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when team does not exist", () => {
|
||||
test("skips membership creation and continues to next team", async () => {
|
||||
const inviteWithMultipleTeams: CreateMembershipInvite = {
|
||||
...MOCK_INVITE,
|
||||
teamIds: ["non-existent-team", MOCK_IDS.teamId],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.team.findUnique)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(MOCK_TEAM as unknown as any);
|
||||
vi.mocked(prisma.teamUser.create).mockResolvedValue(MOCK_TEAM_USER);
|
||||
|
||||
await createTeamMembership(inviteWithMultipleTeams, MOCK_IDS.userId);
|
||||
|
||||
expect(prisma.team.findUnique).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.teamUser.create).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.teamUser.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
teamId: MOCK_IDS.teamId,
|
||||
userId: MOCK_IDS.userId,
|
||||
role: "admin",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTeamProjectIds", () => {
|
||||
test("returns team with projectTeams when team exists", async () => {
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
|
||||
|
||||
const result = await getTeamProjectIds(MOCK_IDS.teamId, MOCK_IDS.organizationId);
|
||||
|
||||
expect(result).toEqual(MOCK_TEAM);
|
||||
expect(prisma.team.findUnique).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: MOCK_IDS.teamId,
|
||||
organizationId: MOCK_IDS.organizationId,
|
||||
},
|
||||
select: {
|
||||
projectTeams: {
|
||||
select: {
|
||||
projectId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("returns null when team does not exist", async () => {
|
||||
vi.mocked(prisma.team.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await getTeamProjectIds(MOCK_IDS.teamId, MOCK_IDS.organizationId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,15 +18,18 @@ export const createTeamMembership = async (invite: CreateMembershipInvite, userI
|
||||
for (const teamId of teamIds) {
|
||||
const team = await getTeamProjectIds(teamId, invite.organizationId);
|
||||
|
||||
if (team) {
|
||||
await prisma.teamUser.create({
|
||||
data: {
|
||||
teamId,
|
||||
userId,
|
||||
role: isOwnerOrManager ? "admin" : "contributor",
|
||||
},
|
||||
});
|
||||
if (!team) {
|
||||
logger.warn({ teamId, userId }, "Team no longer exists during invite acceptance");
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.teamUser.create({
|
||||
data: {
|
||||
teamId,
|
||||
userId,
|
||||
role: isOwnerOrManager ? "admin" : "contributor",
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, `Error creating team membership ${invite.organizationId} ${userId}`);
|
||||
@@ -39,7 +42,10 @@ export const createTeamMembership = async (invite: CreateMembershipInvite, userI
|
||||
};
|
||||
|
||||
export const getTeamProjectIds = reactCache(
|
||||
async (teamId: string, organizationId: string): Promise<{ projectTeams: { projectId: string }[] }> => {
|
||||
async (
|
||||
teamId: string,
|
||||
organizationId: string
|
||||
): Promise<{ projectTeams: { projectId: string }[] } | null> => {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
@@ -55,7 +61,7 @@ export const getTeamProjectIds = reactCache(
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return team;
|
||||
|
||||
@@ -458,21 +458,15 @@ describe("Contacts Lib", () => {
|
||||
attributes: [{ attributeKey: { key: "email", id: "key-1" }, value: "john@example.com" }],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.contact.findMany)
|
||||
.mockResolvedValueOnce([existingContact as any])
|
||||
.mockResolvedValueOnce([{ key: "email", id: "key-1" } as any])
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "userId", id: "key-2" },
|
||||
{ key: "email", id: "key-1" },
|
||||
] as any);
|
||||
|
||||
vi.mocked(prisma.contact.findMany).mockResolvedValueOnce([existingContact as any]);
|
||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttributeKey.findMany)
|
||||
.mockResolvedValueOnce([{ key: "email", id: "key-1" }] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
{ key: "name", id: "key-3" },
|
||||
] as any);
|
||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 2 });
|
||||
|
||||
const result = await createContactsFromCSV(csvData, mockEnvironmentId, "skip", attributeMap);
|
||||
|
||||
@@ -489,25 +483,15 @@ describe("Contacts Lib", () => {
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.contact.findMany)
|
||||
.mockResolvedValueOnce([existingContact as any])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any);
|
||||
|
||||
vi.mocked(prisma.contact.findMany).mockResolvedValueOnce([existingContact as any]);
|
||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttributeKey.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any);
|
||||
|
||||
.mockResolvedValueOnce([{ key: "name", id: "key-3" }] as any);
|
||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 1 });
|
||||
vi.mocked(prisma.contact.update).mockResolvedValue(existingContact as any);
|
||||
|
||||
const result = await createContactsFromCSV(csvData, mockEnvironmentId, "update", attributeMap);
|
||||
@@ -525,25 +509,15 @@ describe("Contacts Lib", () => {
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.contact.findMany)
|
||||
.mockResolvedValueOnce([existingContact as any])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any);
|
||||
|
||||
vi.mocked(prisma.contact.findMany).mockResolvedValueOnce([existingContact as any]);
|
||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttributeKey.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any);
|
||||
|
||||
.mockResolvedValueOnce([{ key: "name", id: "key-3" }] as any);
|
||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 1 });
|
||||
vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 2 });
|
||||
vi.mocked(prisma.contact.update).mockResolvedValue(existingContact as any);
|
||||
|
||||
@@ -582,23 +556,16 @@ describe("Contacts Lib", () => {
|
||||
|
||||
test("creates missing attribute keys", async () => {
|
||||
const attributeMap = { email: "email", userId: "userId" };
|
||||
vi.mocked(prisma.contact.findMany)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
] as any);
|
||||
|
||||
vi.mocked(prisma.contact.findMany).mockResolvedValueOnce([]);
|
||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttributeKey.findMany)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{ key: "email", id: "key-1" },
|
||||
{ key: "userId", id: "key-2" },
|
||||
{ key: "name", id: "key-3" },
|
||||
] as any);
|
||||
|
||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 2 });
|
||||
vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 3 });
|
||||
vi.mocked(prisma.contact.create).mockResolvedValue({
|
||||
id: "new-1",
|
||||
environmentId: mockEnvironmentId,
|
||||
|
||||
@@ -200,6 +200,50 @@ export const deleteContact = async (contactId: string): Promise<TContact | null>
|
||||
}
|
||||
};
|
||||
|
||||
// Shared include clause for contact queries
|
||||
const contactAttributesInclude = {
|
||||
attributes: {
|
||||
select: {
|
||||
attributeKey: { select: { key: true } },
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.ContactInclude;
|
||||
|
||||
// Helper to create attribute objects for Prisma create operations
|
||||
const createAttributeConnections = (record: Record<string, string>, environmentId: string) =>
|
||||
Object.entries(record).map(([key, value]) => ({
|
||||
attributeKey: {
|
||||
connect: { key_environmentId: { key, environmentId } },
|
||||
},
|
||||
value,
|
||||
}));
|
||||
|
||||
// Helper to handle userId conflicts when updating/overwriting contacts
|
||||
const resolveUserIdConflict = (
|
||||
mappedRecord: Record<string, string>,
|
||||
existingContact: { id: string; attributes: { attributeKey: { key: string }; value: string }[] },
|
||||
existingUserIds: { value: string; contactId: string }[]
|
||||
): Record<string, string> => {
|
||||
const existingUserId = existingUserIds.find(
|
||||
(attr) => attr.value === mappedRecord.userId && attr.contactId !== existingContact.id
|
||||
);
|
||||
|
||||
if (!existingUserId) {
|
||||
return { ...mappedRecord };
|
||||
}
|
||||
|
||||
const { userId: _userId, ...rest } = mappedRecord;
|
||||
const existingContactUserId = existingContact.attributes.find(
|
||||
(attr) => attr.attributeKey.key === "userId"
|
||||
)?.value;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(existingContactUserId && { userId: existingContactUserId }),
|
||||
};
|
||||
};
|
||||
|
||||
export const createContactsFromCSV = async (
|
||||
csvData: Record<string, string>[],
|
||||
environmentId: string,
|
||||
@@ -287,22 +331,36 @@ export const createContactsFromCSV = async (
|
||||
});
|
||||
|
||||
const attributeKeyMap = new Map<string, string>();
|
||||
// Map from lowercase key to actual DB key (for case-insensitive lookup)
|
||||
const lowercaseToActualKeyMap = new Map<string, string>();
|
||||
|
||||
existingAttributeKeys.forEach((attrKey) => {
|
||||
attributeKeyMap.set(attrKey.key, attrKey.id);
|
||||
lowercaseToActualKeyMap.set(attrKey.key.toLowerCase(), attrKey.key);
|
||||
});
|
||||
|
||||
// Identify missing attribute keys (normalize keys to lowercase)
|
||||
// Collect all unique CSV keys
|
||||
const csvKeys = new Set<string>();
|
||||
csvData.forEach((record) => {
|
||||
Object.keys(record).forEach((key) => csvKeys.add(key.toLowerCase()));
|
||||
Object.keys(record).forEach((key) => csvKeys.add(key));
|
||||
});
|
||||
|
||||
const missingKeys = Array.from(csvKeys).filter((key) => !attributeKeyMap.has(key));
|
||||
// Identify missing attribute keys (case-insensitive check)
|
||||
const missingKeys = Array.from(csvKeys).filter((key) => !lowercaseToActualKeyMap.has(key.toLowerCase()));
|
||||
|
||||
// Create missing attribute keys
|
||||
// Create missing attribute keys (use original CSV casing for new keys)
|
||||
if (missingKeys.length > 0) {
|
||||
// Deduplicate by lowercase to avoid creating duplicates like "firstName" and "firstname"
|
||||
const uniqueMissingKeys = new Map<string, string>();
|
||||
missingKeys.forEach((key) => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (!uniqueMissingKeys.has(lowerKey)) {
|
||||
uniqueMissingKeys.set(lowerKey, key);
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.contactAttributeKey.createMany({
|
||||
data: missingKeys.map((key) => ({
|
||||
data: Array.from(uniqueMissingKeys.values()).map((key) => ({
|
||||
key,
|
||||
name: key,
|
||||
environmentId,
|
||||
@@ -310,10 +368,10 @@ export const createContactsFromCSV = async (
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// Fetch and update the attributeKeyMap with new keys
|
||||
// Fetch and update the maps with new keys
|
||||
const newAttributeKeys = await prisma.contactAttributeKey.findMany({
|
||||
where: {
|
||||
key: { in: missingKeys },
|
||||
key: { in: Array.from(uniqueMissingKeys.values()) },
|
||||
environmentId,
|
||||
},
|
||||
select: { key: true, id: true },
|
||||
@@ -321,6 +379,7 @@ export const createContactsFromCSV = async (
|
||||
|
||||
newAttributeKeys.forEach((attrKey) => {
|
||||
attributeKeyMap.set(attrKey.key, attrKey.id);
|
||||
lowercaseToActualKeyMap.set(attrKey.key.toLowerCase(), attrKey.key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,18 +387,23 @@ export const createContactsFromCSV = async (
|
||||
|
||||
// Process contacts in parallel
|
||||
const contactPromises = csvData.map(async (record) => {
|
||||
// Normalize record keys to lowercase
|
||||
const normalizedRecord: Record<string, string> = {};
|
||||
// Map CSV keys to actual DB keys (case-insensitive matching, preserving DB key casing)
|
||||
const mappedRecord: Record<string, string> = {};
|
||||
Object.entries(record).forEach(([key, value]) => {
|
||||
normalizedRecord[key.toLowerCase()] = value;
|
||||
const actualKey = lowercaseToActualKeyMap.get(key.toLowerCase());
|
||||
if (!actualKey) {
|
||||
// This should never happen since we create missing keys above
|
||||
throw new ValidationError(`Attribute key "${key}" not found in attribute key map`);
|
||||
}
|
||||
mappedRecord[actualKey] = value;
|
||||
});
|
||||
|
||||
// Skip records without email
|
||||
if (!normalizedRecord.email) {
|
||||
if (!mappedRecord.email) {
|
||||
throw new ValidationError("Email is required for all contacts");
|
||||
}
|
||||
|
||||
const existingContact = emailToContactMap.get(normalizedRecord.email);
|
||||
const existingContact = emailToContactMap.get(mappedRecord.email);
|
||||
|
||||
if (existingContact) {
|
||||
// Handle duplicates based on duplicateContactsAction
|
||||
@@ -348,25 +412,7 @@ export const createContactsFromCSV = async (
|
||||
return null;
|
||||
|
||||
case "update": {
|
||||
// if the record has a userId, check if it already exists
|
||||
const existingUserId = existingUserIds.find(
|
||||
(attr) => attr.value === normalizedRecord.userid && attr.contactId !== existingContact.id
|
||||
);
|
||||
let recordToProcess = { ...normalizedRecord };
|
||||
if (existingUserId) {
|
||||
const { userid, ...rest } = recordToProcess;
|
||||
|
||||
const existingContactUserId = existingContact.attributes.find(
|
||||
(attr) => attr.attributeKey.key === "userId"
|
||||
)?.value;
|
||||
|
||||
recordToProcess = {
|
||||
...rest,
|
||||
...(existingContactUserId && {
|
||||
userId: existingContactUserId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const recordToProcess = resolveUserIdConflict(mappedRecord, existingContact, existingUserIds);
|
||||
|
||||
const attributesToUpsert = Object.entries(recordToProcess).map(([key, value]) => ({
|
||||
where: {
|
||||
@@ -383,7 +429,7 @@ export const createContactsFromCSV = async (
|
||||
}));
|
||||
|
||||
// Update contact with upserted attributes
|
||||
const updatedContact = prisma.contact.update({
|
||||
return prisma.contact.update({
|
||||
where: { id: existingContact.id },
|
||||
data: {
|
||||
attributes: {
|
||||
@@ -391,98 +437,40 @@ export const createContactsFromCSV = async (
|
||||
upsert: attributesToUpsert,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attributes: {
|
||||
select: {
|
||||
attributeKey: { select: { key: true } },
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: contactAttributesInclude,
|
||||
});
|
||||
|
||||
return updatedContact;
|
||||
}
|
||||
|
||||
case "overwrite": {
|
||||
// if the record has a userId, check if it already exists
|
||||
const existingUserId = existingUserIds.find(
|
||||
(attr) => attr.value === normalizedRecord.userid && attr.contactId !== existingContact.id
|
||||
);
|
||||
let recordToProcess = { ...normalizedRecord };
|
||||
if (existingUserId) {
|
||||
const { userid, ...rest } = recordToProcess;
|
||||
const existingContactUserId = existingContact.attributes.find(
|
||||
(attr) => attr.attributeKey.key === "userId"
|
||||
)?.value;
|
||||
|
||||
recordToProcess = {
|
||||
...rest,
|
||||
...(existingContactUserId && {
|
||||
userId: existingContactUserId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const recordToProcess = resolveUserIdConflict(mappedRecord, existingContact, existingUserIds);
|
||||
|
||||
// Overwrite by deleting existing attributes and creating new ones
|
||||
await prisma.contactAttribute.deleteMany({
|
||||
where: { contactId: existingContact.id },
|
||||
});
|
||||
|
||||
const newAttributes = Object.entries(recordToProcess).map(([key, value]) => ({
|
||||
attributeKey: {
|
||||
connect: { key_environmentId: { key, environmentId } },
|
||||
},
|
||||
value,
|
||||
}));
|
||||
|
||||
const updatedContact = prisma.contact.update({
|
||||
return prisma.contact.update({
|
||||
where: { id: existingContact.id },
|
||||
data: {
|
||||
attributes: {
|
||||
create: newAttributes,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attributes: {
|
||||
select: {
|
||||
attributeKey: { select: { key: true } },
|
||||
value: true,
|
||||
},
|
||||
create: createAttributeConnections(recordToProcess, environmentId),
|
||||
},
|
||||
},
|
||||
include: contactAttributesInclude,
|
||||
});
|
||||
|
||||
return updatedContact;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new contact
|
||||
const newAttributes = Object.entries(record).map(([key, value]) => ({
|
||||
attributeKey: {
|
||||
connect: { key_environmentId: { key, environmentId } },
|
||||
},
|
||||
value,
|
||||
}));
|
||||
|
||||
const newContact = prisma.contact.create({
|
||||
// Create new contact - use mappedRecord with proper DB key casing
|
||||
return prisma.contact.create({
|
||||
data: {
|
||||
environmentId,
|
||||
attributes: {
|
||||
create: newAttributes,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attributes: {
|
||||
select: {
|
||||
attributeKey: { select: { key: true } },
|
||||
value: true,
|
||||
},
|
||||
create: createAttributeConnections(mappedRecord, environmentId),
|
||||
},
|
||||
},
|
||||
include: contactAttributesInclude,
|
||||
});
|
||||
|
||||
return newContact;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createCacheKey } from "@formbricks/cache";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { E2E_TESTING } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { hashString } from "@/lib/hash-string";
|
||||
import { getInstanceId } from "@/lib/instance";
|
||||
@@ -262,7 +263,9 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
||||
const startOfNextYear = new Date(now.getFullYear() + 1, 0, 1);
|
||||
|
||||
const [instanceId, responseCount] = await Promise.all([
|
||||
getInstanceId(),
|
||||
// Skip instance ID during E2E tests to avoid license key conflicts
|
||||
// as the instance ID changes with each test run
|
||||
E2E_TESTING ? null : getInstanceId(),
|
||||
prisma.response.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
@@ -274,7 +277,8 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
||||
]);
|
||||
|
||||
// No organization exists, cannot perform license check
|
||||
if (!instanceId) return null;
|
||||
// (skip this check during E2E tests as we intentionally use null)
|
||||
if (!E2E_TESTING && !instanceId) return null;
|
||||
|
||||
const proxyUrl = env.HTTPS_PROXY ?? env.HTTP_PROXY;
|
||||
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
|
||||
@@ -282,12 +286,17 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), CONFIG.API.TIMEOUT_MS);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
licenseKey: env.ENTERPRISE_LICENSE_KEY,
|
||||
usage: { responseCount },
|
||||
};
|
||||
|
||||
if (instanceId) {
|
||||
payload.instanceId = instanceId;
|
||||
}
|
||||
|
||||
const res = await fetch(CONFIG.API.ENDPOINT, {
|
||||
body: JSON.stringify({
|
||||
licenseKey: env.ENTERPRISE_LICENSE_KEY,
|
||||
usage: { responseCount },
|
||||
instanceId,
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
agent,
|
||||
|
||||
@@ -60,7 +60,7 @@ export function AddMemberRole({
|
||||
name="role"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label>{t("common.role_organization")}</Label>
|
||||
<Label>{t("environments.settings.teams.organization_role")}</Label>
|
||||
<Select
|
||||
defaultValue={isAccessControlAllowed ? "member" : "owner"}
|
||||
disabled={!isAccessControlAllowed}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, UnknownError } from "@formbricks/types/errors";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { getProjectPermissionByUserId, getTeamRoleByTeamIdUserId } from "./roles";
|
||||
import { getProjectPermissionByUserId, getTeamRoleByTeamIdUserId, getTeamsWhereUserIsAdmin } from "./roles";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
projectTeam: { findMany: vi.fn() },
|
||||
teamUser: { findUnique: vi.fn() },
|
||||
teamUser: { findUnique: vi.fn(), findMany: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -19,6 +19,7 @@ vi.mock("@/lib/utils/validate", () => ({ validateInputs: vi.fn() }));
|
||||
const mockUserId = "user-1";
|
||||
const mockProjectId = "project-1";
|
||||
const mockTeamId = "team-1";
|
||||
const mockOrganizationId = "org-1";
|
||||
|
||||
describe("roles lib", () => {
|
||||
beforeEach(() => {
|
||||
@@ -90,7 +91,7 @@ describe("roles lib", () => {
|
||||
});
|
||||
|
||||
test("returns role if teamUser exists", async () => {
|
||||
vi.mocked(prisma.teamUser.findUnique).mockResolvedValueOnce({ role: "member" });
|
||||
vi.mocked(prisma.teamUser.findUnique).mockResolvedValueOnce({ role: "member" } as unknown as any);
|
||||
const result = await getTeamRoleByTeamIdUserId(mockTeamId, mockUserId);
|
||||
expect(result).toBe("member");
|
||||
});
|
||||
@@ -110,4 +111,47 @@ describe("roles lib", () => {
|
||||
await expect(getTeamRoleByTeamIdUserId(mockTeamId, mockUserId)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTeamsWhereUserIsAdmin", () => {
|
||||
test("returns empty array if user is not admin of any team", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockResolvedValueOnce([]);
|
||||
const result = await getTeamsWhereUserIsAdmin(mockUserId, mockOrganizationId);
|
||||
expect(result).toEqual([]);
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[mockUserId, expect.anything()],
|
||||
[mockOrganizationId, expect.anything()]
|
||||
);
|
||||
});
|
||||
|
||||
test("returns array of team IDs where user is admin", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockResolvedValueOnce([
|
||||
{ teamId: "team-1" },
|
||||
{ teamId: "team-2" },
|
||||
{ teamId: "team-3" },
|
||||
] as unknown as any);
|
||||
const result = await getTeamsWhereUserIsAdmin(mockUserId, mockOrganizationId);
|
||||
expect(result).toEqual(["team-1", "team-2", "team-3"]);
|
||||
});
|
||||
|
||||
test("returns single team ID when user is admin of one team", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockResolvedValueOnce([{ teamId: "team-1" }] as unknown as any);
|
||||
const result = await getTeamsWhereUserIsAdmin(mockUserId, mockOrganizationId);
|
||||
expect(result).toEqual(["team-1"]);
|
||||
});
|
||||
|
||||
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
const error = new Prisma.PrismaClientKnownRequestError("fail", {
|
||||
code: "P2002",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.teamUser.findMany).mockRejectedValueOnce(error);
|
||||
await expect(getTeamsWhereUserIsAdmin(mockUserId, mockOrganizationId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("throws error on generic error", async () => {
|
||||
const error = new Error("fail");
|
||||
vi.mocked(prisma.teamUser.findMany).mockRejectedValueOnce(error);
|
||||
await expect(getTeamsWhereUserIsAdmin(mockUserId, mockOrganizationId)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,3 +83,31 @@ export const getTeamRoleByTeamIdUserId = reactCache(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getTeamsWhereUserIsAdmin = reactCache(
|
||||
async (userId: string, organizationId: string): Promise<string[]> => {
|
||||
validateInputs([userId, ZId], [organizationId, ZId]);
|
||||
try {
|
||||
const adminTeams = await prisma.teamUser.findMany({
|
||||
where: {
|
||||
userId,
|
||||
role: "admin",
|
||||
team: {
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return adminTeams.map((at) => at.teamId);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ export const AccessTable = ({ teams }: AccessTableProps) => {
|
||||
{team.memberCount} {team.memberCount === 1 ? t("common.member") : t("common.members")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IdBadge id={team.id} showCopyIconOnHover={true} />
|
||||
<IdBadge id={team.id} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="capitalize">{TeamPermissionMapping[team.permission]}</p>
|
||||
|
||||
@@ -9,10 +9,9 @@ import { TProjectTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||
interface AccessViewProps {
|
||||
teams: TProjectTeam[];
|
||||
environmentId: string;
|
||||
isOwnerOrManager: boolean;
|
||||
}
|
||||
|
||||
export const AccessView = ({ teams, environmentId, isOwnerOrManager }: AccessViewProps) => {
|
||||
export const AccessView = ({ teams, environmentId }: AccessViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
@@ -20,7 +19,7 @@ export const AccessView = ({ teams, environmentId, isOwnerOrManager }: AccessVie
|
||||
title={t("common.team_access")}
|
||||
description={t("environments.project.teams.team_settings_description")}>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<ManageTeam environmentId={environmentId} isOwnerOrManager={isOwnerOrManager} />
|
||||
<ManageTeam environmentId={environmentId} />
|
||||
</div>
|
||||
<AccessTable teams={teams} />
|
||||
</SettingsCard>
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface ManageTeamProps {
|
||||
environmentId: string;
|
||||
isOwnerOrManager: boolean;
|
||||
}
|
||||
|
||||
export const ManageTeam = ({ environmentId, isOwnerOrManager }: ManageTeamProps) => {
|
||||
export const ManageTeam = ({ environmentId }: ManageTeamProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -19,20 +17,9 @@ export const ManageTeam = ({ environmentId, isOwnerOrManager }: ManageTeamProps)
|
||||
router.push(`/environments/${environmentId}/settings/teams`);
|
||||
};
|
||||
|
||||
if (isOwnerOrManager) {
|
||||
return (
|
||||
<Button variant="secondary" size="sm" onClick={handleManageTeams}>
|
||||
{t("environments.project.teams.manage_teams")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipRenderer
|
||||
tooltipContent={t("environments.project.teams.only_organization_owners_and_managers_can_manage_teams")}>
|
||||
<Button variant="secondary" size="sm" disabled>
|
||||
{t("environments.project.teams.manage_teams")}
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
<Button variant="secondary" size="sm" onClick={handleManageTeams}>
|
||||
{t("environments.project.teams.manage_teams")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
|
||||
const t = await getTranslate();
|
||||
const params = await props.params;
|
||||
|
||||
const { project, isOwner, isManager } = await getEnvironmentAuth(params.environmentId);
|
||||
const { project } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const teams = await getTeamsByProjectId(project.id);
|
||||
|
||||
@@ -18,14 +18,12 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
|
||||
throw new Error(t("common.teams_not_found"));
|
||||
}
|
||||
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.project_configuration")}>
|
||||
<ProjectConfigNavigation environmentId={params.environmentId} activeId="teams" />
|
||||
</PageHeader>
|
||||
<AccessView environmentId={params.environmentId} teams={teams} isOwnerOrManager={isOwnerOrManager} />
|
||||
<AccessView environmentId={params.environmentId} teams={teams} />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import { PlusIcon, Trash2Icon, XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
||||
@@ -80,6 +80,16 @@ export const TeamSettingsModal = ({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Track initial member IDs to distinguish existing members from newly added ones
|
||||
const initialMemberIds = useMemo(() => {
|
||||
return new Set(team.members.map((member) => member.userId));
|
||||
}, [team.members]);
|
||||
|
||||
// Track initial project IDs to distinguish existing projects from newly added ones
|
||||
const initialProjectIds = useMemo(() => {
|
||||
return new Set(team.projects.map((project) => project.projectId));
|
||||
}, [team.projects]);
|
||||
|
||||
const initialMembers = useMemo(() => {
|
||||
const members = team.members.map((member) => ({
|
||||
userId: member.userId,
|
||||
@@ -259,34 +269,44 @@ export const TeamSettingsModal = ({
|
||||
<FormField
|
||||
control={control}
|
||||
name={`members.${index}.userId`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
handleMemberSelectionChange(index, val);
|
||||
}}
|
||||
disabled={!isOwnerOrManager && !isTeamAdminMember}
|
||||
value={member.userId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select member" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memberOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`member-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
// Disable user select for existing members (can only remove or change role)
|
||||
const isExistingMember =
|
||||
member.userId && initialMemberIds.has(member.userId);
|
||||
const isSelectDisabled =
|
||||
isExistingMember || (!isOwnerOrManager && !isTeamAdminMember);
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
handleMemberSelectionChange(index, val);
|
||||
}}
|
||||
disabled={isSelectDisabled}
|
||||
value={member.userId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("environments.settings.teams.select_member")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memberOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`member-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
@@ -328,18 +348,20 @@ export const TeamSettingsModal = ({
|
||||
|
||||
{/* Delete Button for Member */}
|
||||
{watchMembers.length > 1 && (
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="shrink-0"
|
||||
disabled={
|
||||
!isOwnerOrManager &&
|
||||
(!isTeamAdminMember || member.userId === currentUserId)
|
||||
}
|
||||
onClick={() => handleRemoveMember(index)}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
<TooltipRenderer tooltipContent={t("common.remove_from_team")}>
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="shrink-0"
|
||||
disabled={
|
||||
!isOwnerOrManager &&
|
||||
(!isTeamAdminMember || member.userId === currentUserId)
|
||||
}
|
||||
onClick={() => handleRemoveMember(index)}>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -360,7 +382,7 @@ export const TeamSettingsModal = ({
|
||||
: t("environments.settings.teams.all_members_added")
|
||||
}>
|
||||
<Button
|
||||
size="default"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleAddMember}
|
||||
@@ -396,31 +418,40 @@ export const TeamSettingsModal = ({
|
||||
<FormField
|
||||
control={control}
|
||||
name={`projects.${index}.projectId`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={project.projectId}
|
||||
disabled={!isOwnerOrManager}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`project-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
// Disable project select for existing projects (can only remove or change permission)
|
||||
const isExistingProject =
|
||||
project.projectId && initialProjectIds.has(project.projectId);
|
||||
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={project.projectId}
|
||||
disabled={isSelectDisabled}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("environments.settings.teams.select_project")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`project-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
@@ -481,7 +512,7 @@ export const TeamSettingsModal = ({
|
||||
: t("environments.settings.teams.all_projects_added")
|
||||
}>
|
||||
<Button
|
||||
size="default"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleAddProject}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrganizationRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { createInviteToken } from "@/lib/jwt";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
@@ -16,6 +16,7 @@ import { getOrganizationIdFromInviteId } from "@/lib/utils/helper";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { checkRoleManagementPermission } from "@/modules/ee/role-management/actions";
|
||||
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
|
||||
import { sendInviteMemberEmail } from "@/modules/email";
|
||||
import {
|
||||
deleteMembership,
|
||||
@@ -195,19 +196,55 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
|
||||
)
|
||||
);
|
||||
|
||||
const validateTeamAdminInvitePermissions = (
|
||||
inviterRole: TOrganizationRole,
|
||||
inviterAdminTeams: string[],
|
||||
inviteRole: TOrganizationRole,
|
||||
inviteTeamIds: string[]
|
||||
): void => {
|
||||
const isOrgOwnerOrManager = inviterRole === "owner" || inviterRole === "manager";
|
||||
const isTeamAdmin = inviterAdminTeams.length > 0;
|
||||
|
||||
if (!isOrgOwnerOrManager && !isTeamAdmin) {
|
||||
throw new AuthenticationError("Only organization owners, managers, or team admins can invite members");
|
||||
}
|
||||
|
||||
// Team admins have restrictions
|
||||
if (isTeamAdmin && !isOrgOwnerOrManager) {
|
||||
if (inviteRole !== "member") {
|
||||
throw new OperationNotAllowedError("Team admins can only invite users as members");
|
||||
}
|
||||
|
||||
const invalidTeams = inviteTeamIds.filter((id) => !inviterAdminTeams.includes(id));
|
||||
if (invalidTeams.length > 0) {
|
||||
throw new OperationNotAllowedError("Team admins can only add users to teams where they are admin");
|
||||
}
|
||||
|
||||
if (inviteTeamIds.length === 0) {
|
||||
throw new ValidationError("Team admins must add invited users to at least one team");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ZInviteUserAction = z.object({
|
||||
organizationId: ZId,
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
role: ZOrganizationRole,
|
||||
teamIds: z.array(z.string()),
|
||||
teamIds: z.array(ZId),
|
||||
});
|
||||
|
||||
export const inviteUserAction = authenticatedActionClient.schema(ZInviteUserAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"invite",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZInviteUserAction>;
|
||||
}) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
@@ -224,16 +261,41 @@ export const inviteUserAction = authenticatedActionClient.schema(ZInviteUserActi
|
||||
throw new AuthenticationError("User not a member of this organization");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const isOrgOwnerOrManager =
|
||||
currentUserMembership.role === "owner" || currentUserMembership.role === "manager";
|
||||
|
||||
// Fetch user's admin teams (empty array if owner/manager to skip unnecessary query)
|
||||
const userAdminTeams = isOrgOwnerOrManager
|
||||
? []
|
||||
: await getTeamsWhereUserIsAdmin(ctx.user.id, parsedInput.organizationId);
|
||||
|
||||
const isTeamAdmin = userAdminTeams.length > 0;
|
||||
|
||||
if (!isOrgOwnerOrManager && !isTeamAdmin) {
|
||||
throw new AuthenticationError("Not authorized to invite members");
|
||||
}
|
||||
|
||||
if (isOrgOwnerOrManager) {
|
||||
// Standard org-level auth check
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Validate team admin restrictions
|
||||
validateTeamAdminInvitePermissions(
|
||||
currentUserMembership.role,
|
||||
userAdminTeams,
|
||||
parsedInput.role,
|
||||
parsedInput.teamIds
|
||||
);
|
||||
|
||||
if (currentUserMembership.role === "manager" && parsedInput.role !== "member") {
|
||||
throw new OperationNotAllowedError("Managers can only invite users as members");
|
||||
|
||||
@@ -37,6 +37,8 @@ interface OrganizationActionsProps {
|
||||
isMultiOrgEnabled: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
userAdminTeamIds?: string[];
|
||||
}
|
||||
|
||||
export const OrganizationActions = ({
|
||||
@@ -52,16 +54,20 @@ export const OrganizationActions = ({
|
||||
isMultiOrgEnabled,
|
||||
isUserManagementDisabledFromUi,
|
||||
isStorageConfigured,
|
||||
isTeamAdmin,
|
||||
userAdminTeamIds,
|
||||
}: OrganizationActionsProps) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [isLeaveOrganizationModalOpen, setLeaveOrganizationModalOpen] = useState(false);
|
||||
const [isInviteMemberModalOpen, setInviteMemberModalOpen] = useState(false);
|
||||
const [isLeaveOrganizationModalOpen, setIsLeaveOrganizationModalOpen] = useState(false);
|
||||
const [isInviteMemberModalOpen, setIsInviteMemberModalOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { isOwner, isManager } = getAccessFlags(membershipRole);
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
const canInvite = isOwnerOrManager || (isAccessControlAllowed && isTeamAdmin);
|
||||
|
||||
const handleLeaveOrganization = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -134,18 +140,18 @@ export const OrganizationActions = ({
|
||||
<>
|
||||
<div className="mb-4 flex justify-end space-x-2 text-right">
|
||||
{role !== "owner" && isMultiOrgEnabled && (
|
||||
<Button variant="secondary" size="sm" onClick={() => setLeaveOrganizationModalOpen(true)}>
|
||||
<Button variant="destructive" size="sm" onClick={() => setIsLeaveOrganizationModalOpen(true)}>
|
||||
{t("environments.settings.general.leave_organization")}
|
||||
<XIcon />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isInviteDisabled && isOwnerOrManager && !isUserManagementDisabledFromUi && (
|
||||
{!isInviteDisabled && canInvite && !isUserManagementDisabledFromUi && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setInviteMemberModalOpen(true);
|
||||
setIsInviteMemberModalOpen(true);
|
||||
}}>
|
||||
{t("environments.settings.teams.invite_member")}
|
||||
</Button>
|
||||
@@ -153,7 +159,7 @@ export const OrganizationActions = ({
|
||||
</div>
|
||||
<InviteMemberModal
|
||||
open={isInviteMemberModalOpen}
|
||||
setOpen={setInviteMemberModalOpen}
|
||||
setOpen={setIsInviteMemberModalOpen}
|
||||
onSubmit={handleAddMembers}
|
||||
membershipRole={membershipRole}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
@@ -161,9 +167,12 @@ export const OrganizationActions = ({
|
||||
environmentId={environmentId}
|
||||
teams={teams}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isTeamAdmin={isTeamAdmin}
|
||||
userAdminTeamIds={userAdminTeamIds}
|
||||
/>
|
||||
|
||||
<Dialog open={isLeaveOrganizationModalOpen} onOpenChange={setLeaveOrganizationModalOpen}>
|
||||
<Dialog open={isLeaveOrganizationModalOpen} onOpenChange={setIsLeaveOrganizationModalOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("environments.settings.general.leave_organization_title")}</DialogTitle>
|
||||
@@ -177,7 +186,7 @@ export const OrganizationActions = ({
|
||||
</p>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setLeaveOrganizationModalOpen(false)}>
|
||||
<Button variant="secondary" onClick={() => setIsLeaveOrganizationModalOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -7,13 +7,14 @@ import { useRouter } from "next/navigation";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { ZUserName } from "@formbricks/types/user";
|
||||
import { AddMemberRole } from "@/modules/ee/role-management/components/add-member-role";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
import { FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||
@@ -27,6 +28,7 @@ interface IndividualInviteTabProps {
|
||||
isFormbricksCloud: boolean;
|
||||
environmentId: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
showTeamAdminRestrictions: boolean;
|
||||
}
|
||||
|
||||
export const IndividualInviteTab = ({
|
||||
@@ -37,22 +39,32 @@ export const IndividualInviteTab = ({
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
membershipRole,
|
||||
showTeamAdminRestrictions,
|
||||
}: IndividualInviteTabProps) => {
|
||||
const ZFormSchema = z.object({
|
||||
name: ZUserName,
|
||||
email: z.string().min(1, { message: "Email is required" }).email({ message: "Invalid email" }),
|
||||
role: ZOrganizationRole,
|
||||
teamIds: z.array(z.string()),
|
||||
teamIds: showTeamAdminRestrictions
|
||||
? z.array(ZId).min(1, { message: "Team admins must select at least one team" })
|
||||
: z.array(ZId),
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
type TFormData = z.infer<typeof ZFormSchema>;
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Determine default role based on permissions
|
||||
let defaultRole: TOrganizationRole = "owner";
|
||||
if (showTeamAdminRestrictions || isAccessControlAllowed) {
|
||||
defaultRole = "member";
|
||||
}
|
||||
|
||||
const form = useForm<TFormData>({
|
||||
resolver: zodResolver(ZFormSchema),
|
||||
defaultValues: {
|
||||
role: isAccessControlAllowed ? "member" : "owner",
|
||||
role: defaultRole,
|
||||
teamIds: [],
|
||||
},
|
||||
});
|
||||
@@ -104,43 +116,61 @@ export const IndividualInviteTab = ({
|
||||
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<AddMemberRole
|
||||
control={control}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
{watch("role") === "member" && (
|
||||
<Alert className="mt-2" variant="info">
|
||||
<AlertDescription>{t("environments.settings.teams.member_role_info_message")}</AlertDescription>
|
||||
</Alert>
|
||||
{showTeamAdminRestrictions ? (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label htmlFor="memberRoleSelect">{t("environments.settings.teams.organization_role")}</Label>
|
||||
<Input value={t("environments.settings.teams.member")} disabled />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<AddMemberRole
|
||||
control={control}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
{watch("role") === "member" && (
|
||||
<Alert className="mt-2" variant="info">
|
||||
<AlertDescription>
|
||||
{t("environments.settings.teams.member_role_info_message")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAccessControlAllowed && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="teamIds"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col space-y-2">
|
||||
<FormLabel>{t("common.add_to_team")} </FormLabel>
|
||||
<div className="space-y-2">
|
||||
<MultiSelect
|
||||
value={field.value}
|
||||
options={teamOptions}
|
||||
placeholder={t("environments.settings.teams.team_select_placeholder")}
|
||||
disabled={!teamOptions.length}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
/>
|
||||
{!teamOptions.length && (
|
||||
<Small className="font-normal text-amber-600">
|
||||
{t("environments.settings.teams.create_first_team_message")}
|
||||
</Small>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<FormField
|
||||
control={control}
|
||||
name="teamIds"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col space-y-2">
|
||||
<FormLabel>{t("common.add_to_team")} </FormLabel>
|
||||
<div className="space-y-2">
|
||||
<MultiSelect
|
||||
value={field.value}
|
||||
options={teamOptions}
|
||||
placeholder={t("environments.settings.teams.team_select_placeholder")}
|
||||
disabled={!teamOptions.length}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
/>
|
||||
{!teamOptions.length && (
|
||||
<Small className="font-normal text-amber-600">
|
||||
{t("environments.settings.teams.create_first_team_message")}
|
||||
</Small>
|
||||
)}
|
||||
</div>
|
||||
<FormError>{errors.teamIds?.message}</FormError>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label htmlFor="teamRoleInput">{t("common.team_role")}</Label>
|
||||
<Input value={t("environments.settings.teams.contributor")} disabled />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isAccessControlAllowed && (
|
||||
|
||||
@@ -26,6 +26,9 @@ interface InviteMemberModalProps {
|
||||
environmentId: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
isStorageConfigured: boolean;
|
||||
isOwnerOrManager: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
userAdminTeamIds?: string[];
|
||||
}
|
||||
|
||||
export const InviteMemberModal = ({
|
||||
@@ -38,11 +41,21 @@ export const InviteMemberModal = ({
|
||||
environmentId,
|
||||
membershipRole,
|
||||
isStorageConfigured,
|
||||
isOwnerOrManager,
|
||||
isTeamAdmin,
|
||||
userAdminTeamIds,
|
||||
}: InviteMemberModalProps) => {
|
||||
const [type, setType] = useState<"individual" | "bulk">("individual");
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showTeamAdminRestrictions = !isOwnerOrManager && isTeamAdmin;
|
||||
|
||||
const filteredTeams =
|
||||
showTeamAdminRestrictions && userAdminTeamIds
|
||||
? teams.filter((t) => userAdminTeamIds.includes(t.id))
|
||||
: teams;
|
||||
|
||||
const tabs = {
|
||||
individual: (
|
||||
<IndividualInviteTab
|
||||
@@ -51,8 +64,9 @@ export const InviteMemberModal = ({
|
||||
onSubmit={onSubmit}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
teams={teams}
|
||||
teams={filteredTeams}
|
||||
membershipRole={membershipRole}
|
||||
showTeamAdminRestrictions={showTeamAdminRestrictions}
|
||||
/>
|
||||
),
|
||||
bulk: (
|
||||
@@ -75,16 +89,18 @@ export const InviteMemberModal = ({
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody className="flex flex-col gap-6" unconstrained>
|
||||
<TabToggle
|
||||
id="type"
|
||||
options={[
|
||||
{ value: "individual", label: t("environments.settings.teams.individual") },
|
||||
{ value: "bulk", label: t("environments.settings.teams.bulk_invite") },
|
||||
]}
|
||||
onChange={(inviteType) => setType(inviteType)}
|
||||
defaultSelected={type}
|
||||
/>
|
||||
{tabs[type]}
|
||||
{!showTeamAdminRestrictions && (
|
||||
<TabToggle
|
||||
id="type"
|
||||
options={[
|
||||
{ value: "individual", label: t("environments.settings.teams.individual") },
|
||||
{ value: "bulk", label: t("environments.settings.teams.bulk_invite") },
|
||||
]}
|
||||
onChange={(inviteType) => setType(inviteType)}
|
||||
defaultSelected={type}
|
||||
/>
|
||||
)}
|
||||
{showTeamAdminRestrictions ? tabs.individual : tabs[type]}
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/
|
||||
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { EditMemberships } from "@/modules/organization/settings/teams/components/edit-memberships";
|
||||
@@ -45,6 +46,10 @@ export const MembersView = async ({
|
||||
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
// Fetch admin teams if they're a team admin
|
||||
const userAdminTeamIds = await getTeamsWhereUserIsAdmin(currentUserId, organization.id);
|
||||
const isTeamAdminUser = userAdminTeamIds.length > 0;
|
||||
|
||||
let teams: TOrganizationTeam[] = [];
|
||||
|
||||
if (isAccessControlAllowed) {
|
||||
@@ -69,6 +74,8 @@ export const MembersView = async ({
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
teams={teams}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
isTeamAdmin={isTeamAdminUser}
|
||||
userAdminTeamIds={userAdminTeamIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constan
|
||||
import { getUserManagementAccess } from "@/lib/membership/utils";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
|
||||
import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { MembersView } from "@/modules/organization/settings/teams/components/members-view";
|
||||
@@ -16,11 +17,21 @@ export const TeamsPage = async (props) => {
|
||||
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
const hasUserManagementAccess = getUserManagementAccess(
|
||||
|
||||
// Check if user has standard user management access (owner/manager)
|
||||
const hasStandardUserManagementAccess = getUserManagementAccess(
|
||||
currentUserMembership?.role,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE
|
||||
);
|
||||
|
||||
// Also check if user is a team admin (they get limited user management for invites)
|
||||
const userAdminTeamIds = await getTeamsWhereUserIsAdmin(session.user.id, organization.id);
|
||||
const isTeamAdminUser = userAdminTeamIds.length > 0;
|
||||
|
||||
// Allow user management UI if they're owner/manager OR team admin (when access control is enabled)
|
||||
const hasUserManagementAccess =
|
||||
hasStandardUserManagementAccess || (isAccessControlAllowed && isTeamAdminUser);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { type JSX, useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { findElementLocation } from "@/modules/survey/editor/lib/blocks";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
|
||||
interface BulkEditOptionsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
regularChoices: TSurveyMultipleChoiceElement["choices"];
|
||||
onSave: (updatedChoices: TSurveyMultipleChoiceElement["choices"]) => void;
|
||||
element: TSurveyMultipleChoiceElement;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
surveyLanguageCodes: string[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
const parseUniqueLines = (content: string): string[] => {
|
||||
return [
|
||||
...new Set(
|
||||
content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const updateChoiceLabel = (
|
||||
choice: TSurveyMultipleChoiceElement["choices"][number],
|
||||
newLabel: string,
|
||||
selectedLangCode: string,
|
||||
allLangCodes: string[]
|
||||
): TSurveyMultipleChoiceElement["choices"][number] => {
|
||||
const label = Object.fromEntries([
|
||||
...allLangCodes.map((code) => [code, choice.label[code] ?? ""]),
|
||||
[selectedLangCode, newLabel],
|
||||
]) as TI18nString;
|
||||
|
||||
return { ...choice, label };
|
||||
};
|
||||
|
||||
export const BulkEditOptionsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
regularChoices,
|
||||
onSave,
|
||||
element,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
surveyLanguageCodes,
|
||||
locale,
|
||||
}: BulkEditOptionsModalProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [textareaValue, setTextareaValue] = useState("");
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
const selectedLanguageName = useMemo(() => {
|
||||
if (localSurvey.languages.length <= 1) return null;
|
||||
const code =
|
||||
selectedLanguageCode === "default"
|
||||
? localSurvey.languages.find((lang) => lang.default)?.language.code
|
||||
: selectedLanguageCode;
|
||||
return code ? getLanguageLabel(code, locale) : null;
|
||||
}, [localSurvey.languages, selectedLanguageCode, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTextareaValue(regularChoices.map((c) => c.label[selectedLanguageCode] || "").join("\n"));
|
||||
setValidationError(null);
|
||||
}
|
||||
}, [isOpen, regularChoices, selectedLanguageCode]);
|
||||
|
||||
const validateRemovedOptions = (newLabels: string[]): string | null => {
|
||||
const originalLabels = regularChoices.map((c) => c.label[selectedLanguageCode] || "");
|
||||
const missingLabels = originalLabels.filter((label) => label && !newLabels.includes(label));
|
||||
|
||||
if (missingLabels.length === 0) return null;
|
||||
|
||||
// Find which choices have missing labels and check if they're used in logic
|
||||
const choicesWithMissingLabels = missingLabels
|
||||
.map((label) => regularChoices.find((c) => c.label[selectedLanguageCode] === label))
|
||||
.filter((c): c is TSurveyMultipleChoiceElement["choices"][number] => c !== undefined);
|
||||
|
||||
// Get all elements to find which block has the logic
|
||||
const allElements = getElementsFromBlocks(localSurvey.blocks);
|
||||
|
||||
// Build detailed error info: option label -> block name where it's used
|
||||
const problematicOptions: { optionLabel: string; blockName: string }[] = [];
|
||||
|
||||
for (const choice of choicesWithMissingLabels) {
|
||||
const elementIndex = findOptionUsedInLogic(localSurvey, element.id, choice.id);
|
||||
if (elementIndex !== -1) {
|
||||
const elementWithLogic = allElements[elementIndex];
|
||||
// Find which block contains this element
|
||||
const { block } = findElementLocation(localSurvey, elementWithLogic.id);
|
||||
if (block) {
|
||||
const optionLabel = choice.label[selectedLanguageCode] || "";
|
||||
problematicOptions.push({ optionLabel, blockName: block.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (problematicOptions.length === 0) return null;
|
||||
|
||||
// Format: "Option '3' is used in logic at 'Block Name'"
|
||||
const details = problematicOptions.map((opt) => `"${opt.optionLabel}" → ${opt.blockName}`).join(", ");
|
||||
|
||||
return t("environments.surveys.edit.options_used_in_logic_bulk_error", {
|
||||
questionIndexes: details,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const newLabels = parseUniqueLines(textareaValue);
|
||||
const error = validateRemovedOptions(newLabels);
|
||||
|
||||
if (error) {
|
||||
setValidationError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedChoices = newLabels.map((label, idx) =>
|
||||
idx < regularChoices.length
|
||||
? updateChoiceLabel(regularChoices[idx], label, selectedLanguageCode, surveyLanguageCodes)
|
||||
: { id: createId(), label: createI18nString(label, surveyLanguageCodes) }
|
||||
);
|
||||
|
||||
onSave(updatedChoices);
|
||||
onClose();
|
||||
toast.success(t("environments.surveys.edit.changes_saved"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl space-y-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedLanguageName
|
||||
? t("environments.surveys.edit.bulk_edit_options_for", { language: selectedLanguageName })
|
||||
: t("environments.surveys.edit.bulk_edit_options")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("environments.surveys.edit.bulk_edit_description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<textarea
|
||||
value={textareaValue}
|
||||
onChange={(e) => {
|
||||
setTextareaValue(e.target.value);
|
||||
setValidationError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.shiftKey && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
}}
|
||||
rows={15}
|
||||
className="focus:border-brand w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-sm focus:outline-none"
|
||||
placeholder={t("environments.surveys.edit.bulk_edit_description")}
|
||||
/>
|
||||
{validationError && <div className="text-sm text-red-600">{validationError}</div>}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>{t("environments.surveys.edit.update_options")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -8,12 +8,14 @@ import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
||||
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { BulkEditOptionsModal } from "@/modules/survey/editor/components/bulk-edit-options-modal";
|
||||
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -49,6 +51,7 @@ export const MultipleChoiceElementForm = ({
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
|
||||
const [isBulkEditOpen, setIsBulkEditOpen] = useState(false);
|
||||
|
||||
const elementRef = useRef<HTMLInputElement>(null);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -90,11 +93,31 @@ export const MultipleChoiceElementForm = ({
|
||||
[element.choices]
|
||||
);
|
||||
|
||||
// Get the display name for the selected language (for multi-language surveys)
|
||||
const bulkEditButtonLabel = useMemo(() => {
|
||||
if (localSurvey.languages.length <= 1) {
|
||||
return t("environments.surveys.edit.bulk_edit");
|
||||
}
|
||||
|
||||
const languageCode =
|
||||
selectedLanguageCode === "default"
|
||||
? localSurvey.languages.find((lang) => lang.default)?.language.code
|
||||
: selectedLanguageCode;
|
||||
|
||||
const languageName = languageCode ? getLanguageLabel(languageCode, locale) : "";
|
||||
return `${t("environments.surveys.edit.bulk_edit")} (${languageName})`;
|
||||
}, [localSurvey.languages, selectedLanguageCode, locale, t]);
|
||||
|
||||
const ensureSpecialChoicesOrder = (choices: TSurveyMultipleChoiceElement["choices"]) => {
|
||||
const regularChoicesFromInput = choices.filter((c) => c.id !== "other" && c.id !== "none");
|
||||
const otherChoice = choices.find((c) => c.id === "other");
|
||||
const noneChoice = choices.find((c) => c.id === "none");
|
||||
// [regularChoices, otherChoice, noneChoice]
|
||||
return [...regularChoices, ...(otherChoice ? [otherChoice] : []), ...(noneChoice ? [noneChoice] : [])];
|
||||
return [
|
||||
...regularChoicesFromInput,
|
||||
...(otherChoice ? [otherChoice] : []),
|
||||
...(noneChoice ? [noneChoice] : []),
|
||||
];
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx?: number) => {
|
||||
@@ -283,7 +306,7 @@ export const MultipleChoiceElementForm = ({
|
||||
updateElement(elementIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={element.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
<div className="flex max-h-[25dvh] flex-col gap-2 overflow-y-auto py-1 pr-1" ref={parent}>
|
||||
{element.choices?.map((choice, choiceIdx) => (
|
||||
<ElementOptionChoice
|
||||
key={choice.id}
|
||||
@@ -308,6 +331,9 @@ export const MultipleChoiceElementForm = ({
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 flex items-center justify-between space-x-2">
|
||||
<div className="flex gap-2">
|
||||
{specialChoices.map((specialChoice) => {
|
||||
@@ -323,6 +349,9 @@ export const MultipleChoiceElementForm = ({
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<Button size="sm" variant="secondary" type="button" onClick={() => setIsBulkEditOpen(true)}>
|
||||
{bulkEditButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -352,6 +381,23 @@ export const MultipleChoiceElementForm = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BulkEditOptionsModal
|
||||
isOpen={isBulkEditOpen}
|
||||
onClose={() => setIsBulkEditOpen(false)}
|
||||
regularChoices={regularChoices}
|
||||
onSave={(updatedChoices) => {
|
||||
const newChoices = ensureSpecialChoicesOrder([
|
||||
...updatedChoices,
|
||||
...element.choices.filter((c) => c.id === "other" || c.id === "none"),
|
||||
]);
|
||||
updateElement(elementIdx, { choices: newChoices });
|
||||
}}
|
||||
element={element}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,9 @@ export const LinkSurveyWrapper = ({
|
||||
styling={styling}
|
||||
onBackgroundLoaded={handleBackgroundLoaded}>
|
||||
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
|
||||
{!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && <ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} />}
|
||||
{!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && (
|
||||
<ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} />
|
||||
)}
|
||||
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
||||
{isPreview && (
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
|
||||
@@ -15,7 +15,12 @@ interface ClientLogoProps {
|
||||
previewSurvey?: boolean;
|
||||
}
|
||||
|
||||
export const ClientLogo = ({ environmentId, projectLogo, surveyLogo, previewSurvey = false }: ClientLogoProps) => {
|
||||
export const ClientLogo = ({
|
||||
environmentId,
|
||||
projectLogo,
|
||||
surveyLogo,
|
||||
previewSurvey = false,
|
||||
}: ClientLogoProps) => {
|
||||
const { t } = useTranslation();
|
||||
const logoToUse = surveyLogo?.url ? surveyLogo : projectLogo;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between gap-2 rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-slate-400",
|
||||
"flex h-9 w-full items-center justify-between gap-2 rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-1 hover:enabled:border-slate-400 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-slate-400",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"lucide-react": "0.507.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "3.0.1",
|
||||
"next": "15.5.7",
|
||||
"next": "15.5.9",
|
||||
"next-auth": "4.24.12",
|
||||
"next-safe-action": "7.10.8",
|
||||
"node-fetch": "3.3.2",
|
||||
|
||||
@@ -20,7 +20,7 @@ test.describe("Invite, accept and remove organization member", async () => {
|
||||
|
||||
await page.locator('[data-testid="members-loading-card"]:first-child').waitFor({ state: "hidden" });
|
||||
|
||||
await page.getByRole("link", { name: "Access Control" }).click();
|
||||
await page.getByRole("link", { name: "Members & Teams" }).click();
|
||||
|
||||
// Add member button
|
||||
await expect(page.getByRole("button", { name: "Invite member" })).toBeVisible();
|
||||
@@ -131,8 +131,8 @@ test.describe("Create, update and delete team", async () => {
|
||||
await page.waitForURL(/\/environments\/[^/]+\/settings\/general/);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page.getByText("Access Control")).toBeVisible();
|
||||
await page.getByText("Access Control").click();
|
||||
await expect(page.getByText("Members & Teams")).toBeVisible();
|
||||
await page.getByText("Members & Teams").click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/settings\/teams/);
|
||||
await expect(page.getByRole("button", { name: "Create new team" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Create new team" }).click();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "3.3"
|
||||
x-environment: &environment
|
||||
environment:
|
||||
######################################################## REQUIRED ########################################################
|
||||
|
||||
@@ -496,7 +496,7 @@ EOF
|
||||
if [[ $insert_traefik == "y" ]]; then
|
||||
cat >> "$services_snippet_file" << EOF
|
||||
traefik:
|
||||
image: "traefik:v2.11.29"
|
||||
image: "traefik:v2.11.31"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
depends_on:
|
||||
@@ -525,7 +525,7 @@ EOF
|
||||
cat > "$services_snippet_file" << EOF
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.11.29"
|
||||
image: "traefik:v2.11.31"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
depends_on:
|
||||
|
||||
@@ -109,7 +109,7 @@ Modify the configuration to enforce SSL. The rest of the configuration should re
|
||||
<<: *environment
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.7"
|
||||
image: "traefik:v2.11.31"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
depends_on:
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
"react-dom": "19.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/microsoft-playwright-testing": "1.0.0-beta.7",
|
||||
"@azure/identity": "4.13.0",
|
||||
"@azure/playwright": "1.0.0",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@playwright/test": "1.56.1",
|
||||
"eslint": "8.57.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const JS_LOCAL_STORAGE_KEY = "formbricks-js";
|
||||
export const LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY = "formbricks-js-website";
|
||||
export const LEGACY_JS_APP_LOCAL_STORAGE_KEY = "formbricks-js-app";
|
||||
export const CONTAINER_ID = "formbricks-app-container";
|
||||
export const CONTAINER_ID = "formbricks-modal-container";
|
||||
export const RECAPTCHA_SCRIPT_ID = "formbricks-recaptcha-script";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getIsSetup, setIsSetup } from "@/lib/common/status";
|
||||
import { filterSurveys, getIsDebug, isNowExpired, wrapThrows } from "@/lib/common/utils";
|
||||
import { fetchEnvironmentState } from "@/lib/environment/state";
|
||||
import { checkPageUrl } from "@/lib/survey/no-code-action";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "@/lib/survey/widget";
|
||||
import { closeSurvey } from "@/lib/survey/widget";
|
||||
import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state";
|
||||
import { sendUpdatesToBackend } from "@/lib/user/update";
|
||||
import {
|
||||
@@ -142,9 +142,6 @@ export const setup = async (
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Adding widget container to DOM");
|
||||
addWidgetContainer();
|
||||
|
||||
if (
|
||||
existingConfig?.environment &&
|
||||
existingConfig.environmentId === configInput.environmentId &&
|
||||
@@ -344,10 +341,7 @@ export const tearDown = (): void => {
|
||||
filteredSurveys,
|
||||
});
|
||||
|
||||
// remove container element from DOM
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
setIsSurveyRunning(false);
|
||||
closeSurvey();
|
||||
};
|
||||
|
||||
export const handleErrorOnFirstSetup = (e: { code: string; responseMessage: string }): Promise<never> => {
|
||||
|
||||
@@ -290,12 +290,13 @@ describe("setup.ts", () => {
|
||||
test("resets user state to default", () => {
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue({
|
||||
environment: { data: { surveys: [] } },
|
||||
user: { data: { userId: "XYZ" } },
|
||||
}),
|
||||
update: vi.fn(),
|
||||
};
|
||||
|
||||
getInstanceConfigMock.mockReturnValueOnce(mockConfig as unknown as Config);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
tearDown();
|
||||
|
||||
|
||||
@@ -174,9 +174,8 @@ export const renderWidget = async (
|
||||
export const closeSurvey = (): void => {
|
||||
const config = Config.getInstance();
|
||||
|
||||
// remove container element from DOM
|
||||
// remove the survey modal container from DOM
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
|
||||
const { environment, user } = config.get();
|
||||
const filteredSurveys = filterSurveys(environment, user);
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { ServiceOS, getServiceConfig } from "@azure/microsoft-playwright-testing";
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import config from "./playwright.config";
|
||||
import { createAzurePlaywrightConfig, ServiceAuth, ServiceOS } from '@azure/playwright';
|
||||
import { defineConfig } from '@playwright/test';
|
||||
import config from './playwright.config';
|
||||
|
||||
/* Learn more about service configuration at https://aka.ms/mpt/config */
|
||||
/* Learn more about service configuration at https://aka.ms/pww/docs/config */
|
||||
export default defineConfig(
|
||||
config,
|
||||
getServiceConfig(config, {
|
||||
exposeNetwork: "<loopback>",
|
||||
timeout: 120000, // Increased timeout for cloud environment with network latency
|
||||
createAzurePlaywrightConfig(config, {
|
||||
exposeNetwork: '<loopback>',
|
||||
connectTimeout: 3 * 60 * 1000, // 3 minutes
|
||||
os: ServiceOS.LINUX,
|
||||
useCloudHostedBrowsers: true, // Set to false if you want to only use reporting and not cloud hosted browsers
|
||||
}),
|
||||
{
|
||||
/*
|
||||
Playwright Testing service reporter is added by default.
|
||||
This will override any reporter options specified in the base playwright config.
|
||||
If you are using more reporters, please update your configuration accordingly.
|
||||
*/
|
||||
reporter: [["list"], ["@azure/microsoft-playwright-testing/reporter"]],
|
||||
retries: 2, // Always retry in cloud environment due to potential network/timing issues
|
||||
maxFailures: undefined, // Don't stop on first failure to avoid cascading shutdowns with high parallelism
|
||||
}
|
||||
serviceAuthType: ServiceAuth.ACCESS_TOKEN
|
||||
})
|
||||
);
|
||||
|
||||
360
pnpm-lock.yaml
generated
360
pnpm-lock.yaml
generated
@@ -26,9 +26,12 @@ importers:
|
||||
specifier: 19.1.2
|
||||
version: 19.1.2(react@19.1.2)
|
||||
devDependencies:
|
||||
'@azure/microsoft-playwright-testing':
|
||||
specifier: 1.0.0-beta.7
|
||||
version: 1.0.0-beta.7(@playwright/test@1.56.1)
|
||||
'@azure/identity':
|
||||
specifier: 4.13.0
|
||||
version: 4.13.0
|
||||
'@azure/playwright':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0(@playwright/test@1.56.1)
|
||||
'@formbricks/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:packages/config-eslint
|
||||
@@ -269,7 +272,7 @@ importers:
|
||||
version: 0.0.38(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
'@sentry/nextjs':
|
||||
specifier: 10.5.0
|
||||
version: 10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)(webpack@5.99.8(esbuild@0.25.10))
|
||||
version: 10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)(webpack@5.99.8(esbuild@0.25.10))
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: 0.13.4
|
||||
version: 0.13.4(arktype@2.1.25)(typescript@5.8.3)(zod@3.24.4)
|
||||
@@ -358,14 +361,14 @@ importers:
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
next:
|
||||
specifier: 15.5.7
|
||||
version: 15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
specifier: 15.5.9
|
||||
version: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
next-auth:
|
||||
specifier: 4.24.12
|
||||
version: 4.24.12(patch_hash=bdy3m55bopfzpysceipfxj5eei)(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(nodemailer@7.0.11)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
version: 4.24.12(patch_hash=bdy3m55bopfzpysceipfxj5eei)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(nodemailer@7.0.11)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
next-safe-action:
|
||||
specifier: 7.10.8
|
||||
version: 7.10.8(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(zod@3.24.4)
|
||||
version: 7.10.8(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(zod@3.24.4)
|
||||
node-fetch:
|
||||
specifier: 3.3.2
|
||||
version: 3.3.2
|
||||
@@ -1347,10 +1350,6 @@ packages:
|
||||
resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@azure/core-xml@1.5.0':
|
||||
resolution: {integrity: sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@azure/identity@4.13.0':
|
||||
resolution: {integrity: sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -1367,13 +1366,6 @@ packages:
|
||||
resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@azure/microsoft-playwright-testing@1.0.0-beta.7':
|
||||
resolution: {integrity: sha512-Y6C35LWUfLevHu5NG+7vvFfhpmUrGWKRumcz7/CSCmWlx8RVfWgP6NuL8rIPDuTeJyjaTczNfeg1ppGW26TjBw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: This package has been deprecated and will no longer be maintained after March 8, 2026. Upgrade to the replacement package, @azure/playwright, to continue receiving updates.
|
||||
peerDependencies:
|
||||
'@playwright/test': ^1.43.1
|
||||
|
||||
'@azure/msal-browser@4.26.0':
|
||||
resolution: {integrity: sha512-Ie3SZ4IMrf9lSwWVzzJrhTPE+g9+QDUfeor1LKMBQzcblp+3J/U1G8hMpNSfLL7eA5F/DjjPXkATJ5JRUdDJLA==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -1386,13 +1378,11 @@ packages:
|
||||
resolution: {integrity: sha512-HszfqoC+i2C9+BRDQfuNUGp15Re7menIhCEbFCQ49D3KaqEDrgZIgQ8zSct4T59jWeUIL9N/Dwiv4o2VueTdqQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
'@azure/storage-blob@12.29.1':
|
||||
resolution: {integrity: sha512-7ktyY0rfTM0vo7HvtK6E3UvYnI9qfd6Oz6z/+92VhGRveWng3kJwMKeUpqmW/NmwcDNbxHpSlldG+vsUnRFnBg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@azure/storage-common@12.1.1':
|
||||
resolution: {integrity: sha512-eIOH1pqFwI6UmVNnDQvmFeSg0XppuzDLFeUNO/Xht7ODAzRLgGDh7h550pSxoA+lPDxBl1+D2m/KG3jWzCUjTg==}
|
||||
'@azure/playwright@1.0.0':
|
||||
resolution: {integrity: sha512-HG0XuYT0z7TcyffJZrEZ9fWrP/WsK/jDAmKFB7MuYNy9WMW0oQItUy+o6uIv2IiZzUeO8OpyhompOhpFddf+eQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
'@playwright/test': ^1.47.0
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
@@ -1608,6 +1598,9 @@ packages:
|
||||
'@emnapi/runtime@1.6.0':
|
||||
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
|
||||
|
||||
'@emnapi/runtime@1.7.1':
|
||||
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||
|
||||
@@ -2021,8 +2014,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==}
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@@ -2033,8 +2026,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.4':
|
||||
resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==}
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@@ -2044,8 +2037,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==}
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
@@ -2054,8 +2047,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||
resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==}
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
@@ -2064,8 +2057,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
@@ -2074,8 +2067,8 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
@@ -2084,18 +2077,23 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
@@ -2104,8 +2102,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
@@ -2114,8 +2112,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
@@ -2124,8 +2122,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
@@ -2135,8 +2133,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -2147,26 +2145,32 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.4':
|
||||
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.4':
|
||||
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.1':
|
||||
resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.4':
|
||||
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
@@ -2177,8 +2181,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.4':
|
||||
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@@ -2189,8 +2193,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -2201,8 +2205,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@@ -2212,13 +2216,13 @@ packages:
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-wasm32@0.34.4':
|
||||
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==}
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@@ -2229,8 +2233,8 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.4':
|
||||
resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==}
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@@ -2241,8 +2245,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.4':
|
||||
resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==}
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -2432,8 +2436,8 @@ packages:
|
||||
'@neoconfetti/react@1.0.0':
|
||||
resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==}
|
||||
|
||||
'@next/env@15.5.7':
|
||||
resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==}
|
||||
'@next/env@15.5.9':
|
||||
resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.2':
|
||||
resolution: {integrity: sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==}
|
||||
@@ -5481,6 +5485,9 @@ packages:
|
||||
caniuse-lite@1.0.30001751:
|
||||
resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==}
|
||||
|
||||
caniuse-lite@1.0.30001760:
|
||||
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
||||
|
||||
chai@5.3.3:
|
||||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6428,10 +6435,6 @@ packages:
|
||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||
hasBin: true
|
||||
|
||||
fast-xml-parser@5.3.0:
|
||||
resolution: {integrity: sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==}
|
||||
hasBin: true
|
||||
|
||||
fastest-stable-stringify@2.0.2:
|
||||
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
||||
|
||||
@@ -7751,9 +7754,10 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
next@15.5.7:
|
||||
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
|
||||
next@15.5.9:
|
||||
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
@@ -8884,8 +8888,8 @@ packages:
|
||||
resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
sharp@0.34.4:
|
||||
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -11406,11 +11410,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@azure/core-xml@1.5.0':
|
||||
dependencies:
|
||||
fast-xml-parser: 5.3.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@azure/identity@4.13.0':
|
||||
dependencies:
|
||||
'@azure/abort-controller': 2.1.2
|
||||
@@ -11464,17 +11463,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@azure/microsoft-playwright-testing@1.0.0-beta.7(@playwright/test@1.56.1)':
|
||||
dependencies:
|
||||
'@azure/core-rest-pipeline': 1.22.1
|
||||
'@azure/identity': 4.13.0
|
||||
'@azure/logger': 1.3.0
|
||||
'@azure/storage-blob': 12.29.1
|
||||
'@playwright/test': 1.56.1
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@azure/msal-browser@4.26.0':
|
||||
dependencies:
|
||||
'@azure/msal-common': 15.13.1
|
||||
@@ -11487,35 +11475,12 @@ snapshots:
|
||||
jsonwebtoken: 9.0.2
|
||||
uuid: 8.3.2
|
||||
|
||||
'@azure/storage-blob@12.29.1':
|
||||
'@azure/playwright@1.0.0(@playwright/test@1.56.1)':
|
||||
dependencies:
|
||||
'@azure/abort-controller': 2.1.2
|
||||
'@azure/core-auth': 1.10.1
|
||||
'@azure/core-client': 1.10.1
|
||||
'@azure/core-http-compat': 2.3.1
|
||||
'@azure/core-lro': 2.7.2
|
||||
'@azure/core-paging': 1.6.2
|
||||
'@azure/core-rest-pipeline': 1.22.1
|
||||
'@azure/core-tracing': 1.3.1
|
||||
'@azure/core-util': 1.13.1
|
||||
'@azure/core-xml': 1.5.0
|
||||
'@azure/logger': 1.3.0
|
||||
'@azure/storage-common': 12.1.1
|
||||
events: 3.3.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@azure/storage-common@12.1.1':
|
||||
dependencies:
|
||||
'@azure/abort-controller': 2.1.2
|
||||
'@azure/core-auth': 1.10.1
|
||||
'@azure/core-http-compat': 2.3.1
|
||||
'@azure/core-rest-pipeline': 1.22.1
|
||||
'@azure/core-tracing': 1.3.1
|
||||
'@azure/core-util': 1.13.1
|
||||
'@azure/logger': 1.3.0
|
||||
events: 3.3.0
|
||||
'@playwright/test': 1.56.1
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11830,6 +11795,11 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.7.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -12113,9 +12083,9 @@ snapshots:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.4':
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.1':
|
||||
@@ -12123,63 +12093,66 @@ snapshots:
|
||||
'@img/sharp-libvips-darwin-x64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.4':
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.1':
|
||||
@@ -12187,9 +12160,9 @@ snapshots:
|
||||
'@img/sharp-libvips-linux-arm64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.4':
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.1':
|
||||
@@ -12197,14 +12170,19 @@ snapshots:
|
||||
'@img/sharp-libvips-linux-arm': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.4':
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.4':
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.1':
|
||||
@@ -12212,9 +12190,9 @@ snapshots:
|
||||
'@img/sharp-libvips-linux-s390x': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.4':
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.1':
|
||||
@@ -12222,9 +12200,9 @@ snapshots:
|
||||
'@img/sharp-libvips-linux-x64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.4':
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.1':
|
||||
@@ -12232,9 +12210,9 @@ snapshots:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.1':
|
||||
@@ -12242,9 +12220,9 @@ snapshots:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.1':
|
||||
@@ -12252,24 +12230,24 @@ snapshots:
|
||||
'@emnapi/runtime': 1.6.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.4':
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.6.0
|
||||
'@emnapi/runtime': 1.7.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.4':
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.4':
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.4':
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@intercom/messenger-js-sdk@0.0.14': {}
|
||||
@@ -12595,13 +12573,13 @@ snapshots:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.6.0
|
||||
'@emnapi/runtime': 1.6.0
|
||||
'@emnapi/runtime': 1.7.1
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@neoconfetti/react@1.0.0': {}
|
||||
|
||||
'@next/env@15.5.7': {}
|
||||
'@next/env@15.5.9': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.2':
|
||||
dependencies:
|
||||
@@ -14128,7 +14106,7 @@ snapshots:
|
||||
|
||||
'@sentry/core@10.5.0': {}
|
||||
|
||||
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)(webpack@5.99.8(esbuild@0.25.10))':
|
||||
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)(webpack@5.99.8(esbuild@0.25.10))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.37.0
|
||||
@@ -14141,7 +14119,7 @@ snapshots:
|
||||
'@sentry/vercel-edge': 10.5.0
|
||||
'@sentry/webpack-plugin': 4.6.0(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.10))
|
||||
chalk: 3.0.0
|
||||
next: 15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
resolve: 1.22.8
|
||||
rollup: 4.52.5
|
||||
stacktrace-parser: 0.1.11
|
||||
@@ -16072,6 +16050,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001751: {}
|
||||
|
||||
caniuse-lite@1.0.30001760: {}
|
||||
|
||||
chai@5.3.3:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -17205,10 +17185,6 @@ snapshots:
|
||||
dependencies:
|
||||
strnum: 2.1.1
|
||||
|
||||
fast-xml-parser@5.3.0:
|
||||
dependencies:
|
||||
strnum: 2.1.1
|
||||
|
||||
fastest-stable-stringify@2.0.2: {}
|
||||
|
||||
fastq@1.19.1:
|
||||
@@ -18569,13 +18545,13 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next-auth@4.24.12(patch_hash=bdy3m55bopfzpysceipfxj5eei)(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(nodemailer@7.0.11)(react-dom@19.1.2(react@19.1.2))(react@19.1.2):
|
||||
next-auth@4.24.12(patch_hash=bdy3m55bopfzpysceipfxj5eei)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(nodemailer@7.0.11)(react-dom@19.1.2(react@19.1.2))(react@19.1.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@panva/hkdf': 1.2.1
|
||||
cookie: 0.7.2
|
||||
jose: 4.15.9
|
||||
next: 15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
oauth: 0.9.15
|
||||
openid-client: 5.7.1
|
||||
preact: 10.26.6
|
||||
@@ -18586,19 +18562,19 @@ snapshots:
|
||||
optionalDependencies:
|
||||
nodemailer: 7.0.11
|
||||
|
||||
next-safe-action@7.10.8(next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(zod@3.24.4):
|
||||
next-safe-action@7.10.8(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(zod@3.24.4):
|
||||
dependencies:
|
||||
next: 15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
|
||||
react: 19.1.2
|
||||
react-dom: 19.1.2(react@19.1.2)
|
||||
optionalDependencies:
|
||||
zod: 3.24.4
|
||||
|
||||
next@15.5.7(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2):
|
||||
next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.1.2(react@19.1.2))(react@19.1.2):
|
||||
dependencies:
|
||||
'@next/env': 15.5.7
|
||||
'@next/env': 15.5.9
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001751
|
||||
caniuse-lite: 1.0.30001760
|
||||
postcss: 8.4.31
|
||||
react: 19.1.2
|
||||
react-dom: 19.1.2(react@19.1.2)
|
||||
@@ -18614,7 +18590,7 @@ snapshots:
|
||||
'@next/swc-win32-x64-msvc': 15.5.7
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.56.1
|
||||
sharp: 0.34.4
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
@@ -19809,34 +19785,36 @@ snapshots:
|
||||
'@img/sharp-win32-ia32': 0.34.1
|
||||
'@img/sharp-win32-x64': 0.34.1
|
||||
|
||||
sharp@0.34.4:
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.0.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.4
|
||||
'@img/sharp-darwin-x64': 0.34.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||
'@img/sharp-linux-arm': 0.34.4
|
||||
'@img/sharp-linux-arm64': 0.34.4
|
||||
'@img/sharp-linux-ppc64': 0.34.4
|
||||
'@img/sharp-linux-s390x': 0.34.4
|
||||
'@img/sharp-linux-x64': 0.34.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.4
|
||||
'@img/sharp-linuxmusl-x64': 0.34.4
|
||||
'@img/sharp-wasm32': 0.34.4
|
||||
'@img/sharp-win32-arm64': 0.34.4
|
||||
'@img/sharp-win32-ia32': 0.34.4
|
||||
'@img/sharp-win32-x64': 0.34.4
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
|
||||
Reference in New Issue
Block a user