mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-03 13:37:26 -06:00
Compare commits
5 Commits
feature/ma
...
fix-date-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9a884364 | ||
|
|
da4211f0b0 | ||
|
|
b21827cb32 | ||
|
|
4424a8a21d | ||
|
|
eb030f9ed6 |
@@ -39,7 +39,6 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
# See optional configurations below if you want to disable these features.
|
||||
|
||||
MAIL_FROM=noreply@example.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
@@ -203,4 +202,4 @@ UNKEY_ROOT_KEY=
|
||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||
|
||||
# NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
@@ -5,7 +5,7 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
@@ -14,6 +14,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
23
.github/workflows/tolgee.yml
vendored
23
.github/workflows/tolgee.yml
vendored
@@ -3,7 +3,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
@@ -23,18 +23,16 @@ jobs:
|
||||
- name: Get source branch name
|
||||
id: branch-name
|
||||
run: |
|
||||
# For PR merges, use the head ref from the pull request event
|
||||
SOURCE_BRANCH="${{ github.head_ref }}"
|
||||
RAW_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"
|
||||
|
||||
- name: Setup Node.js
|
||||
@@ -53,6 +51,7 @@ jobs:
|
||||
--filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--tag production \
|
||||
--untag "draft:${SOURCE_BRANCH}"
|
||||
--verbose
|
||||
|
||||
- name: Tag unused production keys as Deprecated
|
||||
run: |
|
||||
@@ -60,6 +59,7 @@ jobs:
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag production \
|
||||
--tag deprecated --untag production
|
||||
--verbose
|
||||
|
||||
- name: Tag unused draft:current-branch keys as Deprecated
|
||||
run: |
|
||||
@@ -67,6 +67,7 @@ jobs:
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
||||
--verbose
|
||||
|
||||
- name: Sync with backup
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,4 +53,5 @@ yarn-error.log*
|
||||
packages/lib/uploads
|
||||
apps/web/public/js
|
||||
packages/database/migrations
|
||||
branch.json
|
||||
branch.json
|
||||
.vercel
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"language": "zh-Hant-TW",
|
||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||
},
|
||||
{
|
||||
"language": "pt-PT",
|
||||
"path": "./packages/lib/messages/pt-PT.json"
|
||||
}
|
||||
],
|
||||
"forceMode": "OVERRIDE"
|
||||
|
||||
@@ -6,7 +6,6 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import {
|
||||
DEBUG,
|
||||
MAIL_FROM,
|
||||
MAIL_FROM_NAME,
|
||||
SMTP_AUTHENTICATED,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
@@ -70,12 +69,13 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
const emailDefaults = {
|
||||
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
};
|
||||
await transporter.sendMail({ ...emailDefaults, ...emailData });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error in sendEmail:", error);
|
||||
throw new InvalidInputError("Incorrect SMTP credentials");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,7 +81,7 @@ export const QuestionToggleTable = ({
|
||||
</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="text-sm font-semibold">{t("common.placeholder")}</th>
|
||||
<th className="text-sm font-semibold">{t("common.label")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -23,7 +23,7 @@ const nextConfig = {
|
||||
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
|
||||
},
|
||||
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,
|
||||
defaultLocale: "en-US",
|
||||
},
|
||||
|
||||
@@ -205,22 +205,20 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Contact Info Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
||||
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Ranking Question
|
||||
@@ -866,21 +864,17 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
.fill("This is my city");
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip)
|
||||
).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// 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 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";
|
||||
|
||||
@@ -20,6 +20,7 @@ export function TolgeeBase() {
|
||||
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
|
||||
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.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"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -224,9 +224,6 @@ EOT
|
||||
echo -n "Enter your SMTP configured Email ID: "
|
||||
read mail_from
|
||||
|
||||
echo -n "Enter your SMTP configured Email Name: "
|
||||
read mail_from_name
|
||||
|
||||
echo -n "Enter your SMTP Host URL: "
|
||||
read smtp_host
|
||||
|
||||
@@ -247,7 +244,6 @@ EOT
|
||||
|
||||
else
|
||||
mail_from=""
|
||||
mail_from_name=""
|
||||
smtp_host=""
|
||||
smtp_port=""
|
||||
smtp_user=""
|
||||
@@ -274,7 +270,6 @@ EOT
|
||||
|
||||
if [[ -n $mail_from ]]; then
|
||||
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
|
||||
sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml
|
||||
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml
|
||||
|
||||
@@ -1039,7 +1039,6 @@ x-environment: &environment
|
||||
|
||||
# Email Configuration
|
||||
MAIL_FROM:
|
||||
MAIL_FROM_NAME:
|
||||
SMTP_HOST:
|
||||
SMTP_PORT:
|
||||
SMTP_SECURE_ENABLED:
|
||||
|
||||
@@ -33,7 +33,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
|
||||
@@ -33,7 +33,6 @@ To enable email functionality, configure the following environment variables:
|
||||
```bash
|
||||
# Basic SMTP Configuration
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.yourprovider.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_username
|
||||
@@ -76,7 +75,6 @@ If you're using the one-click setup with Docker Compose, you can either:
|
||||
environment:
|
||||
# Email Configuration
|
||||
MAIL_FROM: noreply@yourdomain.com
|
||||
MAIL_FROM_NAME: Formbricks
|
||||
SMTP_HOST: smtp.yourprovider.com
|
||||
SMTP_PORT: 587
|
||||
SMTP_USER: your_username
|
||||
@@ -97,7 +95,6 @@ environment:
|
||||
|
||||
```bash
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=apikey
|
||||
@@ -108,7 +105,6 @@ SMTP_PASSWORD=your_sendgrid_api_key
|
||||
|
||||
```bash
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=email-smtp.us-east-1.amazonaws.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_ses_access_key
|
||||
@@ -119,7 +115,6 @@ SMTP_PASSWORD=your_ses_secret_key
|
||||
|
||||
```bash
|
||||
MAIL_FROM=your_email@gmail.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_email@gmail.com
|
||||
|
||||
@@ -83,7 +83,6 @@ export const SMTP_PASSWORD = env.SMTP_PASSWORD;
|
||||
export const SMTP_AUTHENTICATED = env.SMTP_AUTHENTICATED !== "0";
|
||||
export const SMTP_REJECT_UNAUTHORIZED_TLS = env.SMTP_REJECT_UNAUTHORIZED_TLS !== "0";
|
||||
export const MAIL_FROM = env.MAIL_FROM;
|
||||
export const MAIL_FROM_NAME = env.MAIL_FROM_NAME;
|
||||
|
||||
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
|
||||
export const ITEMS_PER_PAGE = 30;
|
||||
@@ -215,7 +214,7 @@ export const STRIPE_API_VERSION = "2024-06-20";
|
||||
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const;
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ export const env = createEnv({
|
||||
INTERCOM_SECRET_KEY: z.string().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
NEXTAUTH_SECRET: z.string().min(1),
|
||||
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
||||
NOTION_OAUTH_CLIENT_SECRET: z.string().optional(),
|
||||
@@ -172,7 +171,6 @@ export const env = createEnv({
|
||||
INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY,
|
||||
IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD,
|
||||
MAIL_FROM: process.env.MAIL_FROM,
|
||||
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export const convertDateString = (dateString: string) => {
|
||||
@@ -88,6 +88,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return fr;
|
||||
case "zh-Hant-TW":
|
||||
return zhTW;
|
||||
case "pt-PT":
|
||||
return pt;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,16 +21,16 @@ export const err = <E = Error>(error: E): ResultError<E> => ({
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
code:
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "network_error"
|
||||
| "too_many_requests";
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "network_error"
|
||||
| "too_many_requests";
|
||||
message: string;
|
||||
status: number;
|
||||
url?: URL;
|
||||
|
||||
@@ -36,6 +36,36 @@ export function FileInput({
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
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
|
||||
useEffect(() => {
|
||||
const handleNativeFileUpload = async (
|
||||
@@ -47,7 +77,7 @@ export function FileInput({
|
||||
setIsUploading(true);
|
||||
|
||||
// Filter out files that exceed the maximum size
|
||||
const filteredFiles: typeof filesFromNative = [];
|
||||
let filteredFiles: typeof filesFromNative = [];
|
||||
const rejectedFiles: string[] = [];
|
||||
|
||||
if (maxSizeInMB) {
|
||||
@@ -67,6 +97,10 @@ export function FileInput({
|
||||
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
|
||||
if (rejectedFiles.length > 0) {
|
||||
const fileNames = rejectedFiles.join(", ");
|
||||
@@ -113,7 +147,7 @@ export function FileInput({
|
||||
};
|
||||
|
||||
const handleFileSelection = async (files: FileList) => {
|
||||
const fileArray = Array.from(files);
|
||||
let fileArray = Array.from(files);
|
||||
|
||||
if (!allowMultipleFiles && fileArray.length > 1) {
|
||||
alert("Only one file can be uploaded at a time.");
|
||||
@@ -125,8 +159,17 @@ export function FileInput({
|
||||
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
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const validFiles = fileArray.filter((file) => {
|
||||
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
|
||||
if (allowedFileExtensions) {
|
||||
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 { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { Label } from "@/components/general/label";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
@@ -56,32 +57,32 @@ export function AddressQuestion({
|
||||
{
|
||||
id: "addressLine1",
|
||||
...question.addressLine1,
|
||||
placeholder: question.addressLine1.placeholder[languageCode],
|
||||
label: question.addressLine1.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "addressLine2",
|
||||
...question.addressLine2,
|
||||
placeholder: question.addressLine2.placeholder[languageCode],
|
||||
label: question.addressLine2.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
...question.city,
|
||||
placeholder: question.city.placeholder[languageCode],
|
||||
label: question.city.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
...question.state,
|
||||
placeholder: question.state.placeholder[languageCode],
|
||||
label: question.state.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "zip",
|
||||
...question.zip,
|
||||
placeholder: question.zip.placeholder[languageCode],
|
||||
label: question.zip.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
...question.country,
|
||||
placeholder: question.country.placeholder[languageCode],
|
||||
label: question.country.placeholder[languageCode],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -155,19 +156,21 @@ export function AddressQuestion({
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<Input
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
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 { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { Label } from "@/components/general/label";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
@@ -56,27 +57,27 @@ export function ContactInfoQuestion({
|
||||
{
|
||||
id: "firstName",
|
||||
...question.firstName,
|
||||
placeholder: question.firstName.placeholder[languageCode],
|
||||
label: question.firstName.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "lastName",
|
||||
...question.lastName,
|
||||
placeholder: question.lastName.placeholder[languageCode],
|
||||
label: question.lastName.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
...question.email,
|
||||
placeholder: question.email.placeholder[languageCode],
|
||||
label: question.email.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
...question.phone,
|
||||
placeholder: question.phone.placeholder[languageCode],
|
||||
label: question.phone.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "company",
|
||||
...question.company,
|
||||
placeholder: question.company.placeholder[languageCode],
|
||||
label: question.company.placeholder[languageCode],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -157,19 +158,21 @@ export function ContactInfoQuestion({
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<Input
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -121,16 +121,16 @@ export type { NetworkError, ForbiddenError };
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
code:
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "network_error"
|
||||
| "too_many_requests";
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "network_error"
|
||||
| "too_many_requests";
|
||||
message: string;
|
||||
status: number;
|
||||
url?: URL;
|
||||
|
||||
@@ -1168,7 +1168,7 @@ export const ZSurvey = z
|
||||
const multiLangIssueInPlaceholder =
|
||||
field.show &&
|
||||
validateQuestionLabels(
|
||||
`Placeholder for field ${field.label}`,
|
||||
`Label for field ${field.label}`,
|
||||
field.placeholder,
|
||||
languages,
|
||||
questionIndex,
|
||||
@@ -1202,7 +1202,7 @@ export const ZSurvey = z
|
||||
const multiLangIssueInPlaceholder =
|
||||
field.show &&
|
||||
validateQuestionLabels(
|
||||
`Placeholder for field ${field.label}`,
|
||||
`Label for field ${field.label}`,
|
||||
field.placeholder,
|
||||
languages,
|
||||
questionIndex,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
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 const ZUserObjective = z.enum([
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"IS_FORMBRICKS_CLOUD",
|
||||
"INTERCOM_SECRET_KEY",
|
||||
"MAIL_FROM",
|
||||
"MAIL_FROM_NAME",
|
||||
"NEXT_PUBLIC_LAYER_API_KEY",
|
||||
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
|
||||
"NEXT_PUBLIC_DOCSEARCH_API_KEY",
|
||||
|
||||
Reference in New Issue
Block a user