mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
Compare commits
5 Commits
response-m
...
fix-date-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9a884364 | ||
|
|
da4211f0b0 | ||
|
|
b21827cb32 | ||
|
|
4424a8a21d | ||
|
|
eb030f9ed6 |
@@ -5,7 +5,7 @@ permissions:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,6 +14,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
23
.github/workflows/tolgee.yml
vendored
23
.github/workflows/tolgee.yml
vendored
@@ -3,7 +3,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -23,18 +23,16 @@ jobs:
|
|||||||
- name: Get source branch name
|
- name: Get source branch name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
run: |
|
run: |
|
||||||
# For PR merges, use the head ref from the pull request event
|
RAW_BRANCH="${{ github.head_ref }}"
|
||||||
SOURCE_BRANCH="${{ github.head_ref }}"
|
SOURCE_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g')
|
||||||
|
|
||||||
# Only remove username prefix if needed
|
|
||||||
if [[ "$SOURCE_BRANCH" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]+/ ]]; then
|
|
||||||
PREFIX=${SOURCE_BRANCH%%/*}
|
|
||||||
if [[ ! "$PREFIX" =~ ^(feature|fix|bugfix|hotfix|release|chore|docs|test|refactor|style|perf|build|ci|revert)$ ]]; then
|
|
||||||
SOURCE_BRANCH=${SOURCE_BRANCH#*/}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> $GITHUB_ENV
|
# Safely add to environment variables using GitHub's recommended method
|
||||||
|
# This prevents environment variable injection attacks
|
||||||
|
echo "SOURCE_BRANCH<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$SOURCE_BRANCH" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
|
||||||
echo "Detected source branch: $SOURCE_BRANCH"
|
echo "Detected source branch: $SOURCE_BRANCH"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -53,6 +51,7 @@ jobs:
|
|||||||
--filter-tag "draft:${SOURCE_BRANCH}" \
|
--filter-tag "draft:${SOURCE_BRANCH}" \
|
||||||
--tag production \
|
--tag production \
|
||||||
--untag "draft:${SOURCE_BRANCH}"
|
--untag "draft:${SOURCE_BRANCH}"
|
||||||
|
--verbose
|
||||||
|
|
||||||
- name: Tag unused production keys as Deprecated
|
- name: Tag unused production keys as Deprecated
|
||||||
run: |
|
run: |
|
||||||
@@ -60,6 +59,7 @@ jobs:
|
|||||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
--filter-not-extracted --filter-tag production \
|
--filter-not-extracted --filter-tag production \
|
||||||
--tag deprecated --untag production
|
--tag deprecated --untag production
|
||||||
|
--verbose
|
||||||
|
|
||||||
- name: Tag unused draft:current-branch keys as Deprecated
|
- name: Tag unused draft:current-branch keys as Deprecated
|
||||||
run: |
|
run: |
|
||||||
@@ -67,6 +67,7 @@ jobs:
|
|||||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
||||||
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
||||||
|
--verbose
|
||||||
|
|
||||||
- name: Sync with backup
|
- name: Sync with backup
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ packages/lib/uploads
|
|||||||
apps/web/public/js
|
apps/web/public/js
|
||||||
packages/database/migrations
|
packages/database/migrations
|
||||||
branch.json
|
branch.json
|
||||||
|
.vercel
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
{
|
{
|
||||||
"language": "zh-Hant-TW",
|
"language": "zh-Hant-TW",
|
||||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "pt-PT",
|
||||||
|
"path": "./packages/lib/messages/pt-PT.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"forceMode": "OVERRIDE"
|
"forceMode": "OVERRIDE"
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error in sendEmail:", error);
|
||||||
throw new InvalidInputError("Incorrect SMTP credentials");
|
throw new InvalidInputError("Incorrect SMTP credentials");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const QuestionToggleTable = ({
|
|||||||
</th>
|
</th>
|
||||||
<th className="w-1/6 text-sm font-semibold">{t("common.show")}</th>
|
<th className="w-1/6 text-sm font-semibold">{t("common.show")}</th>
|
||||||
<th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
|
<th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
|
||||||
<th className="text-sm font-semibold">{t("common.placeholder")}</th>
|
<th className="text-sm font-semibold">{t("common.label")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const nextConfig = {
|
|||||||
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
|
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"],
|
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW", "pt-PT"],
|
||||||
localeDetection: false,
|
localeDetection: false,
|
||||||
defaultLocale: "en-US",
|
defaultLocale: "en-US",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -205,22 +205,20 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
|||||||
|
|
||||||
// Address Question
|
// Address Question
|
||||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||||
await expect(
|
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
|
||||||
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
|
||||||
).toBeVisible();
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||||
.fill("This is my Address");
|
.fill("This is my Address");
|
||||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||||
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
|
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// Contact Info Question
|
// Contact Info Question
|
||||||
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
|
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
|
||||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
await expect(page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
||||||
await page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
await page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
||||||
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
|
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// Ranking Question
|
// Ranking Question
|
||||||
@@ -866,21 +864,17 @@ test.describe("Testing Survey with advanced logic", async () => {
|
|||||||
// Address Question
|
// Address Question
|
||||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
|
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||||
.fill("This is my Address");
|
.fill("This is my Address");
|
||||||
await expect(
|
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
|
||||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
|
||||||
).toBeVisible();
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||||
.fill("This is my city");
|
.fill("This is my city");
|
||||||
await expect(
|
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip)
|
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||||
).toBeVisible();
|
|
||||||
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
|
||||||
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
|
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// loading spinner -> wait for it to disappear
|
// loading spinner -> wait for it to disappear
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DevTools, Tolgee } from "@tolgee/web";
|
|||||||
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
|
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;
|
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;
|
||||||
|
|
||||||
export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"];
|
export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "pt-PT", "zh-Hant-TW"];
|
||||||
|
|
||||||
export const DEFAULT_LANGUAGE = "en-US";
|
export const DEFAULT_LANGUAGE = "en-US";
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export function TolgeeBase() {
|
|||||||
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
|
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
|
||||||
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.json"),
|
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.json"),
|
||||||
"pt-BR": () => import("@formbricks/lib/messages/pt-BR.json"),
|
"pt-BR": () => import("@formbricks/lib/messages/pt-BR.json"),
|
||||||
|
"pt-PT": () => import("@formbricks/lib/messages/pt-PT.json"),
|
||||||
"zh-Hant-TW": () => import("@formbricks/lib/messages/zh-Hant-TW.json"),
|
"zh-Hant-TW": () => import("@formbricks/lib/messages/zh-Hant-TW.json"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export const STRIPE_API_VERSION = "2024-06-20";
|
|||||||
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const;
|
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const;
|
||||||
|
|
||||||
export const DEFAULT_LOCALE = "en-US";
|
export const DEFAULT_LOCALE = "en-US";
|
||||||
export const AVAILABLE_LOCALES: TUserLocale[] = ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW"];
|
export const AVAILABLE_LOCALES: TUserLocale[] = ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"];
|
||||||
|
|
||||||
// Billing constants
|
// Billing constants
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.",
|
"how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "Wie man eine Umfrage in deine React Native App einbettet",
|
"how_to_embed_a_survey_on_your_react_native_app": "Wie man eine Umfrage in deine React Native App einbettet",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "Wie man eine Umfrage in seine App einbettet",
|
"how_to_embed_a_survey_on_your_web_app": "Wie man eine Umfrage in seine App einbettet",
|
||||||
"identify_users": "Benutzer identifizieren",
|
|
||||||
"identify_users_and_set_attributes": "Benutzer identifizieren und Attribute festlegen",
|
"identify_users_and_set_attributes": "Benutzer identifizieren und Attribute festlegen",
|
||||||
"identify_users_description": "Hast Du die E-Mail-Adresse oder eine Benutzer-ID? Füge sie der URL hinzu.",
|
|
||||||
"impressions": "Eindrücke",
|
"impressions": "Eindrücke",
|
||||||
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
||||||
"includes_all": "Beinhaltet alles",
|
"includes_all": "Beinhaltet alles",
|
||||||
|
|||||||
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours you’ll receive the first responses.",
|
"how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours you’ll receive the first responses.",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "How to embed a survey on your React Native app",
|
"how_to_embed_a_survey_on_your_react_native_app": "How to embed a survey on your React Native app",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "How to embed a survey on your web app",
|
"how_to_embed_a_survey_on_your_web_app": "How to embed a survey on your web app",
|
||||||
"identify_users": "Identify users",
|
|
||||||
"identify_users_and_set_attributes": "identify users and set attributes",
|
"identify_users_and_set_attributes": "identify users and set attributes",
|
||||||
"identify_users_description": "You have the email address or a userId? Append it to the URL.",
|
|
||||||
"impressions": "Impressions",
|
"impressions": "Impressions",
|
||||||
"impressions_tooltip": "Number of times the survey has been viewed.",
|
"impressions_tooltip": "Number of times the survey has been viewed.",
|
||||||
"includes_all": "Includes all",
|
"includes_all": "Includes all",
|
||||||
|
|||||||
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.",
|
"how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "Comment intégrer un sondage dans votre application React Native",
|
"how_to_embed_a_survey_on_your_react_native_app": "Comment intégrer un sondage dans votre application React Native",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "Comment intégrer une enquête dans votre application web",
|
"how_to_embed_a_survey_on_your_web_app": "Comment intégrer une enquête dans votre application web",
|
||||||
"identify_users": "Identifier les utilisateurs",
|
|
||||||
"identify_users_and_set_attributes": "identifier les utilisateurs et définir des attributs",
|
"identify_users_and_set_attributes": "identifier les utilisateurs et définir des attributs",
|
||||||
"identify_users_description": "Avez-vous l'adresse e-mail ou un identifiant utilisateur ? Ajoutez-le à l'URL.",
|
|
||||||
"impressions": "Impressions",
|
"impressions": "Impressions",
|
||||||
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
||||||
"includes_all": "Comprend tous",
|
"includes_all": "Comprend tous",
|
||||||
|
|||||||
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.",
|
"how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar uma pesquisa no seu app React Native",
|
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar uma pesquisa no seu app React Native",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "Como incorporar uma pesquisa no seu app web",
|
"how_to_embed_a_survey_on_your_web_app": "Como incorporar uma pesquisa no seu app web",
|
||||||
"identify_users": "Identificar usuários",
|
|
||||||
"identify_users_and_set_attributes": "identificar usuários e definir atributos",
|
"identify_users_and_set_attributes": "identificar usuários e definir atributos",
|
||||||
"identify_users_description": "Você tem o endereço de e-mail ou um userId? Adiciona isso ao URL.",
|
|
||||||
"impressions": "Impressões",
|
"impressions": "Impressões",
|
||||||
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
||||||
"includes_all": "Inclui tudo",
|
"includes_all": "Inclui tudo",
|
||||||
|
|||||||
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.",
|
"how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar um questionário na sua aplicação React Native",
|
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar um questionário na sua aplicação React Native",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "Como incorporar um questionário na sua aplicação web",
|
"how_to_embed_a_survey_on_your_web_app": "Como incorporar um questionário na sua aplicação web",
|
||||||
"identify_users": "Identificar utilizadores",
|
|
||||||
"identify_users_and_set_attributes": "identificar utilizadores e definir atributos",
|
"identify_users_and_set_attributes": "identificar utilizadores e definir atributos",
|
||||||
"identify_users_description": "Tem o endereço de email ou um userId? Adicione-o ao URL.",
|
|
||||||
"impressions": "Impressões",
|
"impressions": "Impressões",
|
||||||
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
||||||
"includes_all": "Inclui tudo",
|
"includes_all": "Inclui tudo",
|
||||||
|
|||||||
@@ -1750,9 +1750,7 @@
|
|||||||
"how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。",
|
"how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。",
|
||||||
"how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷",
|
"how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷",
|
||||||
"how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷",
|
"how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷",
|
||||||
"identify_users": "識別使用者",
|
|
||||||
"identify_users_and_set_attributes": "識別使用者並設定屬性",
|
"identify_users_and_set_attributes": "識別使用者並設定屬性",
|
||||||
"identify_users_description": "您有電子郵件地址或使用者 ID 嗎?將其附加到網址。",
|
|
||||||
"impressions": "曝光數",
|
"impressions": "曝光數",
|
||||||
"impressions_tooltip": "問卷已檢視的次數。",
|
"impressions_tooltip": "問卷已檢視的次數。",
|
||||||
"includes_all": "包含全部",
|
"includes_all": "包含全部",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatDistance, intlFormat } from "date-fns";
|
import { formatDistance, intlFormat } from "date-fns";
|
||||||
import { de, enUS, fr, ptBR, zhTW } from "date-fns/locale";
|
import { de, enUS, fr, pt, ptBR, zhTW } from "date-fns/locale";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|
||||||
export const convertDateString = (dateString: string) => {
|
export const convertDateString = (dateString: string) => {
|
||||||
@@ -88,6 +88,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
|||||||
return fr;
|
return fr;
|
||||||
case "zh-Hant-TW":
|
case "zh-Hant-TW":
|
||||||
return zhTW;
|
return zhTW;
|
||||||
|
case "pt-PT":
|
||||||
|
return pt;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,36 @@ export function FileInput({
|
|||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [parent] = useAutoAnimate();
|
const [parent] = useAutoAnimate();
|
||||||
|
|
||||||
|
// Helper function to filter duplicate files
|
||||||
|
const filterDuplicateFiles = <T extends { name: string }>(
|
||||||
|
files: T[],
|
||||||
|
checkAgainstSelected: boolean = true
|
||||||
|
): {
|
||||||
|
filteredFiles: T[];
|
||||||
|
duplicateFiles: T[];
|
||||||
|
} => {
|
||||||
|
const existingFileNames = fileUrls ? fileUrls.map(getOriginalFileNameFromUrl) : [];
|
||||||
|
|
||||||
|
const duplicateFiles = files.filter(
|
||||||
|
(file) =>
|
||||||
|
existingFileNames.includes(file.name) ||
|
||||||
|
(checkAgainstSelected && selectedFiles.some((selectedFile) => selectedFile.name === file.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredFiles = files.filter(
|
||||||
|
(file) =>
|
||||||
|
!existingFileNames.includes(file.name) &&
|
||||||
|
(!checkAgainstSelected || !selectedFiles.some((selectedFile) => selectedFile.name === file.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicateFiles.length > 0) {
|
||||||
|
const duplicateNames = duplicateFiles.map((file) => file.name).join(", ");
|
||||||
|
alert(`The following files are already uploaded: ${duplicateNames}. Duplicate files are not allowed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { filteredFiles, duplicateFiles };
|
||||||
|
};
|
||||||
|
|
||||||
// Listen for the native file-upload event dispatched via window.formbricksSurveys.onFilePick
|
// Listen for the native file-upload event dispatched via window.formbricksSurveys.onFilePick
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNativeFileUpload = async (
|
const handleNativeFileUpload = async (
|
||||||
@@ -47,7 +77,7 @@ export function FileInput({
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
// Filter out files that exceed the maximum size
|
// Filter out files that exceed the maximum size
|
||||||
const filteredFiles: typeof filesFromNative = [];
|
let filteredFiles: typeof filesFromNative = [];
|
||||||
const rejectedFiles: string[] = [];
|
const rejectedFiles: string[] = [];
|
||||||
|
|
||||||
if (maxSizeInMB) {
|
if (maxSizeInMB) {
|
||||||
@@ -67,6 +97,10 @@ export function FileInput({
|
|||||||
filteredFiles.push(...filesFromNative);
|
filteredFiles.push(...filesFromNative);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for duplicate files - native uploads don't need to check against selectedFiles
|
||||||
|
const { filteredFiles: nonDuplicateFiles } = filterDuplicateFiles(filteredFiles, false);
|
||||||
|
filteredFiles = nonDuplicateFiles;
|
||||||
|
|
||||||
// Display alert for rejected files
|
// Display alert for rejected files
|
||||||
if (rejectedFiles.length > 0) {
|
if (rejectedFiles.length > 0) {
|
||||||
const fileNames = rejectedFiles.join(", ");
|
const fileNames = rejectedFiles.join(", ");
|
||||||
@@ -113,7 +147,7 @@ export function FileInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelection = async (files: FileList) => {
|
const handleFileSelection = async (files: FileList) => {
|
||||||
const fileArray = Array.from(files);
|
let fileArray = Array.from(files);
|
||||||
|
|
||||||
if (!allowMultipleFiles && fileArray.length > 1) {
|
if (!allowMultipleFiles && fileArray.length > 1) {
|
||||||
alert("Only one file can be uploaded at a time.");
|
alert("Only one file can be uploaded at a time.");
|
||||||
@@ -125,8 +159,17 @@ export function FileInput({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for duplicate files
|
||||||
|
const { filteredFiles: nonDuplicateFiles } = filterDuplicateFiles(fileArray);
|
||||||
|
|
||||||
|
if (nonDuplicateFiles.length === 0) {
|
||||||
|
return; // No non-duplicate files to process
|
||||||
|
}
|
||||||
|
|
||||||
|
fileArray = nonDuplicateFiles;
|
||||||
|
|
||||||
// filter out files that are not allowed
|
// filter out files that are not allowed
|
||||||
const validFiles = Array.from(files).filter((file) => {
|
const validFiles = fileArray.filter((file) => {
|
||||||
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
|
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
|
||||||
if (allowedFileExtensions) {
|
if (allowedFileExtensions) {
|
||||||
return allowedFileExtensions.includes(fileExtension);
|
return allowedFileExtensions.includes(fileExtension);
|
||||||
|
|||||||
7
packages/surveys/src/components/general/label.tsx
Normal file
7
packages/surveys/src/components/general/label.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
interface LabelProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Label({ text }: LabelProps) {
|
||||||
|
return <label className="fb-text-subheading fb-font-normal fb-text-sm">{text}</label>;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
|
|||||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||||
import { Headline } from "@/components/general/headline";
|
import { Headline } from "@/components/general/headline";
|
||||||
import { Input } from "@/components/general/input";
|
import { Input } from "@/components/general/input";
|
||||||
|
import { Label } from "@/components/general/label";
|
||||||
import { QuestionMedia } from "@/components/general/question-media";
|
import { QuestionMedia } from "@/components/general/question-media";
|
||||||
import { Subheader } from "@/components/general/subheader";
|
import { Subheader } from "@/components/general/subheader";
|
||||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||||
@@ -56,32 +57,32 @@ export function AddressQuestion({
|
|||||||
{
|
{
|
||||||
id: "addressLine1",
|
id: "addressLine1",
|
||||||
...question.addressLine1,
|
...question.addressLine1,
|
||||||
placeholder: question.addressLine1.placeholder[languageCode],
|
label: question.addressLine1.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "addressLine2",
|
id: "addressLine2",
|
||||||
...question.addressLine2,
|
...question.addressLine2,
|
||||||
placeholder: question.addressLine2.placeholder[languageCode],
|
label: question.addressLine2.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "city",
|
id: "city",
|
||||||
...question.city,
|
...question.city,
|
||||||
placeholder: question.city.placeholder[languageCode],
|
label: question.city.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "state",
|
id: "state",
|
||||||
...question.state,
|
...question.state,
|
||||||
placeholder: question.state.placeholder[languageCode],
|
label: question.state.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "zip",
|
id: "zip",
|
||||||
...question.zip,
|
...question.zip,
|
||||||
placeholder: question.zip.placeholder[languageCode],
|
label: question.zip.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "country",
|
id: "country",
|
||||||
...question.country,
|
...question.country,
|
||||||
placeholder: question.country.placeholder[languageCode],
|
label: question.country.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -155,19 +156,21 @@ export function AddressQuestion({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
field.show && (
|
field.show && (
|
||||||
<Input
|
<div className="fb-space-y-1">
|
||||||
key={field.id}
|
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
<Input
|
||||||
required={isFieldRequired()}
|
key={field.id}
|
||||||
value={safeValue[index] || ""}
|
required={isFieldRequired()}
|
||||||
className="fb-py-3"
|
value={safeValue[index] || ""}
|
||||||
type={field.id === "email" ? "email" : "text"}
|
type={field.id === "email" ? "email" : "text"}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange(field.id, e.currentTarget.value);
|
handleChange(field.id, e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
ref={index === 0 ? addressRef : null}
|
ref={index === 0 ? addressRef : null}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
/>
|
aria-label={field.label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
|
|||||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||||
import { Headline } from "@/components/general/headline";
|
import { Headline } from "@/components/general/headline";
|
||||||
import { Input } from "@/components/general/input";
|
import { Input } from "@/components/general/input";
|
||||||
|
import { Label } from "@/components/general/label";
|
||||||
import { QuestionMedia } from "@/components/general/question-media";
|
import { QuestionMedia } from "@/components/general/question-media";
|
||||||
import { Subheader } from "@/components/general/subheader";
|
import { Subheader } from "@/components/general/subheader";
|
||||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||||
@@ -56,27 +57,27 @@ export function ContactInfoQuestion({
|
|||||||
{
|
{
|
||||||
id: "firstName",
|
id: "firstName",
|
||||||
...question.firstName,
|
...question.firstName,
|
||||||
placeholder: question.firstName.placeholder[languageCode],
|
label: question.firstName.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "lastName",
|
id: "lastName",
|
||||||
...question.lastName,
|
...question.lastName,
|
||||||
placeholder: question.lastName.placeholder[languageCode],
|
label: question.lastName.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "email",
|
id: "email",
|
||||||
...question.email,
|
...question.email,
|
||||||
placeholder: question.email.placeholder[languageCode],
|
label: question.email.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "phone",
|
id: "phone",
|
||||||
...question.phone,
|
...question.phone,
|
||||||
placeholder: question.phone.placeholder[languageCode],
|
label: question.phone.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "company",
|
id: "company",
|
||||||
...question.company,
|
...question.company,
|
||||||
placeholder: question.company.placeholder[languageCode],
|
label: question.company.placeholder[languageCode],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -157,19 +158,21 @@ export function ContactInfoQuestion({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
field.show && (
|
field.show && (
|
||||||
<Input
|
<div className="fb-space-y-1">
|
||||||
ref={index === 0 ? contactInfoRef : null}
|
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||||
key={field.id}
|
<Input
|
||||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
ref={index === 0 ? contactInfoRef : null}
|
||||||
required={isFieldRequired()}
|
key={field.id}
|
||||||
value={safeValue[index] || ""}
|
required={isFieldRequired()}
|
||||||
className="fb-py-3"
|
value={safeValue[index] || ""}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange(field.id, e.currentTarget.value);
|
handleChange(field.id, e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
/>
|
aria-label={field.label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1168,7 +1168,7 @@ export const ZSurvey = z
|
|||||||
const multiLangIssueInPlaceholder =
|
const multiLangIssueInPlaceholder =
|
||||||
field.show &&
|
field.show &&
|
||||||
validateQuestionLabels(
|
validateQuestionLabels(
|
||||||
`Placeholder for field ${field.label}`,
|
`Label for field ${field.label}`,
|
||||||
field.placeholder,
|
field.placeholder,
|
||||||
languages,
|
languages,
|
||||||
questionIndex,
|
questionIndex,
|
||||||
@@ -1202,7 +1202,7 @@ export const ZSurvey = z
|
|||||||
const multiLangIssueInPlaceholder =
|
const multiLangIssueInPlaceholder =
|
||||||
field.show &&
|
field.show &&
|
||||||
validateQuestionLabels(
|
validateQuestionLabels(
|
||||||
`Placeholder for field ${field.label}`,
|
`Label for field ${field.label}`,
|
||||||
field.placeholder,
|
field.placeholder,
|
||||||
languages,
|
languages,
|
||||||
questionIndex,
|
questionIndex,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
|
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
|
||||||
|
|
||||||
export const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW"]);
|
export const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"]);
|
||||||
|
|
||||||
export type TUserLocale = z.infer<typeof ZUserLocale>;
|
export type TUserLocale = z.infer<typeof ZUserLocale>;
|
||||||
export const ZUserObjective = z.enum([
|
export const ZUserObjective = z.enum([
|
||||||
|
|||||||
Reference in New Issue
Block a user