mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 19:30:48 -05:00
Compare commits
246 Commits
api/test-depr-env
...
llm
| Author | SHA1 | Date | |
|---|---|---|---|
| c5d629ef25 | |||
| 94c9e8fcf1 | |||
| 49a8c8c686 | |||
| 2832831db1 | |||
| b5e6567194 | |||
| 86d3f2fae1 | |||
| 62d09f6a8f | |||
| 74dd778630 | |||
| 7ac99c0840 | |||
| dde0f8d32c | |||
| bcd3c91075 | |||
| f376c620ab | |||
| 4865a78338 | |||
| a7c8e1acf9 | |||
| e5a097e56e | |||
| 1ddde9cac7 | |||
| 59f5cdfb4b | |||
| 8431eaf9f6 | |||
| f228e8e06a | |||
| 5e6ab81cb1 | |||
| 1417a5a654 | |||
| f8ae92b3be | |||
| 1bc3f79f30 | |||
| 7151dd5234 | |||
| 086315ce33 | |||
| e01b4311ca | |||
| dd757394af | |||
| 507f80f9b0 | |||
| 8562232280 | |||
| 1234e6685a | |||
| 40a5e8ea6a | |||
| 319a76a70d | |||
| 2abf8e1d8c | |||
| a985dc698b | |||
| 7b59a6300e | |||
| bf8b4079fd | |||
| 5704bfbc03 | |||
| 0920ccf2c3 | |||
| db0c9e7c55 | |||
| ef87d899b9 | |||
| ea92ef9fce | |||
| 778fc2acf1 | |||
| 2ffef36c89 | |||
| 1d6bda74df | |||
| 12ff0b7c0e | |||
| fa1079bac1 | |||
| 1403f0bb01 | |||
| c79553633f | |||
| f16fb3b62f | |||
| 7dfc7f4825 | |||
| 1ecc9f1722 | |||
| 7d1c02b54b | |||
| f2c452d7f9 | |||
| afcfbb7a3a | |||
| 7f8c9dcbb8 | |||
| 3998e4da31 | |||
| 48086faffc | |||
| 38a0d7c810 | |||
| b17bb88daa | |||
| f59e9f13ec | |||
| 5169dec510 | |||
| 0df16f6f0c | |||
| 8442dedf9c | |||
| 22c27c5ebb | |||
| 6638dceb04 | |||
| 8558121e46 | |||
| f1279d51e5 | |||
| 926706be9d | |||
| 85b456e619 | |||
| 3bac488a29 | |||
| fbe2a31133 | |||
| 79d618f77c | |||
| 89eb04f813 | |||
| 8a2b349329 | |||
| a862b739f7 | |||
| 4e5df85538 | |||
| 727b349086 | |||
| f75db6b1d0 | |||
| 7ffca53577 | |||
| 25614b23fc | |||
| 016e14d0f1 | |||
| be80db8418 | |||
| bcc3789ce8 | |||
| 5e76ebdfc1 | |||
| 150f256721 | |||
| da7971328c | |||
| a6cd56b196 | |||
| 7c81cf119e | |||
| 8d29b24352 | |||
| a1ae849496 | |||
| 4d0a686e89 | |||
| 364915e4c8 | |||
| ada2518d0c | |||
| 57d1c0ed99 | |||
| 817b299436 | |||
| c140dae872 | |||
| 6036a8c767 | |||
| bf592937f4 | |||
| 1cfadd968a | |||
| 21ed383a46 | |||
| 7ed7101ac1 | |||
| 7aa12a4f0c | |||
| 2e926936fb | |||
| 8edef8aede | |||
| 54fb202285 | |||
| c720a462a7 | |||
| a386451e6e | |||
| f0bf111e7b | |||
| 8a57a5b74b | |||
| 434cb1d0d0 | |||
| 8bde75a9ff | |||
| 6b880f29cb | |||
| 969c9834e5 | |||
| 5e33b7c9a4 | |||
| 230ea744fa | |||
| fae1fb8f96 | |||
| eac35daed9 | |||
| 45accc1edb | |||
| 02ebe8e9f8 | |||
| cae859e326 | |||
| 5352d563b6 | |||
| 711f2bfe67 | |||
| 6fcb5d39a2 | |||
| 1ed9859ee7 | |||
| cd72a0a78d | |||
| 2b09795787 | |||
| 2451acb9bd | |||
| 14dcded91b | |||
| 46062f91cd | |||
| ffd4478184 | |||
| 69da1862fa | |||
| c11d3241ab | |||
| 3fb09a1a26 | |||
| 6efa449c10 | |||
| 34b94689ca | |||
| 901fac7e08 | |||
| 739c662863 | |||
| 535974ff8a | |||
| a8b97abe9a | |||
| 28103604b4 | |||
| b5a7e15386 | |||
| fec4746d5d | |||
| 175323e7d9 | |||
| 6130737d51 | |||
| bf10a8d0b2 | |||
| 612f8dceb8 | |||
| 0303f16db4 | |||
| 07635b160e | |||
| 9cfcffdb5e | |||
| 02264ffc5f | |||
| e489c6a346 | |||
| 7dde3edd8d | |||
| 730ab6a609 | |||
| 4304a7efd6 | |||
| a89d598f8d | |||
| 6ff5af712f | |||
| 398ba79e7e | |||
| 4e75a57692 | |||
| 5127de9de0 | |||
| 2bf7788a1b | |||
| ee8122778b | |||
| 8aaa7ed9c0 | |||
| bc7c8c5715 | |||
| ab1ea7a5ce | |||
| 4f749355e0 | |||
| 18b60ddd35 | |||
| 87f1b01c7a | |||
| 851ea0deb2 | |||
| 9abbbfdd35 | |||
| cefc2bdf60 | |||
| 78473bf3d0 | |||
| 15403c6a92 | |||
| 35b98863a4 | |||
| 65f5968fb1 | |||
| 2dfea4d72f | |||
| 990c0eee31 | |||
| 07f16b8a43 | |||
| bf556b0608 | |||
| 8b0766a46e | |||
| 1f995d6e25 | |||
| 975a4d57f8 | |||
| 69bd576fc5 | |||
| a2e4a3bbd7 | |||
| 281f854332 | |||
| 24496774a5 | |||
| aeaf3215b4 | |||
| f4c5162590 | |||
| dedb7389f0 | |||
| ff77118932 | |||
| 7aed1b84de | |||
| 79a773432a | |||
| d53869f1df | |||
| fc9ddb2b0d | |||
| 6fcb6863bd | |||
| b1cee91ad9 | |||
| 9d2e988c59 | |||
| 60bd5cbeff | |||
| b6a3a15379 | |||
| c68f214eff | |||
| c90ee84483 | |||
| dc1ee72594 | |||
| 924132287e | |||
| e6f347aa07 | |||
| 367bc23dd4 | |||
| a1a11b2bb8 | |||
| 31d2ea7444 | |||
| 0653c6a59f | |||
| b6d793e109 | |||
| d361c334d3 | |||
| a4d808b479 | |||
| 18ae1748d3 | |||
| 3404e0c494 | |||
| 83499ae552 | |||
| 2ac0c1eb07 | |||
| 54ede3015e | |||
| 1b4f05a062 | |||
| 197dbf5aa6 | |||
| 7ca52a7a93 | |||
| 4a48839d17 | |||
| 92bd9bdac7 | |||
| ad4b6f8b8c | |||
| 8de5079db3 | |||
| a60206dd44 | |||
| d66abdcdaf | |||
| 03fa41a911 | |||
| cab438e474 | |||
| a6dfe78c81 | |||
| e4d96f4379 | |||
| 581a66b4a9 | |||
| 5cf0c15812 | |||
| ebaa2d363c | |||
| 597ea40b75 | |||
| 3c39dcc2de | |||
| e8df1dbb35 | |||
| 84987ce557 | |||
| 784ed855d7 | |||
| 5a17d4144d | |||
| 65c9db86c6 | |||
| bc94d34d1e | |||
| 22be60a0ba | |||
| a384963863 | |||
| c067ae73bb | |||
| dc78a30cbe | |||
| 9c9ae8a3a2 | |||
| 29a08151aa | |||
| f42a8822a9 |
@@ -0,0 +1 @@
|
||||
{"sessionId":"f77248e2-8840-41c6-968b-c3b7d8a9e913","pid":49125,"acquiredAt":1776168010367}
|
||||
+49
-8
@@ -32,6 +32,24 @@ CRON_SECRET=
|
||||
# Set the minimum log level(debug, info, warn, error, fatal)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# BullMQ workers require REDIS_URL (for example `redis://localhost:6379`) to be set.
|
||||
# BullMQ worker startup is enabled by default outside tests. Set to 0 to disable.
|
||||
# BULLMQ_WORKER_ENABLED=1
|
||||
# Set to 1 on web/API pods that only enqueue jobs while a separate BullMQ worker deployment consumes them.
|
||||
# BULLMQ_EXTERNAL_WORKER_ENABLED=0
|
||||
|
||||
# Number of BullMQ worker instances started per Formbricks server process.
|
||||
# BULLMQ_WORKER_COUNT=1
|
||||
|
||||
# Number of concurrent jobs each BullMQ worker can process.
|
||||
# BULLMQ_WORKER_CONCURRENCY=1
|
||||
|
||||
# Survey publish/close scheduling is configured with public build-time env vars because the editor UI
|
||||
# also needs to render the selected execution time and timezone.
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_TIME_ZONE=Europe/Berlin
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_HOUR=0
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_MINUTE=0
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
@@ -46,6 +64,21 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
HUB_API_KEY=dev-api-key
|
||||
HUB_API_URL=http://localhost:8080
|
||||
HUB_DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
# Hub image tag used by docker-compose.dev.yml (hub + hub-migrate). Leave unset to use the
|
||||
# pinned default in the compose file; override here when testing a specific Hub release.
|
||||
# HUB_IMAGE_TAG=0.2.0
|
||||
|
||||
###########################
|
||||
# CUBE ANALYTICS (XM V5) #
|
||||
###########################
|
||||
# XM Suite v5 analysis features require Cube.js. The optional xm dev profile exposes Cube on port 4000.
|
||||
# Uncomment COMPOSE_PROFILES=xm to run the optional Cube analytics service.
|
||||
# COMPOSE_PROFILES=xm
|
||||
CUBEJS_API_URL=http://localhost:4000
|
||||
# Generate with: openssl rand -hex 32. `pnpm dev:setup` will create/preserve this automatically.
|
||||
CUBEJS_API_SECRET=
|
||||
CUBEJS_JWT_ISSUER=formbricks-web
|
||||
CUBEJS_JWT_AUDIENCE=formbricks-cube
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
@@ -149,16 +182,17 @@ AZUREAD_TENANT_ID=
|
||||
|
||||
# Configure Formbricks AI at the instance level
|
||||
# Set the provider used for AI features on this instance.
|
||||
# Accepted values for AI_PROVIDER: aws, gcp, azure
|
||||
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching credentials below.
|
||||
# AI_PROVIDER=gcp
|
||||
# Accepted values for AI_PROVIDER: aws, google, azure
|
||||
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching provider settings below.
|
||||
# AI_PROVIDER=google
|
||||
# AI_MODEL=gemini-2.5-flash
|
||||
|
||||
# Google Vertex AI credentials
|
||||
# AI_GCP_PROJECT=
|
||||
# AI_GCP_LOCATION=
|
||||
# AI_GCP_CREDENTIALS_JSON=
|
||||
# AI_GCP_APPLICATION_CREDENTIALS=
|
||||
# Google Cloud settings for Gemini models
|
||||
# Credentials are optional when Application Default Credentials are available.
|
||||
# AI_GOOGLE_CLOUD_PROJECT=
|
||||
# AI_GOOGLE_CLOUD_LOCATION=
|
||||
# AI_GOOGLE_CLOUD_CREDENTIALS_JSON=
|
||||
# AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS=
|
||||
|
||||
# Amazon Bedrock credentials
|
||||
# AI_AWS_REGION=
|
||||
@@ -277,6 +311,13 @@ REDIS_URL=redis://localhost:6379
|
||||
# If the ip should be added in the log or not. Default 0
|
||||
# AUDIT_LOG_GET_USER_IP=0
|
||||
|
||||
# Optional Cube.js database overrides. The official local docker-compose.dev.yml stack points Cube at the
|
||||
# local `postgres` service automatically; set these only when running Cube yourself or changing bundled defaults.
|
||||
# CUBEJS_DB_HOST=postgres
|
||||
# CUBEJS_DB_PORT=5432
|
||||
# CUBEJS_DB_NAME=postgres
|
||||
# CUBEJS_DB_USER=postgres
|
||||
# CUBEJS_DB_PASS=postgres
|
||||
|
||||
# Lingo.dev API key for translation generation
|
||||
LINGO_API_KEY=your_api_key_here
|
||||
|
||||
@@ -57,16 +57,14 @@ runs:
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
- name: Fill E2E_TESTING in .env
|
||||
env:
|
||||
E2E_TESTING_MODE: ${{ inputs.e2e_testing_mode }}
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=$E2E_TESTING_MODE" >> .env
|
||||
shell: bash
|
||||
|
||||
|
||||
@@ -68,16 +68,12 @@ jobs:
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
- name: Fill ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${{ secrets.ENTERPRISE_LICENSE_KEY }}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://localhost:6379|" .env
|
||||
echo "" >> .env
|
||||
|
||||
@@ -31,15 +31,8 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
@@ -35,15 +35,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
- name: Adjust CI-specific env values
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=|" .env
|
||||
|
||||
- name: Run tests with coverage
|
||||
|
||||
@@ -32,15 +32,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
- name: Adjust CI-specific env values
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=|" .env
|
||||
|
||||
- name: Test
|
||||
|
||||
@@ -32,6 +32,7 @@ The `@formbricks/surveys` package is pre-compiled (Vite → UMD + ESM) and the b
|
||||
|
||||
TypeScript, React, and Prisma are the primary languages. Use the shared ESLint presets (`@formbricks/eslint-config`) and Prettier preset (110-char width, semicolons, double quotes, sorted import groups). Two-space indentation is standard; prefer `PascalCase` for React components and folders under `modules/`, `camelCase` for functions/variables, and `SCREAMING_SNAKE_CASE` only for constants. When adding mocks, place them inside `__mocks__` so import ordering stays stable.
|
||||
We are using SonarQube to identify code smells and security hotspots.
|
||||
Always mark React component props as `Readonly<>` (e.g., `({ children }: Readonly<MyProps>)`).
|
||||
|
||||
## Architecture & Patterns
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.2.17",
|
||||
"storybook": "10.2.17",
|
||||
"vite": "7.3.1",
|
||||
"vite": "7.3.2",
|
||||
"@storybook/addon-docs": "10.2.17"
|
||||
}
|
||||
}
|
||||
|
||||
+148
-29
@@ -1,20 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
Building2Icon,
|
||||
ChevronRightIcon,
|
||||
Loader2,
|
||||
LogOutIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { getOrganizationsForSwitcherAction } from "@/app/(app)/workspaces/[workspaceId]/actions";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
@@ -22,14 +34,65 @@ import {
|
||||
interface LandingSidebarProps {
|
||||
user: TUser;
|
||||
organization: TOrganization;
|
||||
isMultiOrgEnabled: boolean;
|
||||
}
|
||||
|
||||
export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false);
|
||||
export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: LandingSidebarProps) => {
|
||||
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<{ id: string; name: string }[]>([]);
|
||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(false);
|
||||
const [organizationLoadError, setOrganizationLoadError] = useState<string | null>(null);
|
||||
const [isOrgDropdownOpen, setIsOrgDropdownOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||
|
||||
const loadOrganizations = useCallback(async () => {
|
||||
setIsLoadingOrganizations(true);
|
||||
setOrganizationLoadError(null);
|
||||
try {
|
||||
const result = await getOrganizationsForSwitcherAction({ organizationId: organization.id });
|
||||
if (result?.data) {
|
||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setOrganizations(sorted);
|
||||
} else {
|
||||
setOrganizationLoadError(
|
||||
getFormattedErrorMessage(result) || t("common.failed_to_load_organizations")
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
setOrganizationLoadError(t("common.failed_to_load_organizations"));
|
||||
} finally {
|
||||
setIsLoadingOrganizations(false);
|
||||
}
|
||||
}, [organization.id, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOrgDropdownOpen &&
|
||||
organizations.length === 0 &&
|
||||
!isLoadingOrganizations &&
|
||||
!organizationLoadError
|
||||
) {
|
||||
loadOrganizations();
|
||||
}
|
||||
}, [
|
||||
isOrgDropdownOpen,
|
||||
organizations.length,
|
||||
isLoadingOrganizations,
|
||||
organizationLoadError,
|
||||
loadOrganizations,
|
||||
]);
|
||||
|
||||
const handleOrganizationChange = (orgId: string) => {
|
||||
startTransition(() => {
|
||||
setIsOrgDropdownOpen(false);
|
||||
router.push(`/organizations/${orgId}/`);
|
||||
});
|
||||
};
|
||||
|
||||
const dropdownNavigation = [
|
||||
{
|
||||
label: t("common.documentation"),
|
||||
@@ -39,6 +102,11 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const switcherTriggerClasses =
|
||||
"w-full border-t px-3 py-3 text-left transition-colors duration-200 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-inset";
|
||||
const switcherIconClasses =
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-600";
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
@@ -46,45 +114,97 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
)}>
|
||||
<Image src={FBLogo} width={160} height={30} alt={t("workspace.formbricks_logo")} />
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col">
|
||||
{/* Organization Switcher */}
|
||||
<DropdownMenu onOpenChange={setIsOrgDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild className={switcherTriggerClasses}>
|
||||
<button type="button" className="flex w-full items-center gap-3">
|
||||
<span className={switcherIconClasses}>
|
||||
<Building2Icon className="h-4 w-4" strokeWidth={1.5} />
|
||||
</span>
|
||||
<div className="grow overflow-hidden">
|
||||
<p className="truncate text-sm font-bold text-slate-700">{organization.name}</p>
|
||||
<p className="text-sm text-slate-500">{t("common.organization")}</p>
|
||||
</div>
|
||||
{isPending && <Loader2 className="h-4 w-4 animate-spin text-slate-600" strokeWidth={1.5} />}
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 text-slate-600" strokeWidth={1.5} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" sideOffset={10} alignOffset={5} align="end">
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<Building2Icon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.change_organization")}
|
||||
</div>
|
||||
{isLoadingOrganizations && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingOrganizations && organizationLoadError && (
|
||||
<div className="px-2 py-4">
|
||||
<p className="mb-2 text-sm text-red-600">{organizationLoadError}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOrganizationLoadError(null);
|
||||
setOrganizations([]);
|
||||
}}
|
||||
className="text-xs text-slate-600 underline hover:text-slate-800">
|
||||
{t("common.try_again")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingOrganizations && !organizationLoadError && (
|
||||
<>
|
||||
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
|
||||
{organizations.map((org) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={org.id}
|
||||
checked={org.id === organization.id}
|
||||
onClick={() => handleOrganizationChange(org.id)}
|
||||
className="cursor-pointer">
|
||||
{org.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
{isMultiOrgEnabled && (
|
||||
<DropdownMenuCheckboxItem
|
||||
onClick={() => setOpenCreateOrganizationModal(true)}
|
||||
className="w-full cursor-pointer justify-between">
|
||||
<span>{t("common.create_new_organization")}</span>
|
||||
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* User Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
id="userDropdownTrigger"
|
||||
className="w-full rounded-br-xl border-t p-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("flex w-full cursor-pointer flex-row items-center gap-3 text-left")}
|
||||
aria-haspopup="menu">
|
||||
<ProfileAvatar userId={user.id} />
|
||||
className={cn(switcherTriggerClasses, "rounded-br-xl")}>
|
||||
<button type="button" className="flex w-full items-center gap-3">
|
||||
<span className={switcherIconClasses}>
|
||||
<ProfileAvatar userId={user.id} />
|
||||
</span>
|
||||
<div className="grow overflow-hidden">
|
||||
<p
|
||||
title={user?.email}
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 truncate text-sm font-bold text-slate-700"
|
||||
)}>
|
||||
className="ph-no-capture -mb-0.5 truncate text-sm font-bold text-slate-700">
|
||||
{user?.name ? <span>{user?.name}</span> : <span>{user?.email}</span>}
|
||||
</p>
|
||||
<p title={organization?.name} className="truncate text-sm text-slate-500">
|
||||
{organization?.name}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{t("common.account")}</p>
|
||||
</div>
|
||||
<ChevronRightIcon className={cn("h-5 w-5 shrink-0 text-slate-700 hover:text-slate-500")} />
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 text-slate-600" strokeWidth={1.5} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
id="userDropdownInnerContentWrapper"
|
||||
side="right"
|
||||
sideOffset={10}
|
||||
alignOffset={5}
|
||||
align="end">
|
||||
{/* Dropdown Items */}
|
||||
|
||||
<DropdownMenuContent side="right" sideOffset={10} alignOffset={5} align="end">
|
||||
{dropdownNavigation.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
id={link.href}
|
||||
href={link.href}
|
||||
target={link.target}
|
||||
rel={link.target === "_blank" ? "noopener noreferrer" : undefined}
|
||||
@@ -95,8 +215,6 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Logout */}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOutWithAudit({
|
||||
@@ -113,6 +231,7 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<CreateOrganizationModal open={openCreateOrganizationModal} setOpen={setOpenCreateOrganizationModal} />
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organiza
|
||||
import { WorkspaceAndOrgSwitch } from "@/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -26,12 +25,11 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
const { isMember, isBilling } = getAccessFlags(membership?.role);
|
||||
const isMembershipPending = membership?.role === undefined;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-row">
|
||||
<LandingSidebar user={user} organization={organization} />
|
||||
<LandingSidebar user={user} organization={organization} isMultiOrgEnabled={isMultiOrgEnabled} />
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="p-6">
|
||||
@@ -45,8 +43,6 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
||||
isLicenseActive={false}
|
||||
isOwnerOrManager={false}
|
||||
isAccessControlAllowed={false}
|
||||
isMember={isMember}
|
||||
isBilling={isBilling}
|
||||
isMembershipPending={isMembershipPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ const mockTemplate: TXMTemplate = {
|
||||
],
|
||||
styling: {
|
||||
brandColor: { light: "#0000FF" },
|
||||
questionColor: { light: "#00FF00" },
|
||||
inputColor: { light: "#FF0000" },
|
||||
elementHeadlineColor: { light: "#00FF00" },
|
||||
inputBgColor: { light: "#FF0000" },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ export const ConfirmationPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild className="w-full justify-center">
|
||||
<Link href={resolvedWorkspaceId ? `/workspaces/${resolvedWorkspaceId}/settings/billing` : "/"}>
|
||||
<Link
|
||||
href={
|
||||
resolvedWorkspaceId ? `/workspaces/${resolvedWorkspaceId}/settings/organization/billing` : "/"
|
||||
}>
|
||||
{t("billing_confirmation.back_to_billing_overview")}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const Page = () => {
|
||||
return redirect("/");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChartsListPage } from "@/modules/ee/analysis/charts/components/charts-list-page";
|
||||
|
||||
const ChartsPage = async (props: Readonly<{ params: Promise<{ workspaceId: string }> }>) => {
|
||||
const { workspaceId } = await props.params;
|
||||
return <ChartsListPage workspaceId={workspaceId} />;
|
||||
};
|
||||
|
||||
export default ChartsPage;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { DashboardDetailPage } from "@/modules/ee/analysis/dashboards/pages/dashboard-detail-page";
|
||||
|
||||
const Page = (props: { params: Promise<{ workspaceId: string; dashboardId: string }> }) => {
|
||||
return <DashboardDetailPage params={props.params} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DashboardsListPage } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page";
|
||||
|
||||
const DashboardsPage = async (props: Readonly<{ params: Promise<{ workspaceId: string }> }>) => {
|
||||
const { workspaceId } = await props.params;
|
||||
return <DashboardsListPage workspaceId={workspaceId} />;
|
||||
};
|
||||
|
||||
export default DashboardsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { AnalysisListLoading } from "@/modules/ee/analysis/loading";
|
||||
|
||||
export default AnalysisListLoading;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function FeedbackSourcesRedirect(
|
||||
props: Readonly<{ params: Promise<{ workspaceId: string }> }>
|
||||
) {
|
||||
const { workspaceId } = await props.params;
|
||||
redirect(`/workspaces/${workspaceId}/settings/workspace/feedback-sources`);
|
||||
}
|
||||
@@ -23,9 +23,13 @@ import { createWorkspace } from "@/modules/workspaces/settings/lib/workspace";
|
||||
import { getOrganizationsByUserId } from "./lib/organization";
|
||||
import { getWorkspacesByUserId } from "./lib/workspace";
|
||||
|
||||
const ZCreateWorkspaceInput = ZWorkspaceUpdateInput.extend({
|
||||
feedbackDirectoryId: ZId.optional(),
|
||||
});
|
||||
|
||||
const ZCreateWorkspaceAction = z.object({
|
||||
organizationId: ZId,
|
||||
data: ZWorkspaceUpdateInput,
|
||||
data: ZCreateWorkspaceInput,
|
||||
});
|
||||
|
||||
export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCreateWorkspaceAction).action(
|
||||
@@ -40,7 +44,7 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
|
||||
access: [
|
||||
{
|
||||
data: parsedInput.data,
|
||||
schema: ZWorkspaceUpdateInput,
|
||||
schema: ZCreateWorkspaceInput,
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
@@ -117,7 +121,7 @@ const ZGetWorkspacesForSwitcherAction = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches projects list for switcher dropdown.
|
||||
* Fetches workspaces list for switcher dropdown.
|
||||
* Called on-demand when user opens the workspace switcher.
|
||||
*/
|
||||
export const getWorkspacesForSwitcherAction = authenticatedActionClient
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,450 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BellIcon,
|
||||
BlocksIcon,
|
||||
BrushIcon,
|
||||
Building2Icon,
|
||||
ChevronDownIcon,
|
||||
CreditCardIcon,
|
||||
FoldersIcon,
|
||||
GlobeIcon,
|
||||
KeyIcon,
|
||||
LanguagesIcon,
|
||||
ListChecksIcon,
|
||||
Loader2,
|
||||
ShapesIcon,
|
||||
ShieldIcon,
|
||||
TagIcon,
|
||||
UnplugIcon,
|
||||
UserCircleIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface SettingsSidebarContentProps {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
isFormbricksCloud: boolean;
|
||||
isCollapsed: boolean;
|
||||
isTextVisible: boolean;
|
||||
// Workspace switcher
|
||||
workspaces: { id: string; name: string }[];
|
||||
isLoadingWorkspaces: boolean;
|
||||
onWorkspaceChange: (id: string) => void;
|
||||
onWorkspaceDropdownOpen: () => void;
|
||||
// Organization switcher
|
||||
organizations: { id: string; name: string }[];
|
||||
isLoadingOrganizations: boolean;
|
||||
onOrganizationChange: (id: string) => void;
|
||||
onOrganizationDropdownOpen: () => void;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SettingsNavLink = ({
|
||||
item,
|
||||
isActive,
|
||||
isCollapsed,
|
||||
isTextVisible,
|
||||
disabledMessage,
|
||||
}: {
|
||||
item: NavItem;
|
||||
isActive: boolean;
|
||||
isCollapsed: boolean;
|
||||
isTextVisible: boolean;
|
||||
disabledMessage?: string;
|
||||
}) => {
|
||||
const activeClass = "bg-slate-50 border-r-4 border-brand-dark font-semibold text-slate-900";
|
||||
const inactiveClass =
|
||||
"hover:bg-slate-50 border-r-4 border-transparent hover:border-slate-300 transition-all duration-150 ease-in-out";
|
||||
const disabledClass = "cursor-not-allowed border-r-4 border-transparent text-slate-400";
|
||||
|
||||
const isDisabled = item.disabled;
|
||||
|
||||
const getStateClass = () => {
|
||||
if (isDisabled) return disabledClass;
|
||||
return isActive ? activeClass : inactiveClass;
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<li className={cn("rounded-l-md py-1.5 pl-2 text-sm", getStateClass())}>
|
||||
{isDisabled ? (
|
||||
<div className="flex items-center">{item.icon}</div>
|
||||
) : (
|
||||
<Link href={item.href} className="flex items-center text-slate-600 hover:text-slate-900">
|
||||
{item.icon}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{isDisabled ? disabledMessage || item.label : item.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
return (
|
||||
<li className={cn("rounded-l-md py-1.5 pl-8 text-sm", disabledClass)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
{item.icon}
|
||||
<span
|
||||
className={cn(
|
||||
"ml-2 transition-opacity duration-100",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit max-w-72 px-3 py-2 text-sm text-slate-700">
|
||||
{disabledMessage || item.label}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"rounded-l-md py-1.5 pl-8 text-sm",
|
||||
isActive ? activeClass : inactiveClass,
|
||||
"text-slate-600 hover:text-slate-900"
|
||||
)}>
|
||||
<Link href={item.href} className="flex items-center">
|
||||
{item.icon}
|
||||
<span
|
||||
className={cn("ml-2 transition-opacity duration-100", isTextVisible ? "opacity-0" : "opacity-100")}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionHeader = ({
|
||||
icon,
|
||||
label,
|
||||
isCollapsed,
|
||||
isTextVisible,
|
||||
switcherName,
|
||||
switcherItems,
|
||||
isLoadingSwitcher,
|
||||
currentId,
|
||||
onSwitcherChange,
|
||||
onSwitcherOpen,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
isCollapsed: boolean;
|
||||
isTextVisible: boolean;
|
||||
switcherName?: string;
|
||||
switcherItems?: { id: string; name: string }[];
|
||||
isLoadingSwitcher?: boolean;
|
||||
currentId?: string;
|
||||
onSwitcherChange?: (id: string) => void;
|
||||
onSwitcherOpen?: () => void;
|
||||
}) => {
|
||||
if (isCollapsed) {
|
||||
return <div className="mb-1 mt-3 flex justify-center px-2 text-slate-400">{icon}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-1 mt-4 flex min-w-0 items-center gap-2 px-3",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
<span className="text-slate-500">{icon}</span>
|
||||
<span className="shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-500">{label}</span>
|
||||
{switcherName && switcherItems && onSwitcherChange && (
|
||||
<DropdownMenu onOpenChange={(open) => open && onSwitcherOpen?.()}>
|
||||
<DropdownMenuTrigger className="ml-auto flex min-w-0 max-w-[50%] items-center gap-1 rounded-md border border-slate-200 px-2 py-0.5 text-xs text-slate-600 hover:bg-slate-50">
|
||||
<span className="truncate">{switcherName}</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-h-[300px]">
|
||||
{isLoadingSwitcher ? (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<DropdownMenuGroup className="overflow-y-auto">
|
||||
{switcherItems.map((item) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={item.id}
|
||||
checked={item.id === currentId}
|
||||
onClick={() => onSwitcherChange(item.id)}
|
||||
className="cursor-pointer text-sm">
|
||||
{item.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsSidebarContent = ({
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
organizationId,
|
||||
organizationName,
|
||||
membershipRole,
|
||||
isFormbricksCloud,
|
||||
isCollapsed,
|
||||
isTextVisible,
|
||||
workspaces,
|
||||
isLoadingWorkspaces,
|
||||
onWorkspaceChange,
|
||||
onWorkspaceDropdownOpen,
|
||||
organizations,
|
||||
isLoadingOrganizations,
|
||||
onOrganizationChange,
|
||||
onOrganizationDropdownOpen,
|
||||
}: SettingsSidebarContentProps) => {
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
const { isMember, isBilling, isOwner, isManager } = getAccessFlags(membershipRole);
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
const iconClassName = "h-4 w-4 shrink-0";
|
||||
|
||||
const basePath = `/workspaces/${workspaceId}/settings`;
|
||||
|
||||
const workspaceItems: NavItem[] = [
|
||||
{
|
||||
id: "general",
|
||||
label: t("common.general"),
|
||||
href: `${basePath}/workspace/general`,
|
||||
icon: <FoldersIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.team_access"),
|
||||
href: `${basePath}/workspace/teams`,
|
||||
icon: <UsersIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "languages",
|
||||
label: t("common.survey_languages"),
|
||||
href: `${basePath}/workspace/languages`,
|
||||
icon: <LanguagesIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "app-connection",
|
||||
label: t("common.connect_your_app"),
|
||||
href: `${basePath}/workspace/app-connection`,
|
||||
icon: <UnplugIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "feedback-sources",
|
||||
label: t("workspace.unify.feedback_sources"),
|
||||
href: `${basePath}/workspace/feedback-sources`,
|
||||
icon: <ShapesIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "integrations",
|
||||
label: t("common.integrations"),
|
||||
href: `${basePath}/workspace/integrations`,
|
||||
icon: <BlocksIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "look",
|
||||
label: t("common.appearance"),
|
||||
href: `${basePath}/workspace/look`,
|
||||
icon: <BrushIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "user-actions",
|
||||
label: t("common.user_actions"),
|
||||
href: `${basePath}/workspace/user-actions`,
|
||||
icon: <ListChecksIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
label: t("common.tags"),
|
||||
href: `${basePath}/workspace/tags`,
|
||||
icon: <TagIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
];
|
||||
|
||||
const organizationItems: NavItem[] = [
|
||||
{
|
||||
id: "org-general",
|
||||
label: t("common.general"),
|
||||
href: `${basePath}/organization/general`,
|
||||
icon: <Building2Icon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "org-teams",
|
||||
label: t("common.teams"),
|
||||
href: `${basePath}/organization/teams`,
|
||||
icon: <UsersIcon className={iconClassName} />,
|
||||
disabled: isBilling,
|
||||
},
|
||||
{
|
||||
id: "org-feedback-directories",
|
||||
label: t("workspace.settings.feedback_directories.nav_label"),
|
||||
href: `${basePath}/organization/feedback-directories`,
|
||||
icon: <FoldersIcon className={iconClassName} />,
|
||||
hidden: isMember,
|
||||
},
|
||||
{
|
||||
id: "org-api-keys",
|
||||
label: t("common.api_keys"),
|
||||
href: `${basePath}/organization/api-keys`,
|
||||
icon: <KeyIcon className={iconClassName} />,
|
||||
hidden: !isOwnerOrManager,
|
||||
},
|
||||
{
|
||||
id: "org-domain",
|
||||
label: t("common.domain"),
|
||||
href: `${basePath}/organization/domain`,
|
||||
icon: <GlobeIcon className={iconClassName} />,
|
||||
hidden: isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "org-billing",
|
||||
label: t("common.billing"),
|
||||
href: `${basePath}/organization/billing`,
|
||||
icon: <CreditCardIcon className={iconClassName} />,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "org-enterprise",
|
||||
label: t("common.enterprise_license"),
|
||||
href: `${basePath}/organization/enterprise`,
|
||||
icon: <ShieldIcon className={iconClassName} />,
|
||||
hidden: isFormbricksCloud,
|
||||
disabled: isMember || isBilling,
|
||||
},
|
||||
];
|
||||
|
||||
const accountItems: NavItem[] = [
|
||||
{
|
||||
id: "profile",
|
||||
label: t("common.your_profile"),
|
||||
href: `${basePath}/account/profile`,
|
||||
icon: <UserCircleIcon className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: t("common.notifications"),
|
||||
href: `${basePath}/account/notifications`,
|
||||
icon: <BellIcon className={iconClassName} />,
|
||||
},
|
||||
];
|
||||
|
||||
const disabledMessage = t("common.you_are_not_authorized_to_perform_this_action");
|
||||
|
||||
const renderSection = (items: NavItem[]) => {
|
||||
const visibleItems = items.filter((item) => !item.hidden);
|
||||
return (
|
||||
<ul className="space-y-0.5">
|
||||
{visibleItems.map((item) => (
|
||||
<SettingsNavLink
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={pathname.includes(item.href)}
|
||||
isCollapsed={isCollapsed}
|
||||
isTextVisible={isTextVisible}
|
||||
disabledMessage={item.disabled ? disabledMessage : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
<div>
|
||||
<SectionHeader
|
||||
icon={<FoldersIcon className="h-4 w-4" />}
|
||||
label={t("common.workspace")}
|
||||
isCollapsed={isCollapsed}
|
||||
isTextVisible={isTextVisible}
|
||||
switcherName={workspaceName}
|
||||
switcherItems={workspaces}
|
||||
isLoadingSwitcher={isLoadingWorkspaces}
|
||||
currentId={workspaceId}
|
||||
onSwitcherChange={onWorkspaceChange}
|
||||
onSwitcherOpen={onWorkspaceDropdownOpen}
|
||||
/>
|
||||
{renderSection(workspaceItems)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionHeader
|
||||
icon={<Building2Icon className="h-4 w-4" />}
|
||||
label={t("common.organization")}
|
||||
isCollapsed={isCollapsed}
|
||||
isTextVisible={isTextVisible}
|
||||
switcherName={organizationName}
|
||||
switcherItems={organizations}
|
||||
isLoadingSwitcher={isLoadingOrganizations}
|
||||
currentId={organizationId}
|
||||
onSwitcherChange={onOrganizationChange}
|
||||
onSwitcherOpen={onOrganizationDropdownOpen}
|
||||
/>
|
||||
{renderSection(organizationItems)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionHeader
|
||||
icon={<UserCircleIcon className="h-4 w-4" />}
|
||||
label={t("common.account")}
|
||||
isCollapsed={isCollapsed}
|
||||
isTextVisible={isTextVisible}
|
||||
/>
|
||||
{renderSection(accountItems)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { WorkspaceAndOrgSwitch } from "@/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
|
||||
interface TopControlBarProps {
|
||||
currentOrganizationId: string;
|
||||
@@ -26,7 +25,6 @@ export const TopControlBar = ({
|
||||
isAccessControlAllowed,
|
||||
membershipRole,
|
||||
}: TopControlBarProps) => {
|
||||
const { isMember, isBilling } = getAccessFlags(membershipRole);
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const isMembershipPending = membershipRole === undefined;
|
||||
|
||||
@@ -42,8 +40,6 @@ export const TopControlBar = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isLicenseActive={isLicenseActive}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isMember={isMember}
|
||||
isBilling={isBilling}
|
||||
isMembershipPending={isMembershipPending}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
|
||||
+14
-108
@@ -9,7 +9,7 @@ import {
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
|
||||
import { useOrganization, useWorkspace } from "../context/workspace-context";
|
||||
|
||||
interface OrganizationBreadcrumbProps {
|
||||
@@ -33,37 +32,17 @@ interface OrganizationBreadcrumbProps {
|
||||
currentOrganizationName?: string; // Optional: pass directly if context not available
|
||||
isMultiOrgEnabled: boolean;
|
||||
currentWorkspaceId?: string;
|
||||
isFormbricksCloud: boolean;
|
||||
isMember: boolean;
|
||||
isOwnerOrManager: boolean;
|
||||
isMembershipPending: boolean;
|
||||
}
|
||||
|
||||
const isActiveOrganizationSetting = (pathname: string, settingId: string): boolean => {
|
||||
// Match /settings/{settingId} or /settings/{settingId}/... but exclude account settings
|
||||
// Exclude paths with /(account)/
|
||||
if (pathname.includes("/(account)/")) {
|
||||
return false;
|
||||
}
|
||||
// Check if path matches /settings/{settingId} (with optional trailing path)
|
||||
const pattern = new RegExp(`/settings/${settingId}(?:/|$)`);
|
||||
return pattern.test(pathname);
|
||||
};
|
||||
|
||||
export const OrganizationBreadcrumb = ({
|
||||
currentOrganizationId,
|
||||
currentOrganizationName,
|
||||
isMultiOrgEnabled,
|
||||
currentWorkspaceId,
|
||||
isFormbricksCloud,
|
||||
isMember,
|
||||
isOwnerOrManager,
|
||||
isMembershipPending,
|
||||
}: OrganizationBreadcrumbProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOrganizationDropdownOpen, setIsOrganizationDropdownOpen] = useState(false);
|
||||
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(false);
|
||||
@@ -117,8 +96,12 @@ export const OrganizationBreadcrumb = ({
|
||||
const workspaceBasePath = `/workspaces/${workspace?.id}`;
|
||||
|
||||
const handleOrganizationChange = (organizationId: string) => {
|
||||
if (organizationId === currentOrganizationId) return;
|
||||
startTransition(() => {
|
||||
setIsOrganizationDropdownOpen(false);
|
||||
if (organizationId === currentOrganizationId && currentWorkspaceId) {
|
||||
router.push(`/workspaces/${currentWorkspaceId}/settings/organization/general`);
|
||||
return;
|
||||
}
|
||||
router.push(`/organizations/${organizationId}/`);
|
||||
});
|
||||
};
|
||||
@@ -133,56 +116,6 @@ export const OrganizationBreadcrumb = ({
|
||||
});
|
||||
};
|
||||
|
||||
const organizationSettings = [
|
||||
{
|
||||
id: "general",
|
||||
label: t("common.general"),
|
||||
href: `${workspaceBasePath}/settings/general`,
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.members_and_teams"),
|
||||
href: `${workspaceBasePath}/settings/teams`,
|
||||
},
|
||||
{
|
||||
id: "feedback-record-directories",
|
||||
label: t("workspace.settings.feedback_record_directories.nav_label"),
|
||||
href: `${workspaceBasePath}/settings/feedback-record-directories`,
|
||||
hidden: isMember,
|
||||
},
|
||||
{
|
||||
id: "api-keys",
|
||||
label: t("common.api_keys"),
|
||||
href: `${workspaceBasePath}/settings/api-keys`,
|
||||
disabled: isMembershipPending || !isOwnerOrManager,
|
||||
disabledMessage: isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action"),
|
||||
},
|
||||
{
|
||||
id: "domain",
|
||||
label: t("common.domain"),
|
||||
href: `${workspaceBasePath}/settings/domain`,
|
||||
hidden: isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "billing",
|
||||
label: t("common.billing"),
|
||||
href: `${workspaceBasePath}/settings/billing`,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
label: t("common.enterprise_license"),
|
||||
href: `${workspaceBasePath}/settings/enterprise`,
|
||||
hidden: isFormbricksCloud || isMember,
|
||||
disabled: isMembershipPending || isMember,
|
||||
disabledMessage: isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BreadcrumbItem isActive={isOrganizationDropdownOpen}>
|
||||
<DropdownMenu onOpenChange={setIsOrganizationDropdownOpen}>
|
||||
@@ -252,42 +185,15 @@ export const OrganizationBreadcrumb = ({
|
||||
</>
|
||||
)}
|
||||
{currentWorkspaceId && (
|
||||
<div>
|
||||
<>
|
||||
{showOrganizationDropdown && <DropdownMenuSeparator />}
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<SettingsIcon className="mr-2 inline h-4 w-4" />
|
||||
{t("common.organization_settings")}
|
||||
</div>
|
||||
|
||||
{organizationSettings.map((setting) => {
|
||||
return setting.hidden ? null : (
|
||||
<div key={setting.id}>
|
||||
{setting.disabled ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-disabled="true"
|
||||
className="relative flex w-full cursor-not-allowed select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm font-medium text-slate-400">
|
||||
{setting.label}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit max-w-72 px-3 py-2 text-sm text-slate-700">
|
||||
{setting.disabledMessage}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={isActiveOrganizationSetting(pathname, setting.id)}
|
||||
onClick={() => handleSettingChange(setting.href)}
|
||||
className="cursor-pointer">
|
||||
{setting.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<DropdownMenuCheckboxItem
|
||||
onClick={() => handleSettingChange(`${workspaceBasePath}/settings/organization/general`)}
|
||||
className="cursor-pointer">
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
{t("common.settings")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -14,9 +14,7 @@ interface WorkspaceAndOrgSwitchProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isLicenseActive: boolean;
|
||||
isOwnerOrManager: boolean;
|
||||
isMember: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isBilling: boolean;
|
||||
isMembershipPending: boolean;
|
||||
}
|
||||
|
||||
@@ -31,8 +29,6 @@ export const WorkspaceAndOrgSwitch = ({
|
||||
isLicenseActive,
|
||||
isOwnerOrManager,
|
||||
isAccessControlAllowed,
|
||||
isMember,
|
||||
isBilling,
|
||||
isMembershipPending,
|
||||
}: WorkspaceAndOrgSwitchProps) => {
|
||||
return (
|
||||
@@ -43,10 +39,6 @@ export const WorkspaceAndOrgSwitch = ({
|
||||
currentOrganizationName={currentOrganizationName}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isMember={isMember}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isMembershipPending={isMembershipPending}
|
||||
/>
|
||||
{currentWorkspaceId && (
|
||||
<WorkspaceBreadcrumb
|
||||
@@ -59,7 +51,6 @@ export const WorkspaceAndOrgSwitch = ({
|
||||
isLicenseActive={isLicenseActive}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isEnvironmentBreadcrumbVisible={false}
|
||||
isBilling={isBilling}
|
||||
isMembershipPending={isMembershipPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { ChevronDownIcon, ChevronRightIcon, CogIcon, FoldersIcon, Loader2, PlusIcon } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
@@ -33,20 +33,9 @@ interface WorkspaceBreadcrumbProps {
|
||||
currentOrganizationId: string;
|
||||
isAccessControlAllowed: boolean;
|
||||
isEnvironmentBreadcrumbVisible: boolean;
|
||||
isBilling: boolean;
|
||||
isMembershipPending: boolean;
|
||||
}
|
||||
|
||||
const isActiveWorkspaceSetting = (pathname: string, settingId: string): boolean => {
|
||||
// Match /{settingId} or /{settingId}/... but exclude settings paths
|
||||
if (pathname.includes("/settings/")) {
|
||||
return false;
|
||||
}
|
||||
// Check if path matches /workspaces/{id}/{settingId} (with optional trailing path)
|
||||
const pattern = new RegExp(`/workspaces/[^/]+/${settingId}(?:/|$)`);
|
||||
return pattern.test(pathname);
|
||||
};
|
||||
|
||||
export const WorkspaceBreadcrumb = ({
|
||||
currentWorkspaceId,
|
||||
currentWorkspaceName,
|
||||
@@ -57,7 +46,6 @@ export const WorkspaceBreadcrumb = ({
|
||||
currentOrganizationId,
|
||||
isAccessControlAllowed,
|
||||
isEnvironmentBreadcrumbVisible,
|
||||
isBilling,
|
||||
isMembershipPending,
|
||||
}: WorkspaceBreadcrumbProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -69,7 +57,6 @@ export const WorkspaceBreadcrumb = ({
|
||||
const [workspaces, setWorkspaces] = useState<{ id: string; name: string }[]>([]);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Get current workspace name from context OR prop
|
||||
// Context is preferred, but prop is fallback for pages without EnvironmentContextWrapper
|
||||
@@ -102,49 +89,6 @@ export const WorkspaceBreadcrumb = ({
|
||||
}
|
||||
}, [isWorkspaceDropdownOpen, currentOrganizationId, workspaces.length, isLoadingWorkspaces, loadError, t]);
|
||||
|
||||
const workspaceSettings = [
|
||||
{
|
||||
id: "general",
|
||||
label: t("common.general"),
|
||||
href: `${workspaceBasePath}/general`,
|
||||
},
|
||||
{
|
||||
id: "look",
|
||||
label: t("common.look_and_feel"),
|
||||
href: `${workspaceBasePath}/look`,
|
||||
},
|
||||
{
|
||||
id: "app-connection",
|
||||
label: t("common.website_and_app_connection"),
|
||||
href: `${workspaceBasePath}/app-connection`,
|
||||
},
|
||||
{
|
||||
id: "integrations",
|
||||
label: t("common.integrations"),
|
||||
href: `${workspaceBasePath}/integrations`,
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.team_access"),
|
||||
href: `${workspaceBasePath}/teams`,
|
||||
},
|
||||
{
|
||||
id: "languages",
|
||||
label: t("common.survey_languages"),
|
||||
href: `${workspaceBasePath}/languages`,
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
label: t("common.tags"),
|
||||
href: `${workspaceBasePath}/tags`,
|
||||
},
|
||||
];
|
||||
|
||||
const areWorkspaceSettingsDisabled = isMembershipPending || isBilling;
|
||||
const workspaceSettingsDisabledMessage = isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action");
|
||||
|
||||
if (!currentWorkspace) {
|
||||
const errorMessage = `Workspace not found for workspace id: ${currentWorkspaceId}`;
|
||||
logger.error(errorMessage);
|
||||
@@ -153,9 +97,13 @@ export const WorkspaceBreadcrumb = ({
|
||||
}
|
||||
|
||||
const handleWorkspaceChange = (workspaceId: string) => {
|
||||
if (workspaceId === currentWorkspaceId) return;
|
||||
const targetPath =
|
||||
workspaceId === currentWorkspaceId
|
||||
? `/workspaces/${currentWorkspaceId}/surveys`
|
||||
: `/workspaces/${workspaceId}/`;
|
||||
startTransition(() => {
|
||||
router.push(`/workspaces/${workspaceId}/`);
|
||||
setIsWorkspaceDropdownOpen(false);
|
||||
router.push(targetPath);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,9 +115,9 @@ export const WorkspaceBreadcrumb = ({
|
||||
setOpenCreateWorkspaceModal(true);
|
||||
};
|
||||
|
||||
const handleWorkspaceSettingsNavigation = (settingId: string) => {
|
||||
const handleWorkspaceSettingsNavigation = (href: string) => {
|
||||
startTransition(() => {
|
||||
router.push(`${workspaceBasePath}/${settingId}`);
|
||||
router.push(href);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -178,7 +126,7 @@ export const WorkspaceBreadcrumb = ({
|
||||
return [
|
||||
{
|
||||
text: t("workspace.settings.billing.upgrade"),
|
||||
href: `${workspaceBasePath}/settings/billing`,
|
||||
href: `${workspaceBasePath}/settings/organization/billing`,
|
||||
},
|
||||
{
|
||||
text: t("common.cancel"),
|
||||
@@ -191,7 +139,7 @@ export const WorkspaceBreadcrumb = ({
|
||||
{
|
||||
text: t("workspace.settings.billing.upgrade"),
|
||||
href: isLicenseActive
|
||||
? `${workspaceBasePath}/settings/enterprise`
|
||||
? `${workspaceBasePath}/settings/organization/enterprise`
|
||||
: "https://formbricks.com/upgrade-self-hosted-license",
|
||||
},
|
||||
{
|
||||
@@ -282,39 +230,15 @@ export const WorkspaceBreadcrumb = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<CogIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.workspace_configuration")}
|
||||
</div>
|
||||
{workspaceSettings.map((setting) => (
|
||||
<div key={setting.id}>
|
||||
{areWorkspaceSettingsDisabled ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-disabled="true"
|
||||
className="relative flex w-full cursor-not-allowed select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm font-medium text-slate-400">
|
||||
{setting.label}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit max-w-72 px-3 py-2 text-sm text-slate-700">
|
||||
{workspaceSettingsDisabledMessage}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={isActiveWorkspaceSetting(pathname, setting.id)}
|
||||
onClick={() => handleWorkspaceSettingsNavigation(setting.id)}
|
||||
className="cursor-pointer">
|
||||
{setting.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem
|
||||
onClick={() =>
|
||||
handleWorkspaceSettingsNavigation(`${workspaceBasePath}/settings/workspace/general`)
|
||||
}
|
||||
className="cursor-pointer">
|
||||
<CogIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.settings")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* Modals */}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getWorkspace } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
const AccountSettingsLayout = async (props: {
|
||||
params: Promise<{ workspaceId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const t = await getTranslate();
|
||||
const [workspace, session] = await Promise.all([
|
||||
getWorkspace(params.workspaceId),
|
||||
getServerSession(authOptions),
|
||||
]);
|
||||
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const organization = await getOrganization(workspace.organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AccountSettingsLayout;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import Loading from "@/modules/organization/settings/api-keys/loading";
|
||||
|
||||
export default function LoadingPage() {
|
||||
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
|
||||
interface OrganizationSettingsNavbarProps {
|
||||
isFormbricksCloud: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
activeId: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const OrganizationSettingsNavbar = ({
|
||||
isFormbricksCloud,
|
||||
membershipRole,
|
||||
activeId,
|
||||
loading,
|
||||
}: OrganizationSettingsNavbarProps) => {
|
||||
const pathname = usePathname();
|
||||
const { isMember, isOwner, isManager } = getAccessFlags(membershipRole);
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
const isMembershipPending = membershipRole === undefined || loading;
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const workspaceBasePath = `/workspaces/${workspace?.id}`;
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
id: "general",
|
||||
label: t("common.general"),
|
||||
href: `${workspaceBasePath}/settings/general`,
|
||||
current: pathname?.includes("/general"),
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.members_and_teams"),
|
||||
href: `${workspaceBasePath}/settings/teams`,
|
||||
current: pathname?.includes("/teams"),
|
||||
},
|
||||
{
|
||||
id: "feedback-record-directories",
|
||||
label: t("workspace.settings.feedback_record_directories.nav_label"),
|
||||
href: `${workspaceBasePath}/settings/feedback-record-directories`,
|
||||
current: pathname?.includes("/feedback-record-directories"),
|
||||
hidden: isMember,
|
||||
},
|
||||
{
|
||||
id: "api-keys",
|
||||
label: t("common.api_keys"),
|
||||
href: `${workspaceBasePath}/settings/api-keys`,
|
||||
current: pathname?.includes("/api-keys"),
|
||||
disabled: isMembershipPending || !isOwnerOrManager,
|
||||
disabledMessage: isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action"),
|
||||
},
|
||||
{
|
||||
id: "domain",
|
||||
label: t("common.domain"),
|
||||
href: `${workspaceBasePath}/settings/domain`,
|
||||
current: pathname?.includes("/domain"),
|
||||
hidden: isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "billing",
|
||||
label: t("common.billing"),
|
||||
href: `${workspaceBasePath}/settings/billing`,
|
||||
hidden: !isFormbricksCloud,
|
||||
current: pathname?.includes("/billing"),
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
label: t("common.enterprise_license"),
|
||||
href: `${workspaceBasePath}/settings/enterprise`,
|
||||
hidden: isFormbricksCloud,
|
||||
disabled: isMembershipPending || isMember,
|
||||
disabledMessage: isMembershipPending
|
||||
? t("common.loading")
|
||||
: t("common.you_are_not_authorized_to_perform_this_action"),
|
||||
current: pathname?.includes("/enterprise"),
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { FeedbackRecordDirectoriesPage as default } from "@/modules/ee/feedback-record-directory/page";
|
||||
@@ -1,36 +0,0 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getWorkspace } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
const Layout = async (props: { params: Promise<{ workspaceId: string }>; children: React.ReactNode }) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const t = await getTranslate();
|
||||
const [workspace, session] = await Promise.all([
|
||||
getWorkspace(params.workspaceId),
|
||||
getServerSession(authOptions),
|
||||
]);
|
||||
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const organization = await getOrganization(workspace.organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
+2
-2
@@ -19,13 +19,13 @@ export const AccountSettingsNavbar = ({ activeId, loading }: AccountSettingsNavb
|
||||
{
|
||||
id: "profile",
|
||||
label: t("common.profile"),
|
||||
href: `${workspaceBasePath}/settings/profile`,
|
||||
href: `${workspaceBasePath}/settings/account/profile`,
|
||||
current: pathname?.includes("/profile"),
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
label: t("common.notifications"),
|
||||
href: `${workspaceBasePath}/settings/notifications`,
|
||||
href: `${workspaceBasePath}/settings/account/notifications`,
|
||||
current: pathname?.includes("/notifications"),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
const AccountSettingsLayout = (props: { children: React.ReactNode }) => {
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default AccountSettingsLayout;
|
||||
+3
-1
@@ -99,7 +99,9 @@ export const EditAlerts = ({
|
||||
)}
|
||||
<p className="pb-3 pl-4 text-xs text-slate-400">
|
||||
{t("workspace.settings.notifications.want_to_loop_in_organization_mates")}{" "}
|
||||
<Link className="font-semibold" href={`/workspaces/${currentWorkspace?.id}/settings/general`}>
|
||||
<Link
|
||||
className="font-semibold"
|
||||
href={`/workspaces/${currentWorkspace?.id}/settings/organization/general`}>
|
||||
{t("common.invite_them")}
|
||||
</Link>
|
||||
</p>
|
||||
+1
-1
@@ -14,7 +14,7 @@ export const IntegrationsTip = () => {
|
||||
<p className="text-sm">
|
||||
{t("workspace.settings.notifications.need_slack_or_discord_notifications")}?
|
||||
<a
|
||||
href={`/workspaces/${workspace?.id}/integrations`}
|
||||
href={`/workspaces/${workspace?.id}/settings/workspace/integrations`}
|
||||
className="ml-1 cursor-pointer text-sm underline">
|
||||
{t("workspace.settings.notifications.use_the_integration")}
|
||||
</a>
|
||||
+1
-4
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
|
||||
@@ -18,9 +17,7 @@ const Loading = () => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar activeId="notifications" loading />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.notifications")} />
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
+5
-11
@@ -2,10 +2,9 @@ import { getServerSession } from "next-auth";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { EditAlerts } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/notifications/components/EditAlerts";
|
||||
import { IntegrationsTip } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/notifications/components/IntegrationsTip";
|
||||
import type { Membership } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/notifications/types";
|
||||
import { EditAlerts } from "@/app/(app)/workspaces/[workspaceId]/settings/account/notifications/components/EditAlerts";
|
||||
import { IntegrationsTip } from "@/app/(app)/workspaces/[workspaceId]/settings/account/notifications/components/IntegrationsTip";
|
||||
import type { Membership } from "@/app/(app)/workspaces/[workspaceId]/settings/account/notifications/types";
|
||||
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
@@ -128,10 +127,7 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
|
||||
return memberships;
|
||||
};
|
||||
|
||||
const Page = async (props: {
|
||||
params: Promise<{ workspaceId: string }>;
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) => {
|
||||
const Page = async (props: { searchParams: Promise<Record<string, string>> }) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -155,9 +151,7 @@ const Page = async (props: {
|
||||
}
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar activeId="notifications" />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.notifications")} />
|
||||
<SettingsCard
|
||||
title={t("workspace.settings.notifications.email_alerts_surveys")}
|
||||
description={t("workspace.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses")}>
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
getIsEmailUnique,
|
||||
verifyUserPassword,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/profile/lib/user";
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/settings/account/profile/lib/user";
|
||||
import { EMAIL_VERIFICATION_DISABLED, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||
import { getUser, updateUser } from "@/lib/user/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
+1
-1
@@ -8,7 +8,7 @@ import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
||||
import { PasswordConfirmationModal } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/profile/components/password-confirmation-modal";
|
||||
import { PasswordConfirmationModal } from "@/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/password-confirmation-modal";
|
||||
import { appLanguages, sortedAppLanguages } from "@/lib/i18n/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
+1
-4
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
|
||||
@@ -28,9 +27,7 @@ const Loading = () => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar activeId="profile" loading />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.profile")} />
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
+6
-9
@@ -1,8 +1,7 @@
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { AccountSecurity } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/profile/components/AccountSecurity";
|
||||
import { DeleteAccount } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/profile/components/DeleteAccount";
|
||||
import { EditProfileDetailsForm } from "@/app/(app)/workspaces/[workspaceId]/settings/(account)/profile/components/EditProfileDetailsForm";
|
||||
import { AccountSecurity } from "@/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/AccountSecurity";
|
||||
import { DeleteAccount } from "@/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/DeleteAccount";
|
||||
import { EditProfileDetailsForm } from "@/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/EditProfileDetailsForm";
|
||||
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
|
||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
@@ -34,9 +33,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar activeId="profile" />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.profile")} />
|
||||
{user && (
|
||||
<div>
|
||||
<SettingsCard
|
||||
@@ -62,13 +59,13 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
? t("common.upgrade_plan")
|
||||
: t("common.request_trial_license"),
|
||||
href: IS_FORMBRICKS_CLOUD
|
||||
? `/workspaces/${params.workspaceId}/settings/billing`
|
||||
? `/workspaces/${params.workspaceId}/settings/organization/billing`
|
||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||
},
|
||||
{
|
||||
text: t("common.learn_more"),
|
||||
href: IS_FORMBRICKS_CLOUD
|
||||
? `/workspaces/${params.workspaceId}/settings/billing`
|
||||
? `/workspaces/${params.workspaceId}/settings/organization/billing`
|
||||
: "https://formbricks.com/learn-more-self-hosting-license",
|
||||
},
|
||||
]}
|
||||
@@ -21,6 +21,7 @@ export const SettingsCard = ({
|
||||
beta,
|
||||
className,
|
||||
buttonInfo,
|
||||
cta,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -30,6 +31,7 @@ export const SettingsCard = ({
|
||||
beta?: boolean;
|
||||
className?: string;
|
||||
buttonInfo?: ButtonInfo;
|
||||
cta?: React.ReactNode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
@@ -52,11 +54,12 @@ export const SettingsCard = ({
|
||||
{description}
|
||||
</Small>
|
||||
</div>
|
||||
{buttonInfo && (
|
||||
<Button type="button" onClick={buttonInfo?.onClick} variant={buttonInfo?.variant ?? "default"}>
|
||||
{buttonInfo?.text}
|
||||
</Button>
|
||||
)}
|
||||
{cta ??
|
||||
(buttonInfo && (
|
||||
<Button type="button" onClick={buttonInfo?.onClick} variant={buttonInfo?.variant ?? "default"}>
|
||||
{buttonInfo?.text}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className={cn(noPadding ? "" : "px-4 pt-4")}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const SettingsLayout = (props: { children: React.ReactNode }) => {
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default SettingsLayout;
|
||||
@@ -0,0 +1,5 @@
|
||||
import Loading from "@/modules/organization/settings/api-keys/loading";
|
||||
|
||||
export default function LoadingPage() {
|
||||
return <Loading />;
|
||||
}
|
||||
+1
-5
@@ -1,5 +1,3 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -8,9 +6,7 @@ const Loading = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="billing" loading />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.billing")} />
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</PageContentWrapper>
|
||||
+3
-12
@@ -1,8 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { PrettyUrlsTable } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/domain/components/pretty-urls-table";
|
||||
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
|
||||
import { PrettyUrlsTable } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/domain/components/pretty-urls-table";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -21,9 +20,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { session, currentUserMembership, organization, isOwner, isManager } = await getWorkspaceAuth(
|
||||
params.workspaceId
|
||||
);
|
||||
const { session, organization, isOwner, isManager } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
@@ -36,13 +33,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
activeId="domain"
|
||||
/>
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.domain")} />
|
||||
|
||||
{!IS_STORAGE_CONFIGURED && (
|
||||
<div className="max-w-4xl">
|
||||
+11
-1
@@ -8,7 +8,7 @@ import type { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/type
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
|
||||
type TPublicLicenseFeatureKey = Exclude<keyof TEnterpriseLicenseFeatures, "isMultiOrgEnabled" | "ai">;
|
||||
type TPublicLicenseFeatureKey = Exclude<keyof TEnterpriseLicenseFeatures, "isMultiOrgEnabled">;
|
||||
|
||||
type TFeatureDefinition = {
|
||||
key: TPublicLicenseFeatureKey;
|
||||
@@ -61,6 +61,16 @@ const getFeatureDefinitions = (t: TFunction): TFeatureDefinition[] => {
|
||||
labelKey: t("workspace.settings.enterprise.license_feature_spam_protection"),
|
||||
docsUrl: "https://formbricks.com/docs/xm-and-surveys/surveys/general-features/spam-protection",
|
||||
},
|
||||
{
|
||||
key: "aiSmartTools",
|
||||
labelKey: t("workspace.settings.general.ai_smart_tools_enabled"),
|
||||
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
|
||||
},
|
||||
{
|
||||
key: "aiDataAnalysis",
|
||||
labelKey: t("workspace.settings.general.ai_data_analysis_enabled"),
|
||||
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
|
||||
},
|
||||
{
|
||||
key: "auditLogs",
|
||||
labelKey: t("workspace.settings.enterprise.license_feature_audit_logs"),
|
||||
+1
-5
@@ -1,5 +1,3 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -8,9 +6,7 @@ const Loading = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="enterprise" loading />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.enterprise_license")} />
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</PageContentWrapper>
|
||||
+4
-11
@@ -1,9 +1,8 @@
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { EnterpriseLicenseFeaturesTable } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/enterprise/components/EnterpriseLicenseFeaturesTable";
|
||||
import { EnterpriseLicenseStatus } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/enterprise/components/EnterpriseLicenseStatus";
|
||||
import { EnterpriseLicenseFeaturesTable } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/enterprise/components/EnterpriseLicenseFeaturesTable";
|
||||
import { EnterpriseLicenseStatus } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/enterprise/components/EnterpriseLicenseStatus";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { GRACE_PERIOD_MS, getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
@@ -19,7 +18,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { isMember, currentUserMembership } = await getWorkspaceAuth(params.workspaceId);
|
||||
const { isMember } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
const isPricingDisabled = isMember;
|
||||
|
||||
@@ -85,13 +84,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
activeId="enterprise"
|
||||
/>
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("common.enterprise_license")} />
|
||||
{hasLicense ? (
|
||||
<>
|
||||
<EnterpriseLicenseStatus
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { FeedbackDirectoriesPage as default } from "@/modules/ee/feedback-directory/page";
|
||||
+35
-1
@@ -6,24 +6,32 @@ import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { updateOrganizationAISettingsAction } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/general/actions";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { updateOrganizationAISettingsAction } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/general/actions";
|
||||
import { getDisplayedOrganizationAISettingValue, getOrganizationAIEnablementState } from "@/lib/ai/utils";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { type ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
|
||||
interface AISettingsToggleProps {
|
||||
organization: TOrganization;
|
||||
membershipRole?: TOrganizationRole;
|
||||
isInstanceAIConfigured: boolean;
|
||||
hasAIPermission: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export const AISettingsToggle = ({
|
||||
organization,
|
||||
membershipRole,
|
||||
isInstanceAIConfigured,
|
||||
hasAIPermission,
|
||||
isFormbricksCloud,
|
||||
}: Readonly<AISettingsToggleProps>) => {
|
||||
const { workspace } = useWorkspace();
|
||||
const workspaceBasePath = `/workspaces/${workspace?.id}`;
|
||||
const [loadingField, setLoadingField] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -78,6 +86,32 @@ export const AISettingsToggle = ({
|
||||
}
|
||||
};
|
||||
|
||||
const upgradeButtons: [ModalButton, ModalButton] = [
|
||||
{
|
||||
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
|
||||
href: isFormbricksCloud
|
||||
? `${workspaceBasePath}/settings/organization/billing`
|
||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||
},
|
||||
{
|
||||
text: t("common.learn_more"),
|
||||
href: isFormbricksCloud
|
||||
? `${workspaceBasePath}/settings/organization/billing`
|
||||
: "https://formbricks.com/learn-more-self-hosting-license",
|
||||
},
|
||||
];
|
||||
|
||||
if (!hasAIPermission) {
|
||||
return (
|
||||
<UpgradePrompt
|
||||
title={t("workspace.settings.general.unlock_ai_features_with_a_higher_plan")}
|
||||
description={t("workspace.settings.general.unlock_ai_features_description")}
|
||||
buttons={upgradeButtons}
|
||||
feature="ai_features"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{showInstanceConfigWarning && (
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Dispatch, SetStateAction, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { deleteOrganizationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/general/actions";
|
||||
import { deleteOrganizationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/general/actions";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
|
||||
import { updateOrganizationNameAction } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/general/actions";
|
||||
import { updateOrganizationNameAction } from "@/app/(app)/workspaces/[workspaceId]/settings/organization/general/actions";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
+1
-5
@@ -1,6 +1,4 @@
|
||||
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -23,9 +21,7 @@ const Loading = async () => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="general" loading />
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")} />
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
+17
-11
@@ -1,9 +1,13 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { isInstanceAIConfigured } from "@/lib/ai/service";
|
||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getIsAIDataAnalysisEnabled,
|
||||
getIsAISmartToolsEnabled,
|
||||
getIsMultiOrgEnabled,
|
||||
getWhiteLabelPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
@@ -27,8 +31,14 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
const user = session?.user?.id ? await getUser(session.user.id) : null;
|
||||
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.id);
|
||||
const [isMultiOrgEnabled, hasWhiteLabelPermission, hasAISmartToolsPermission, hasAIDataAnalysisPermission] =
|
||||
await Promise.all([
|
||||
getIsMultiOrgEnabled(),
|
||||
getWhiteLabelPermission(organization.id),
|
||||
getIsAISmartToolsEnabled(organization.id),
|
||||
getIsAIDataAnalysisEnabled(organization.id),
|
||||
]);
|
||||
const hasAIPermission = hasAISmartToolsPermission || hasAIDataAnalysisPermission;
|
||||
|
||||
const isDeleteDisabled = !isOwner || !isMultiOrgEnabled;
|
||||
const currentUserRole = currentUserMembership?.role;
|
||||
@@ -37,13 +47,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
activeId="general"
|
||||
/>
|
||||
</PageHeader>
|
||||
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")} />
|
||||
{!IS_STORAGE_CONFIGURED && (
|
||||
<div className="max-w-4xl">
|
||||
<Alert variant="warning">
|
||||
@@ -64,6 +68,8 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
organization={organization}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
isInstanceAIConfigured={isInstanceAIConfigured()}
|
||||
hasAIPermission={hasAIPermission}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<EmailCustomizationSettings
|
||||
@@ -0,0 +1,5 @@
|
||||
const OrganizationSettingsLayout = (props: { children: React.ReactNode }) => {
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default OrganizationSettingsLayout;
|
||||
@@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
const params = await props.params;
|
||||
return redirect(`/workspaces/${params.workspaceId}/settings/profile`);
|
||||
return redirect(`/workspaces/${params.workspaceId}/settings/workspace/general`);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { UserActionsLoading as default } from "@/modules/workspaces/settings/(setup)/user-actions/loading";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { UserActionsPage as default } from "@/modules/workspaces/settings/(setup)/user-actions/page";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { WorkspaceFeedbackSourcesPage as default } from "@/modules/ee/unify-feedback/sources/page";
|
||||
+3
-3
@@ -17,9 +17,9 @@ import {
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||
import { BaseSelectDropdown } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/lib/airtable";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/actions";
|
||||
import { BaseSelectDropdown } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/lib/airtable";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/lib/airtable";
|
||||
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/lib/airtable";
|
||||
import airtableLogo from "@/images/airtableLogo.svg";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||
import { AddIntegrationModal } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/AddIntegrationModal";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/actions";
|
||||
import { AddIntegrationModal } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/components/AddIntegrationModal";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { AirtableWrapper } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/AirtableWrapper";
|
||||
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys";
|
||||
import { AirtableWrapper } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/airtable/components/AirtableWrapper";
|
||||
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/lib/surveys";
|
||||
import { getAirtableTables } from "@/lib/airtable/service";
|
||||
import { AIRTABLE_CLIENT_ID, DEFAULT_LOCALE, WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
@@ -40,7 +40,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/integrations`} />
|
||||
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/settings/workspace/integrations`} />
|
||||
<PageHeader pageTitle={t("workspace.integrations.airtable.airtable_integration")} />
|
||||
<div className="h-[75vh] w-full">
|
||||
<AirtableWrapper
|
||||
+3
-3
@@ -12,13 +12,13 @@ import {
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/actions";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/actions";
|
||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/actions";
|
||||
import {
|
||||
constructGoogleSheetsUrl,
|
||||
extractSpreadsheetIdFromUrl,
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/lib/util";
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import {
|
||||
GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION,
|
||||
+3
-3
@@ -7,9 +7,9 @@ import {
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { validateGoogleSheetsConnectionAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/actions";
|
||||
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/lib/google";
|
||||
import { validateGoogleSheetsConnectionAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/actions";
|
||||
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/lib/google";
|
||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { GOOGLE_SHEET_INTEGRATION_INVALID_GRANT } from "@/lib/googleSheet/constants";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/actions";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert";
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys";
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/settings/workspace/integrations/lib/surveys";
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
@@ -39,7 +39,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/integrations`} />
|
||||
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/settings/workspace/integrations`} />
|
||||
<PageHeader pageTitle={t("workspace.integrations.google_sheets.google_sheets_integration")} />
|
||||
<div className="h-[75vh] w-full">
|
||||
<GoogleSheetWrapper
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user