Compare commits

..

40 Commits

Author SHA1 Message Date
Piyush Gupta
a020e66008 fix: license restores 2024-11-18 18:17:39 +05:30
Piyush Gupta
9edf5a6f81 Merge branch 'main' of https://github.com/formbricks/formbricks into fix/license-cache-restored 2024-11-18 17:42:24 +05:30
Dhruwang Jariwala
7f8549124f fix: name regex (#4328) 2024-11-18 11:58:27 +00:00
Piyush Gupta
897b18d5b5 fix: build error 2024-11-16 10:28:22 +05:30
Piyush Gupta
1f693689ce Merge branch 'main' of https://github.com/formbricks/formbricks into 4307-move-ee-services-to-new-module-structure 2024-11-15 21:57:31 +05:30
Piyush Gupta
7bf6ffb2ba fix: build error, removed license cache invalidation 2024-11-15 18:47:38 +05:30
Piyush Gupta
43b1cb904d feat: logic fallback option added (#4306)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-11-15 13:13:08 +00:00
Piyush Gupta
d2cd155518 fix: cache invalidation restored 2024-11-15 14:07:35 +05:30
Piyush Gupta
9e038e326a Merge branch 'main' of https://github.com/formbricks/formbricks into 4307-move-ee-services-to-new-module-structure 2024-11-15 12:58:53 +05:30
Piyush Gupta
74fdf12e58 fix: build errors 2024-11-15 12:57:30 +05:30
Dhruwang Jariwala
762a3ca626 chore: make redis an enterprise feature (#4314) 2024-11-15 07:10:48 +00:00
Piyush Gupta
9465b3fe67 chore: migrated ee package 2024-11-15 11:10:54 +05:30
Sai Suhas Sawant
91f0d00ba2 fix: survey release and close bug for first date of the month (#4311)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2024-11-15 05:35:51 +00:00
Dhruwang Jariwala
41f42f4427 fix: recall in slack integration (#4304) 2024-11-15 04:17:26 +00:00
Piyush Gupta
98181bfe6c chore: next version upgrade (#4291) 2024-11-14 12:33:05 +00:00
Matti Nannt
a8ab4aaf2e chore: prepare 2.7.1 release (#4302) 2024-11-13 15:37:36 +01:00
Piyush Gupta
78dca7a2bf fix: response cache invalidation on person delete (#4300)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-11-13 14:13:55 +00:00
Piyush Gupta
844ea40c3a feat: adds does not include options in conditions (#4296) 2024-11-13 13:01:03 +00:00
Piyush Gupta
7a6dedf452 fix: cache invalidation in sentiment and category update (#4295)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2024-11-13 12:58:03 +00:00
Dhruwang Jariwala
b641b37308 fix: rate limiting to forget password (#4297)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-11-13 12:27:21 +00:00
Dhruwang Jariwala
8c1f8bfb42 fix: email enumeration via forgot password page (#4299)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-11-13 12:05:44 +00:00
Dhruwang Jariwala
1f1563401d fix: Email Address Disclosure via URL in Registration Process (#4241)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-11-13 11:57:53 +00:00
Dhruwang Jariwala
9fd585ee07 fix: styling fixes (#4279)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-11-13 10:59:01 +00:00
Sai Suhas Sawant
609dcabf77 feat: promote dev-actions to prod (#4245)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-11-13 08:17:41 +00:00
Matti Nannt
80d338c998 chore: add expected behaviour to github bug template (#4292) 2024-11-12 14:27:53 +01:00
ayaang-layer
306784c31b docs: add support for Layer widget (#4262)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-11-12 14:07:33 +01:00
Matti Nannt
bcd68e0f19 fix: simplify LRU cache warning to avoid confusion (#4290) 2024-11-12 13:41:05 +01:00
Dhruwang Jariwala
5764148753 fix: formbricks not working in github codespace (#4289) 2024-11-12 11:30:31 +00:00
Dhruwang Jariwala
e7edfe3ba1 fix: security checks for user input fields for email (#3819) 2024-11-12 10:01:21 +00:00
Dhruwang Jariwala
da6f54eede fix: All roles need insight into consumption (#4242)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-11-12 06:12:38 +00:00
Khaja Shaik
ade5c3d80e feat: Expanded the options of time periods in filters (#4226)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-11-12 05:49:35 +00:00
Dhruwang Jariwala
cc2600cfba fix: language error on template page (#4281)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-11-12 02:27:47 +00:00
Piyush Gupta
9b191ef3e4 fix: Back Button is Autofocused with No Margin in Modal (#4283) 2024-11-12 02:18:32 +00:00
Dhruwang Jariwala
c450c35baf fix: increase card size for link surveys (#4285)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-11-12 02:16:51 +00:00
Anshuman Pandey
b35b82f4ee fix: restricting archived attributes in the survey and segment editor (#4269)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-11-11 13:09:58 +00:00
Matti Nannt
ea52624ab2 docs: add n8n community node note (#4280) 2024-11-11 12:33:47 +01:00
Piyush Gupta
0484bccfd1 fix: correct className order in PricingTable component (#4277)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2024-11-11 11:19:50 +00:00
Mert Eroğlu
d094f63faa fix: slack integration with large organizations (#3201)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-11-11 10:19:36 +00:00
Matti Nannt
dc6bc61442 fix: conditional in german translation (#4275) 2024-11-11 11:30:58 +01:00
Dhruwang Jariwala
5918c42cf9 fix: attribute activity tab crashing (#4276) 2024-11-11 08:34:26 +00:00
268 changed files with 3249 additions and 2272 deletions

View File

@@ -1,16 +0,0 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=20
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node modules
# RUN su node -c "npm install -g <your-package-list-here>"
RUN su node -c "npm install -g pnpm"

View File

@@ -1,28 +1,6 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
{
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["dbaeumer.vscode-eslint"]
}
},
"dockerComposeFile": "docker-compose.yml",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [3000, 5432, 8025],
"name": "Node.js & PostgreSQL",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"service": "app",
"workspaceFolder": "/workspace"
"features": {},
"image": "mcr.microsoft.com/devcontainers/universal:2",
"postAttachCommand": "pnpm go",
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev"
}

View File

@@ -1,51 +0,0 @@
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick an LTS version of Node.js: 20, 18, 16, 14.
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: "20"
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Uncomment the next line to use a non-root user for all processes.
# user: node
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: pgvector/pgvector:pg17
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: formbricks
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
mailhog:
image: mailhog/mailhog
network_mode: service:app
logging:
driver:
"none" # disable saving logs
# ports:
# - 8025:8025 # web ui
# 1025:1025 # smtp server
volumes:
postgres-data: null

View File

@@ -10,6 +10,13 @@ body:
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: issue-expected-behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
id: other-information
attributes:

View File

@@ -2,7 +2,7 @@ Copyright (c) 2024 Formbricks GmbH
Portions of this software are licensed as follows:
- All content that resides under the "packages/ee/", "apps/web/modules/ee" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE".
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.

View File

@@ -228,7 +228,7 @@ The Formbricks core application is licensed under the [AGPLv3 Open Source Licens
### The Enterprise Edition
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/packages/ee) and [license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE) for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/apps/web/modules/ee) and [license](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE) for the enterprise functionality can be found in the `/apps/web/modules/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
### White-Labeling Formbricks and Other Licensing Needs

View File

@@ -15,7 +15,8 @@
"@formbricks/react-native": "workspace:*",
"expo": "51.0.26",
"expo-status-bar": "1.12.1",
"react": "18.3.1",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110",
"react-native": "0.74.4",
"react-native-webview": "13.8.6"
},

View File

@@ -1,4 +1,5 @@
import { StatusBar } from "expo-status-bar";
import type { JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, { track } from "@formbricks/react-native";

View File

@@ -14,9 +14,9 @@
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "0.452.0",
"next": "14.2.16",
"react": "18.3.1",
"react-dom": "18.3.1"
"next": "15.0.3",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110"
},
"devDependencies": {
"@formbricks/eslint-config": "workspace:*",

View File

@@ -26,15 +26,20 @@ export const metadata = {
# n8n Setup
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
<Note>
Nail down your survey? Any changes in the survey cause additional work in the n8n node. It makes
sense to first settle on the survey you want to run and then get to setting up n8n.
The Formbricks n8n node is currently only available in the n8n self-hosted version as a community node. To
install it go to "Settings" -> "Community Nodes" and install @formbricks/n8n-nodes-formbricks.
</Note>
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
## Step 1: Setup your survey incl. `questionId` for every question
<Note>
Nailed down your survey? Any changes in the survey cause additional work in the n8n node. It makes sense to
first settle on the survey you want to run and then get to setting up n8n.
</Note>
When setting up the node your life will be easier when you change the `questionId`s of your survey questions. You can only do so **before** you publish your survey.
<MdxImage

View File

@@ -5,6 +5,7 @@ import "@/styles/tailwind.css";
import glob from "fast-glob";
import { type Metadata } from "next";
import { Jost } from "next/font/google";
import Script from "next/script";
export const metadata: Metadata = {
title: {
@@ -27,6 +28,18 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<head>
{process.env.NEXT_PUBLIC_LAYER_API_KEY && (
<Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/>
)}
</head>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<Providers>
<div className="w-full">

View File

@@ -19,7 +19,7 @@ The Formbricks Core source code is licensed under AGPLv3 and available on GitHub
## Enterprise Edition License
Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our **[Enterprise License](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)**. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our **[Enterprise License](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE)**. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
### When do I need an Enterprise License?
@@ -64,7 +64,7 @@ The Formbricks core application is licensed under the **[AGPLv3 Open Source Lice
### The Enterprise Edition
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise License. The **[code](https://github.com/formbricks/formbricks/tree/main/packages/ee)** and **[license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)** for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it.
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The **[code](https://github.com/formbricks/formbricks/tree/main/apps/web/modules/ee)** and **[license](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE)** for the enterprise functionality can be found in the `/apps/web/modules/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **[Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise)** to unlock it.
## White-Labeling Formbricks and Other Licensing Needs

View File

@@ -24,7 +24,7 @@
"@mapbox/rehype-prism": "0.9.0",
"@mdx-js/loader": "3.0.1",
"@mdx-js/react": "3.0.1",
"@next/mdx": "14.2.15",
"@next/mdx": "15.0.3",
"@paralleldrive/cuid2": "2.2.2",
"@sindresorhus/slugify": "2.2.1",
"@tailwindcss/typography": "0.5.15",
@@ -39,7 +39,7 @@
"lucide-react": "0.452.0",
"mdast-util-to-string": "4.0.0",
"mdx-annotations": "0.1.4",
"next": "14.2.16",
"next": "15.0.3",
"next-plausible": "3.12.2",
"next-seo": "6.6.0",
"next-sitemap": "4.2.3",
@@ -47,8 +47,8 @@
"node-fetch": "3.3.2",
"prism-react-renderer": "2.4.0",
"prismjs": "1.29.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110",
"react-highlight-words": "0.20.0",
"react-markdown": "9.0.1",
"react-responsive-embed": "2.1.0",

View File

@@ -13,8 +13,8 @@
"dependencies": {
"@formbricks/ui": "workspace:*",
"eslint-plugin-react-refresh": "0.4.12",
"react": "18.3.1",
"react-dom": "18.3.1"
"react": "19.0.0-rc-ed15d500-20241110",
"react-dom": "19.0.0-rc-ed15d500-20241110"
},
"devDependencies": {
"@chromatic-com/storybook": "2.0.2",

View File

@@ -19,7 +19,11 @@ interface InviteOrganizationMemberProps {
const ZInviteOrganizationMemberDetails = z.object({
email: z.string().email(),
inviteMessage: z.string().trim().min(1),
inviteMessage: z
.string()
.trim()
.min(1)
.refine((value) => !/https?:\/\/|<script/i.test(value), "Invite message cannot contain URLs or scripts"),
});
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>;

View File

@@ -10,12 +10,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface InvitePageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: InvitePageProps) => {
const Page = async (props: InvitePageProps) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -8,12 +8,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ConnectPageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: ConnectPageProps) => {
const Page = async (props: ConnectPageProps) => {
const params = await props.params;
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/errors";
const OnboardingLayout = async ({ children, params }) => {
const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -11,12 +11,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface XMTemplatePageProps {
params: {
params: Promise<{
environmentId: string;
};
}>;
}
const Page = async ({ params }: XMTemplatePageProps) => {
const Page = async (props: XMTemplatePageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId);
const t = await getTranslations();

View File

@@ -11,7 +11,7 @@ import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
export const getTeamsByOrganizationId = reactCache(
(organizationId: string): Promise<TOrganizationTeam[] | null> =>
async (organizationId: string): Promise<TOrganizationTeam[] | null> =>
cache(
async () => {
validateInputs([organizationId, ZId]);

View File

@@ -5,7 +5,11 @@ import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProducts } from "@formbricks/lib/product/service";
const LandingLayout = async ({ children, params }) => {
const LandingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -1,14 +1,15 @@
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { Header } from "@formbricks/ui/components/Header";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -9,7 +9,11 @@ import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const ProductOnboardingLayout = async ({ children, params }) => {
const ProductOnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
const OnboardingLayout = async ({ children, params }) => {
const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -9,12 +9,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ChannelPageProps {
params: {
params: Promise<{
organizationId: string;
};
}>;
}
const Page = async ({ params }: ChannelPageProps) => {
const Page = async (props: ChannelPageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -9,12 +9,13 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ModePageProps {
params: {
params: Promise<{
organizationId: string;
};
}>;
}
const Page = async ({ params }: ModePageProps) => {
const Page = async (props: ModePageProps) => {
const params = await props.params;
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -1,11 +1,11 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service";
@@ -16,17 +16,19 @@ import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ProductSettingsPageProps {
params: {
params: Promise<{
organizationId: string;
};
searchParams: {
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
mode?: TProductMode;
};
}>;
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const Page = async (props: ProductSettingsPageProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);

View File

@@ -13,7 +13,11 @@ import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const SurveyEditorEnvironmentLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -4,7 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";

View File

@@ -4,7 +4,7 @@ import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";

View File

@@ -1,7 +1,7 @@
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { type JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";

View File

@@ -41,8 +41,8 @@ export const CardStylingSettings = ({
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
const isLogoVisible = !!product.logo?.url;
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "simple";
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "simple";
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "straight";
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "straight";
const roundness = form.watch("roundness") ?? 8;
const [parent] = useAutoAnimate();

View File

@@ -87,10 +87,12 @@ export function ConditionalLogic({
const handleRemoveLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const isLast = logicCopy.length === 1;
logicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
logic: logicCopy,
logicFallback: isLast ? undefined : question.logicFallback,
});
};

View File

@@ -3,7 +3,7 @@
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";

View File

@@ -4,7 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";

View File

@@ -2,6 +2,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";

View File

@@ -3,13 +3,17 @@
import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import {
findEndingCardUsedInLogic,
formatTextWithSlashes,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { GripIcon, Handshake, Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
@@ -93,6 +97,14 @@ export const EditEndingCard = ({
};
const deleteEndingCard = () => {
// checking if this ending card is used in logic
const quesIdx = findEndingCardUsedInLogic(localSurvey, endingCard.id);
if (quesIdx !== -1) {
toast.error(t("environments.surveys.edit.ending_card_used_in_logic", { questionIndex: quesIdx + 1 }));
return;
}
setLocalSurvey((prevSurvey) => {
const updatedEndings = prevSurvey.endings.filter((_, index) => index !== endingCardIndex);
return { ...prevSurvey, endings: updatedEndings };

View File

@@ -6,7 +6,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useMemo, useState } from "react";
import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";

View File

@@ -2,7 +2,17 @@ import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[en
import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import { ArrowRightIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ReactElement, useMemo } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { QUESTIONS_ICON_MAP } from "@formbricks/lib/utils/questions";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
interface LogicEditorProps {
localSurvey: TSurvey;
@@ -24,6 +34,36 @@ export function LogicEditor({
isLast,
}: LogicEditorProps) {
const t = useTranslations();
const fallbackOptions = useMemo(() => {
let options: {
icon?: ReactElement;
label: string;
value: string;
}[] = [];
localSurvey.questions.forEach((ques) => {
if (ques.id === question.id) return null;
options.push({
icon: QUESTIONS_ICON_MAP[ques.type],
label: getLocalizedValue(ques.headline, "default"),
value: ques.id,
});
});
localSurvey.endings.forEach((ending) => {
options.push({
label:
ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default") || t("environments.surveys.edit.end_screen_card")
: ending.label || t("environments.surveys.edit.redirect_thank_you_card"),
value: ending.id,
});
});
return options;
}, [localSurvey.questions, localSurvey.endings, question.id, t]);
return (
<div className="flex w-full grow flex-col gap-4 overflow-x-auto pb-2 text-sm">
<LogicEditorConditions
@@ -43,11 +83,36 @@ export function LogicEditor({
questionIdx={questionIdx}
/>
{isLast ? (
<div className="flex flex-wrap items-center space-x-2">
<div className="flex items-center space-x-2">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">
{t("environments.surveys.edit.all_other_answers_will_continue_to_the_next_question")}
<p className="text-nowrap text-slate-700">
{t("environments.surveys.edit.all_other_answers_will_continue_to")}
</p>
<Select
autoComplete="true"
defaultValue={question.logicFallback || "defaultSelection"}
onValueChange={(val) => {
updateQuestion(questionIdx, {
logicFallback: val === "defaultSelection" ? undefined : val,
});
}}>
<SelectTrigger className="w-auto bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem key="fallback_default_selection" value={"defaultSelection"}>
{t("environments.surveys.edit.next_question")}
</SelectItem>
{fallbackOptions.map((option) => (
<SelectItem key={`fallback_${option.value}`} value={option.value}>
<div className="flex items-center gap-2">
{option.icon}
{option.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</div>

View File

@@ -244,7 +244,13 @@ export function LogicEditorConditions({
const conditionOperatorOptions = getConditionOperatorOptions(condition, localSurvey);
const { show, options, showInput = false, inputType } = getMatchValueProps(condition, localSurvey, t);
const allowMultiSelect = ["equalsOneOf", "includesAllOf", "includesOneOf"].includes(condition.operator);
const allowMultiSelect = [
"equalsOneOf",
"includesAllOf",
"includesOneOf",
"doesNotIncludeOneOf",
"doesNotIncludeAllOf",
].includes(condition.operator);
return (
<div key={condition.id} className="flex items-center gap-x-2">
<div className="w-10 shrink-0">

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";

View File

@@ -8,7 +8,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { type JSX, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";

View File

@@ -4,6 +4,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {

View File

@@ -3,6 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { cn } from "@formbricks/lib/cn";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";

View File

@@ -1,6 +1,6 @@
import { PaintbrushIcon, Rows3Icon, SettingsIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { type JSX, useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyEditorTabs } from "@formbricks/types/surveys/types";

View File

@@ -7,7 +7,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { type JSX, useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";

View File

@@ -130,21 +130,15 @@ export const ResponseOptionsCard = ({
};
const handleRunOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
date?.setDate(equivalentDate);
setRunOnDate(date);
setLocalSurvey({ ...localSurvey, runOnDate: date ?? null });
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setRunOnDate(utcDate);
setLocalSurvey({ ...localSurvey, runOnDate: utcDate ?? null });
};
const handleCloseOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
date?.setDate(equivalentDate);
setCloseOnDate(date);
setLocalSurvey({ ...localSurvey, closeOnDate: date ?? null });
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setCloseOnDate(utcDate);
setLocalSurvey({ ...localSurvey, closeOnDate: utcDate ?? null });
};
const handleClosedSurveyMessageChange = ({

View File

@@ -2,12 +2,10 @@ import { RotateCcwIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
import { UseFormReturn, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TBaseStyling } from "@formbricks/types/styling";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
import { AlertDialog } from "@formbricks/ui/components/AlertDialog";
import { Button } from "@formbricks/ui/components/Button";
@@ -53,50 +51,8 @@ export const StylingView = ({
}: StylingViewProps) => {
const t = useTranslations();
const stylingDefaults: TBaseStyling = useMemo(() => {
let stylingDefaults: TBaseStyling;
const isOverwriteEnabled = localSurvey.styling?.overwriteThemeStyling ?? false;
if (isOverwriteEnabled) {
const { overwriteThemeStyling, ...baseSurveyStyles } = localSurvey.styling ?? {};
stylingDefaults = baseSurveyStyles;
} else {
const { allowStyleOverwrite, ...baseProductStyles } = product.styling ?? {};
stylingDefaults = baseProductStyles;
}
return {
brandColor: { light: stylingDefaults.brandColor?.light ?? COLOR_DEFAULTS.brandColor },
questionColor: { light: stylingDefaults.questionColor?.light ?? COLOR_DEFAULTS.questionColor },
inputColor: { light: stylingDefaults.inputColor?.light ?? COLOR_DEFAULTS.inputColor },
inputBorderColor: { light: stylingDefaults.inputBorderColor?.light ?? COLOR_DEFAULTS.inputBorderColor },
cardBackgroundColor: {
light: stylingDefaults.cardBackgroundColor?.light ?? COLOR_DEFAULTS.cardBackgroundColor,
},
cardBorderColor: { light: stylingDefaults.cardBorderColor?.light ?? COLOR_DEFAULTS.cardBorderColor },
cardShadowColor: { light: stylingDefaults.cardShadowColor?.light ?? COLOR_DEFAULTS.cardShadowColor },
highlightBorderColor: stylingDefaults.highlightBorderColor?.light
? {
light: stylingDefaults.highlightBorderColor.light,
}
: undefined,
isDarkModeEnabled: stylingDefaults.isDarkModeEnabled ?? false,
roundness: stylingDefaults.roundness ?? 8,
cardArrangement: stylingDefaults.cardArrangement ?? {
linkSurveys: "simple",
appSurveys: "simple",
},
background: stylingDefaults.background,
hideProgressBar: stylingDefaults.hideProgressBar ?? false,
isLogoHidden: stylingDefaults.isLogoHidden ?? false,
};
}, [localSurvey.styling, product.styling]);
const form = useForm<TSurveyStyling>({
defaultValues: {
...localSurvey.styling,
...stylingDefaults,
},
defaultValues: localSurvey.styling ?? product.styling,
});
const overwriteThemeStyling = form.watch("overwriteThemeStyling");
@@ -133,20 +89,17 @@ export const StylingView = ({
}
}, [overwriteThemeStyling]);
const watchedValues = useWatch({
control: form.control,
});
useEffect(() => {
// @ts-expect-error
setLocalSurvey((prev) => ({
...prev,
styling: {
...prev.styling,
...watchedValues,
},
}));
}, [watchedValues, setLocalSurvey]);
form.watch((data: TSurveyStyling) => {
setLocalSurvey((prev) => ({
...prev,
styling: {
...prev.styling,
...data,
},
}));
});
}, [setLocalSurvey]);
const defaultProductStyling = useMemo(() => {
const { styling: productStyling } = product;

View File

@@ -344,9 +344,7 @@ export const SurveyMenuBar = ({
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
</TooltipTrigger>
<TooltipContent side={"top"} className="lg:hidden">
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">
{t(cautionText)}
</p>
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -116,6 +116,14 @@ export const logicRules = {
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "environments.surveys.edit.does_not_include_one_of",
value: ZSurveyLogicConditionsOperator.Enum.doesNotIncludeOneOf,
},
{
label: "environments.surveys.edit.does_not_include_all_of",
value: ZSurveyLogicConditionsOperator.Enum.doesNotIncludeAllOf,
},
{
label: "environments.surveys.edit.includes_all_of",
value: ZSurveyLogicConditionsOperator.Enum.includesAllOf,
@@ -144,6 +152,14 @@ export const logicRules = {
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "environments.surveys.edit.does_not_include_one_of",
value: ZSurveyLogicConditionsOperator.Enum.doesNotIncludeOneOf,
},
{
label: "environments.surveys.edit.does_not_include_all_of",
value: ZSurveyLogicConditionsOperator.Enum.doesNotIncludeAllOf,
},
{
label: "environments.surveys.edit.includes_all_of",
value: ZSurveyLogicConditionsOperator.Enum.includesAllOf,

View File

@@ -1060,7 +1060,9 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues
};
return survey.questions.findIndex(
(question) => question.logic && question.id !== questionId && question.logic.some(isUsedInLogicRule)
(question) =>
question.logicFallback === questionId ||
(question.id !== questionId && question.logic?.some(isUsedInLogicRule))
);
};
@@ -1145,3 +1147,17 @@ export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: strin
return survey.questions.findIndex((question) => question.logic?.some(isUsedInLogicRule));
};
export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string): number => {
const isUsedInAction = (action: TSurveyLogicAction): boolean => {
return action.objective === "jumpToQuestion" && action.target === endingCardId;
};
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
return logicRule.actions.some(isUsedInAction);
};
return survey.questions.findIndex(
(question) => question.logicFallback === endingCardId || question.logic?.some(isUsedInLogicRule)
);
};

View File

@@ -1,8 +1,11 @@
import {
getAdvancedTargetingPermission,
getMultiLanguagePermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
@@ -24,14 +27,17 @@ import { getUserLocale } from "@formbricks/lib/user/service";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { SurveyEditor } from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
export const generateMetadata = async (props) => {
const params = await props.params;
const survey = await getSurvey(params.surveyId);
return {
title: survey?.name ? `${survey?.name} | Editor` : "Editor",
};
};
const Page = async ({ params, searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const [
survey,
@@ -48,7 +54,7 @@ const Page = async ({ params, searchParams }) => {
getProductByEnvironmentId(params.environmentId),
getEnvironment(params.environmentId),
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getResponseCountBySurveyId(params.surveyId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),

View File

@@ -14,17 +14,19 @@ import { TTemplateRole } from "@formbricks/types/templates";
import { TemplateContainerWithPreview } from "./components/TemplateContainer";
interface SurveyTemplateProps {
params: {
params: Promise<{
environmentId: string;
};
searchParams: {
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
role?: TTemplateRole;
};
}>;
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const Page = async (props: SurveyTemplateProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
const environmentId = params.environmentId;

View File

@@ -3,7 +3,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
export const dynamic = "force-dynamic";
const Page = ({ searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const { environmentId } = searchParams;
return (

View File

@@ -20,13 +20,17 @@ export const getSegmentsByAttributeClassAction = authenticatedActionClient
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "read",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
const segments = await getSegmentsByAttributeClassName(
parsedInput.environmentId,
parsedInput.attributeClass.name

View File

@@ -4,6 +4,7 @@ import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TUserLocale } from "@formbricks/types/user";
import { Label } from "@formbricks/ui/components/Label";
import { Switch } from "@formbricks/ui/components/Switch";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
@@ -49,8 +50,15 @@ export const AttributeClassesTable = ({
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="flex items-center text-sm font-medium">
{t("environments.attributes.show_archived")}
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
<Label htmlFor="showArchivedToggle" className="cursor-pointer">
{t("environments.attributes.show_archived")}
</Label>
<Switch
id="showArchivedToggle"
className="mx-3"
checked={showArchived}
onCheckedChange={toggleShowArchived}
/>
</div>
</div>
)}

View File

@@ -21,7 +21,8 @@ export const metadata: Metadata = {
title: "Attributes",
};
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
let attributeClasses = await getAttributeClasses(params.environmentId);
const t = await getTranslations();
const product = await getProductByEnvironmentId(params.environmentId);

View File

@@ -9,7 +9,11 @@ import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/ser
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { AuthorizationError } from "@formbricks/types/errors";
const ConfigLayout = async ({ children, params }) => {
const ConfigLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),

View File

@@ -54,7 +54,7 @@ export const ResponseSection = async ({
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
return (
<ResponseTimeline

View File

@@ -19,7 +19,8 @@ import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
await Promise.all([

View File

@@ -16,7 +16,8 @@ import { Button } from "@formbricks/ui/components/Button";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);

View File

@@ -2,11 +2,11 @@ import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environment
import { BasicCreateSegmentModal } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal";
import { SegmentTable } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/SegmentTable";
import { CreateSegmentModal } from "@/modules/ee/advanced-targeting/components/create-segment-modal";
import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
@@ -19,12 +19,13 @@ import { getSegments } from "@formbricks/lib/segment/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, segments, attributeClasses, organization, product] = await Promise.all([
getEnvironment(params.environmentId),
getSegments(params.environmentId),
getAttributeClasses(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);

View File

@@ -2,8 +2,8 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { z } from "zod";
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
import { createMembership } from "@formbricks/lib/membership/service";
import { createOrganization } from "@formbricks/lib/organization/service";
import { createProduct } from "@formbricks/lib/product/service";

View File

@@ -1,12 +1,16 @@
"use client";
import { createActionClassAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TActionClass } from "@formbricks/types/action-classes";
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { Button } from "@formbricks/ui/components/Button";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { Label } from "@formbricks/ui/components/Label";
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
@@ -15,15 +19,25 @@ import { getActiveInactiveSurveysAction } from "../actions";
interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
environment: TEnvironment;
otherEnvActionClasses: TActionClass[];
otherEnvironment: TEnvironment;
isReadOnly: boolean;
}
export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabProps) => {
export const ActionActivityTab = ({
actionClass,
otherEnvActionClasses,
otherEnvironment,
environmentId,
environment,
isReadOnly,
}: ActivityTabProps) => {
const t = useTranslations();
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
@@ -32,7 +46,6 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
const getActiveInactiveSurveysResponse = await getActiveInactiveSurveysAction({
actionClassId: actionClass.id,
});
console.log(getActiveInactiveSurveysResponse, "randike");
if (getActiveInactiveSurveysResponse?.data) {
setActiveSurveys(getActiveInactiveSurveysResponse.data.activeSurveys);
setInactiveSurveys(getActiveInactiveSurveysResponse.data.inactiveSurveys);
@@ -46,6 +59,57 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
updateState();
}, [actionClass.id, environmentId]);
const actionClassNames = useMemo(
() => otherEnvActionClasses.map((actionClass) => actionClass.name),
[otherEnvActionClasses]
);
const actionClassKeys = useMemo(() => {
const codeActionClasses: TActionClassInputCode[] = otherEnvActionClasses.filter(
(actionClass) => actionClass.type === "code"
) as TActionClassInputCode[];
return codeActionClasses.map((actionClass) => actionClass.key);
}, [otherEnvActionClasses]);
const copyAction = async (data: TActionClassInput) => {
const { type } = data;
let copyName = data.name;
try {
if (isReadOnly) {
throw new Error(t("common.you_are_not_authorised_to_perform_this_action"));
}
if (copyName && actionClassNames.includes(copyName)) {
while (actionClassNames.includes(copyName)) {
copyName += " (copy)";
}
}
if (type === "code" && data.key && actionClassKeys.includes(data.key)) {
throw new Error(t("environments.actions.action_with_key_already_exists", { key: data.key }));
}
let updatedAction = {
...data,
name: copyName.trim(),
environmentId: otherEnvironment.id,
};
const createActionClassResponse = await createActionClassAction({
action: updatedAction as TActionClassInput,
});
if (!createActionClassResponse?.data) {
throw new Error(t("environments.actions.action_copy_failed", {}));
}
toast.success(t("environments.actions.action_copied_successfully"));
} catch (e: any) {
toast.error(e.message);
}
};
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -99,6 +163,22 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
</div>
</div>
<div className="">
<Label className="text-xs font-normal text-slate-500">Environment</Label>
<div className="items-center-center flex gap-2">
<p className="text-xs text-slate-700">
{environment.type === "development" ? "Development" : "Production"}
</p>
<Button
onClick={() => {
copyAction(actionClass);
}}
className="m-0 p-0 text-xs font-medium text-black underline underline-offset-4 focus:ring-0 focus:ring-offset-0"
variant="minimal">
{environment.type === "development" ? "Copy to Production" : "Copy to Development"}
</Button>
</div>
</div>
</div>
</div>
);

View File

@@ -1,21 +1,28 @@
"use client";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ActionDetailModal } from "./ActionDetailModal";
interface ActionClassesTableProps {
environmentId: string;
actionClasses: TActionClass[];
environment: TEnvironment;
children: [JSX.Element, JSX.Element[]];
isReadOnly: boolean;
otherEnvironment: TEnvironment;
otherEnvActionClasses: TActionClass[];
}
export const ActionClassesTable = ({
environmentId,
actionClasses,
environment,
children: [TableHeading, actionRows],
isReadOnly,
otherEnvActionClasses,
otherEnvironment,
}: ActionClassesTableProps) => {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
@@ -48,11 +55,14 @@ export const ActionClassesTable = ({
{activeActionClass && (
<ActionDetailModal
environmentId={environmentId}
environment={environment}
open={isActionDetailModalOpen}
setOpen={setActionDetailModalOpen}
actionClasses={actionClasses}
actionClass={activeActionClass}
isReadOnly={isReadOnly}
otherEnvActionClasses={otherEnvActionClasses}
otherEnvironment={otherEnvironment}
/>
)}
</>

View File

@@ -1,17 +1,21 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
import { ActionActivityTab } from "./ActionActivityTab";
import { ActionSettingsTab } from "./ActionSettingsTab";
interface ActionDetailModalProps {
environmentId: string;
environment: TEnvironment;
open: boolean;
setOpen: (v: boolean) => void;
actionClass: TActionClass;
actionClasses: TActionClass[];
isReadOnly: boolean;
otherEnvironment: TEnvironment;
otherEnvActionClasses: TActionClass[];
}
export const ActionDetailModal = ({
@@ -20,13 +24,25 @@ export const ActionDetailModal = ({
setOpen,
actionClass,
actionClasses,
environment,
isReadOnly,
otherEnvActionClasses,
otherEnvironment,
}: ActionDetailModalProps) => {
const t = useTranslations();
const tabs = [
{
title: t("common.activity"),
children: <ActionActivityTab actionClass={actionClass} environmentId={environmentId} />,
children: (
<ActionActivityTab
otherEnvActionClasses={otherEnvActionClasses}
otherEnvironment={otherEnvironment}
isReadOnly={isReadOnly}
environment={environment}
actionClass={actionClass}
environmentId={environmentId}
/>
),
},
{
title: t("common.settings"),

View File

@@ -10,6 +10,7 @@ import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
@@ -22,7 +23,8 @@ export const metadata: Metadata = {
title: "Actions",
};
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
const [actionClasses, organization, product] = await Promise.all([
@@ -30,7 +32,7 @@ const Page = async ({ params }) => {
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
if (!session) {
throw new Error(t("common.session_not_found"));
@@ -44,6 +46,17 @@ const Page = async ({ params }) => {
throw new Error(t("common.product_not_found"));
}
const environments = await getEnvironments(product.id);
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
if (!currentEnvironment) {
throw new Error(t("common.environment_not_found"));
}
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
@@ -69,6 +82,9 @@ const Page = async ({ params }) => {
<PageContentWrapper>
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
<ActionClassesTable
environment={currentEnvironment}
otherEnvironment={otherEnvironment}
otherEnvActionClasses={otherEnvActionClasses}
environmentId={params.environmentId}
actionClasses={actionClasses}
isReadOnly={isReadOnly}>

View File

@@ -1,10 +1,10 @@
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
import { getIsAIEnabled } from "@/app/lib/utils";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import type { Session } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -59,15 +59,13 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const membershipRole = currentUserMembership?.role;
const { isOwner, isManager } = getAccessFlags(membershipRole);
const isOwnerOrManager = isOwner || isManager;
const { isMember } = getAccessFlags(membershipRole);
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
const productPermission = await getProductPermissionByUserId(session.user.id, environment.productId);
if (!isOwnerOrManager && !productPermission) {
if (isMember && !productPermission) {
throw new Error(t("common.product_permission_not_found"));
}

View File

@@ -222,7 +222,7 @@ export const MainNavigation = ({
{
label: t("common.billing"),
href: `/environments/${environment.id}/settings/billing`,
hidden: !isFormbricksCloud || isPricingDisabled,
hidden: !isFormbricksCloud,
icon: CreditCardIcon,
},
{
@@ -481,7 +481,11 @@ export const MainNavigation = ({
{dropdownNavigation.map(
(link) =>
!link.hidden && (
<Link href={link.href} target={link.target} className="flex w-full items-center">
<Link
href={link.href}
target={link.target}
className="flex w-full items-center"
key={link.label}>
<DropdownMenuItem>
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
{link.label}

View File

@@ -21,7 +21,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!AIRTABLE_CLIENT_ID;
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
@@ -53,7 +54,7 @@ const Page = async ({ params }) => {
airtableArray = await getAirtableTables(params.environmentId);
}
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,

View File

@@ -24,7 +24,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
@@ -51,7 +52,7 @@ const Page = async ({ params }) => {
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
);
const locale = findMatchingLocale();
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,

View File

@@ -26,7 +26,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const enabled = !!(
NOTION_OAUTH_CLIENT_ID &&
@@ -80,7 +81,7 @@ const Page = async ({ params }) => {
return (
<PageContentWrapper>
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
<PageHeader pageTitle={"environments.integrations.notion.notion_integration"} />
<PageHeader pageTitle={t("environments.integrations.notion.notion_integration")} />
<NotionWrapper
enabled={enabled}
surveys={surveys}

View File

@@ -25,7 +25,8 @@ import { Card } from "@formbricks/ui/components/IntegrationCard";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const environmentId = params.environmentId;
const t = await getTranslations();
const [

View File

@@ -91,7 +91,7 @@ export const ManageIntegration = ({
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">
{t("environments.integrations.slack.connected_with_team", {
team: slackIntegration.config.key.team.name,
team: slackIntegration.config.key.team?.name,
})}
</span>
</div>

View File

@@ -46,7 +46,8 @@ export const SlackWrapper = ({
if (
getSlackChannelsResponse?.serverError &&
getSlackChannelsResponse.serverError.includes("missing_scope")
(getSlackChannelsResponse.serverError.includes("missing_scope") ||
getSlackChannelsResponse.serverError.includes("invalid_auth"))
) {
setShowReconnectButton(true);
}

View File

@@ -19,7 +19,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
const t = await getTranslations();
const [session, surveys, slackIntegration, environment, attributeClasses] = await Promise.all([

View File

@@ -2,7 +2,7 @@
import { WebhookModal } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { type JSX, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TWebhook } from "@formbricks/types/webhooks";

View File

@@ -18,7 +18,8 @@ import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [session, organization, webhooksUnsorted, surveys, environment] = await Promise.all([
getServerSession(authOptions),

View File

@@ -15,7 +15,11 @@ import { FormbricksClient } from "../../components/FormbricksClient";
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
import { PosthogIdentify } from "./components/PosthogIdentify";
export const EnvLayout = async ({ children, params }) => {
export const EnvLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {

View File

@@ -7,7 +7,8 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
const organization = await getOrganizationByEnvironmentId(params.environmentId);

View File

@@ -2,8 +2,11 @@ import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/
import { EnvironmentIdField } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/EnvironmentIdField";
import { SetupInstructions } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getTranslations } from "next-intl/server";
import { getMultiLanguagePermission, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
@@ -12,7 +15,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SettingsCard } from "../../../settings/components/SettingsCard";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, organization] = await Promise.all([
getEnvironment(params.environmentId),

View File

@@ -19,7 +19,7 @@ const LoadingCard = () => {
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-4 sm:col-span-2">{t("common.label")}</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">
{t("environments.product.api_keys.api_key")}
{t("environments.product.api-keys.api_key")}
</div>
<div className="col-span-4 sm:col-span-2">{t("common.created_at")}</div>
</div>

View File

@@ -1,9 +1,12 @@
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getMultiLanguagePermission, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -17,7 +20,8 @@ import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SettingsCard } from "../../settings/components/SettingsCard";
import { ApiKeyList } from "./components/ApiKeyList";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [session, environment, organization, product] = await Promise.all([
getServerSession(authOptions),

View File

@@ -1,10 +1,13 @@
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import packageJson from "@/package.json";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getMultiLanguagePermission, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -19,7 +22,8 @@ import { DeleteProduct } from "./components/DeleteProduct";
import { EditProductNameForm } from "./components/EditProductNameForm";
import { EditWaitingTimeForm } from "./components/EditWaitingTimeForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const [product, session, organization] = await Promise.all([
getProductByEnvironmentId(params.environmentId),

View File

@@ -1,12 +1,15 @@
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { EditLanguage } from "@/modules/ee/multi-language-surveys/components/edit-language";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { getMultiLanguagePermission, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -16,7 +19,8 @@ import { getUser } from "@formbricks/lib/user/service";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const product = await getProductByEnvironmentId(params.environmentId);

View File

@@ -12,7 +12,11 @@ export const metadata: Metadata = {
title: "Config",
};
const ConfigLayout = async ({ children, params }) => {
const ConfigLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, session] = await Promise.all([

View File

@@ -72,8 +72,8 @@ export const ThemeStyling = ({
isDarkModeEnabled: product.styling.isDarkModeEnabled ?? false,
roundness: product.styling.roundness ?? 8,
cardArrangement: product.styling.cardArrangement ?? {
linkSurveys: "simple",
appSurveys: "simple",
linkSurveys: "straight",
appSurveys: "straight",
},
background: product.styling.background,
hideProgressBar: product.styling.hideProgressBar ?? false,
@@ -119,8 +119,8 @@ export const ThemeStyling = ({
},
roundness: 8,
cardArrangement: {
linkSurveys: "simple",
appSurveys: "simple",
linkSurveys: "straight",
appSurveys: "straight",
},
};

View File

@@ -28,9 +28,9 @@ const Loading = () => {
<ProductConfigNavigation activeId="look" loading />
</PageHeader>
<SettingsCard
title="environments.product.look.theme"
title={t("environments.product.look.theme")}
className="max-w-7xl"
description="environments.product.look.theme_settings_description">
description={t("environments.product.look.theme_settings_description")}>
<div className="flex animate-pulse">
<div className="w-1/2">
<div className="flex flex-col gap-4 pr-6">

View File

@@ -1,15 +1,15 @@
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { EditLogo } from "@/app/(app)/environments/[environmentId]/product/look/components/EditLogo";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import {
getMultiLanguagePermission,
getRemoveInAppBrandingPermission,
getRemoveLinkBrandingPermission,
getRoleManagementPermission,
} from "@formbricks/ee/lib/service";
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { authOptions } from "@formbricks/lib/authOptions";
import { cn } from "@formbricks/lib/cn";
import { DEFAULT_LOCALE, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
@@ -26,7 +26,8 @@ import { EditFormbricksBranding } from "./components/EditBranding";
import { EditPlacementForm } from "./components/EditPlacementForm";
import { ThemeStyling } from "./components/ThemeStyling";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const [session, organization, product] = await Promise.all([
getServerSession(authOptions),

View File

@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
const Page = ({ params }) => {
const Page = async (props) => {
const params = await props.params;
return redirect(`/environments/${params.environmentId}/product/general`);
};

View File

@@ -1,10 +1,13 @@
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getMultiLanguagePermission, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -17,7 +20,8 @@ import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { EditTagsWrapper } from "./components/EditTagsWrapper";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);
if (!environment) {

View File

@@ -4,7 +4,11 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
const AccountSettingsLayout = async ({ children, params }) => {
const AccountSettingsLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),

View File

@@ -141,7 +141,9 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
return memberships;
};
const Page = async ({ params, searchParams }) => {
const Page = async (props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {

View File

@@ -15,7 +15,8 @@ import { DeleteAccount } from "./components/DeleteAccount";
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
import { EditProfileDetailsForm } from "./components/EditProfileDetailsForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const { environmentId } = params;
const session = await getServerSession(authOptions);

View File

@@ -40,7 +40,7 @@ export const OrganizationSettingsNavbar = ({
id: "billing",
label: t("common.billing"),
href: `/environments/${environmentId}/settings/billing`,
hidden: !isFormbricksCloud || isPricingDisabled || loading,
hidden: !isFormbricksCloud || loading,
current: pathname?.includes("/billing"),
},
{

View File

@@ -1,9 +1,9 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { getEnterpriseLicense, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { CheckIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { getEnterpriseLicense, getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -13,7 +13,8 @@ import { Button } from "@formbricks/ui/components/Button";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Page = async ({ params }) => {
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
if (IS_FORMBRICKS_CLOUD) {
notFound();

View File

@@ -7,10 +7,10 @@ import {
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromInviteId } from "@/lib/utils/helper";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { sendInviteMemberEmail } from "@/modules/email";
import { OrganizationRole } from "@prisma/client";
import { z } from "zod";
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { deleteInvite, getInvite, inviteUser, resendInvite } from "@formbricks/lib/invite/service";
import { createInviteToken } from "@formbricks/lib/jwt";

View File

@@ -1,7 +1,7 @@
import { MembersInfo } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditMemberships/MembersInfo";
import { getMembersByOrganizationId } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/lib/membership";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { getTranslations } from "next-intl/server";
import { getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getInvitesByOrganizationId } from "@formbricks/lib/invite/service";
import { TMembership } from "@formbricks/types/memberships";

View File

@@ -1,10 +1,13 @@
"use client";
import { AddMemberRole } from "@/modules/ee/role-management/components/add-member-role";
import { zodResolver } from "@hookform/resolvers/zod";
import { OrganizationRole } from "@prisma/client";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { z } from "zod";
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
import { ZUserName } from "@formbricks/types/user";
import { Alert, AlertDescription } from "@formbricks/ui/components/Alert";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
@@ -18,6 +21,7 @@ interface IndividualInviteTabProps {
isFormbricksCloud: boolean;
environmentId: string;
}
export const IndividualInviteTab = ({
setOpen,
onSubmit,
@@ -25,6 +29,13 @@ export const IndividualInviteTab = ({
isFormbricksCloud,
environmentId,
}: IndividualInviteTabProps) => {
const ZFormSchema = z.object({
name: ZUserName,
email: z.string().email("Invalid email address"),
role: ZOrganizationRole,
});
type TFormData = z.infer<typeof ZFormSchema>;
const t = useTranslations();
const {
register,
@@ -33,17 +44,18 @@ export const IndividualInviteTab = ({
reset,
control,
watch,
formState: { isSubmitting },
} = useForm<{
name: string;
email: string;
role: TOrganizationRole;
}>();
formState: { isSubmitting, errors },
} = useForm<TFormData>({
resolver: zodResolver(ZFormSchema),
defaultValues: {
role: "owner",
},
});
const submitEventClass = async () => {
const data = getValues();
data.role = data.role || OrganizationRole.owner;
await onSubmit([data]);
onSubmit([data]);
setOpen(false);
reset();
};
@@ -55,9 +67,10 @@ export const IndividualInviteTab = ({
<Label htmlFor="memberNameInput">{t("common.full_name")}</Label>
<Input
id="memberNameInput"
placeholder="e.g. Hans Wurst"
placeholder="Hans Wurst"
{...register("name", { required: true, validate: (value) => value.trim() !== "" })}
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div>
<Label htmlFor="memberEmailInput">{t("common.email")}</Label>

Some files were not shown because too many files have changed in this diff Show More