fix: breadcrumb dropdown active state and loading indicators (#6714)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Johannes
2025-10-26 22:54:18 -07:00
committed by GitHub
parent a5fa876aa3
commit a6d45a63fa
2 changed files with 50 additions and 14 deletions

View File

@@ -10,7 +10,7 @@ import {
SettingsIcon,
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
@@ -34,6 +34,17 @@ interface OrganizationBreadcrumbProps {
isOwnerOrManager: 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,
organizations,
@@ -48,7 +59,7 @@ export const OrganizationBreadcrumb = ({
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState(false);
const pathname = usePathname();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const currentOrganization = organizations.find((org) => org.id === currentOrganizationId);
if (!currentOrganization) {
@@ -60,13 +71,21 @@ export const OrganizationBreadcrumb = ({
const handleOrganizationChange = (organizationId: string) => {
if (organizationId === currentOrganizationId) return;
setIsLoading(true);
router.push(`/organizations/${organizationId}/`);
startTransition(() => {
router.push(`/organizations/${organizationId}/`);
});
};
// Hide organization dropdown for single org setups (on-premise)
const showOrganizationDropdown = isMultiOrgEnabled || organizations.length > 1;
const handleSettingChange = (href: string) => {
startTransition(() => {
setIsOrganizationDropdownOpen(false);
router.push(href);
});
};
const organizationSettings = [
{
id: "general",
@@ -108,7 +127,7 @@ export const OrganizationBreadcrumb = ({
<div className="flex items-center gap-1">
<BuildingIcon className="h-3 w-3" strokeWidth={1.5} />
<span>{currentOrganization.name}</span>
{isLoading && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
{isOrganizationDropdownOpen ? (
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
) : (
@@ -156,9 +175,9 @@ export const OrganizationBreadcrumb = ({
return setting.hidden ? null : (
<DropdownMenuCheckboxItem
key={setting.id}
checked={pathname.includes(setting.id)}
checked={isActiveOrganizationSetting(pathname, setting.id)}
hidden={setting.hidden}
onClick={() => router.push(setting.href)}
onClick={() => handleSettingChange(setting.href)}
className="cursor-pointer">
{setting.label}
</DropdownMenuCheckboxItem>

View File

@@ -3,7 +3,7 @@
import * as Sentry from "@sentry/nextjs";
import { ChevronDownIcon, ChevronRightIcon, CogIcon, FolderOpenIcon, Loader2, PlusIcon } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { CreateProjectModal } from "@/modules/projects/components/create-project-modal";
@@ -32,6 +32,16 @@ interface ProjectBreadcrumbProps {
isEnvironmentBreadcrumbVisible: boolean;
}
const isActiveProjectSetting = (pathname: string, settingId: string): boolean => {
// Match /project/{settingId} or /project/{settingId}/... but exclude settings paths
if (pathname.includes("/settings/")) {
return false;
}
// Check if path matches /project/{settingId} (with optional trailing path)
const pattern = new RegExp(`/project/${settingId}(?:/|$)`);
return pattern.test(pathname);
};
export const ProjectBreadcrumb = ({
currentProjectId,
projects,
@@ -49,7 +59,7 @@ export const ProjectBreadcrumb = ({
const [openCreateProjectModal, setOpenCreateProjectModal] = useState(false);
const [openLimitModal, setOpenLimitModal] = useState(false);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const pathname = usePathname();
const projectSettings = [
@@ -101,8 +111,9 @@ export const ProjectBreadcrumb = ({
const handleProjectChange = (projectId: string) => {
if (projectId === currentProjectId) return;
setIsLoading(true);
router.push(`/projects/${projectId}/`);
startTransition(() => {
router.push(`/projects/${projectId}/`);
});
};
const handleAddProject = () => {
@@ -113,6 +124,12 @@ export const ProjectBreadcrumb = ({
setOpenCreateProjectModal(true);
};
const handleProjectSettingsNavigation = (settingId: string) => {
startTransition(() => {
router.push(`/environments/${currentEnvironmentId}/project/${settingId}`);
});
};
const LimitModalButtons = (): [ModalButton, ModalButton] => {
if (isFormbricksCloud) {
return [
@@ -150,7 +167,7 @@ export const ProjectBreadcrumb = ({
<div className="flex items-center gap-1">
<FolderOpenIcon className="h-3 w-3" strokeWidth={1.5} />
<span>{currentProject.name}</span>
{isLoading && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
{isProjectDropdownOpen ? (
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
) : (
@@ -194,8 +211,8 @@ export const ProjectBreadcrumb = ({
{projectSettings.map((setting) => (
<DropdownMenuCheckboxItem
key={setting.id}
checked={pathname.includes(setting.id)}
onClick={() => router.push(setting.href)}
checked={isActiveProjectSetting(pathname, setting.id)}
onClick={() => handleProjectSettingsNavigation(setting.id)}
className="cursor-pointer">
{setting.label}
</DropdownMenuCheckboxItem>