|
|
|
|
@@ -2,13 +2,18 @@
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ArrowUpRightIcon,
|
|
|
|
|
Building2Icon,
|
|
|
|
|
ChevronRightIcon,
|
|
|
|
|
Cog,
|
|
|
|
|
HotelIcon,
|
|
|
|
|
Loader2,
|
|
|
|
|
LogOutIcon,
|
|
|
|
|
MessageCircle,
|
|
|
|
|
PanelLeftCloseIcon,
|
|
|
|
|
PanelLeftOpenIcon,
|
|
|
|
|
PlusIcon,
|
|
|
|
|
RocketIcon,
|
|
|
|
|
SettingsIcon,
|
|
|
|
|
UserCircleIcon,
|
|
|
|
|
UserIcon,
|
|
|
|
|
WorkflowIcon,
|
|
|
|
|
@@ -16,28 +21,40 @@ import {
|
|
|
|
|
import Image from "next/image";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { useEffect, useMemo, useState, useTransition } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { TEnvironment } from "@formbricks/types/environment";
|
|
|
|
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
|
|
|
|
import { TOrganization } from "@formbricks/types/organizations";
|
|
|
|
|
import { TUser } from "@formbricks/types/user";
|
|
|
|
|
import {
|
|
|
|
|
getOrganizationsForSwitcherAction,
|
|
|
|
|
getProjectsForSwitcherAction,
|
|
|
|
|
} from "@/app/(app)/environments/[environmentId]/actions";
|
|
|
|
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
|
|
|
|
import { isNewerVersion } from "@/app/(app)/environments/[environmentId]/lib/utils";
|
|
|
|
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
|
|
|
|
import { cn } from "@/lib/cn";
|
|
|
|
|
import { getAccessFlags } from "@/lib/membership/utils";
|
|
|
|
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
|
|
|
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
|
|
|
|
import { TrialAlert } from "@/modules/ee/billing/components/trial-alert";
|
|
|
|
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
|
|
|
|
import { CreateProjectModal } from "@/modules/projects/components/create-project-modal";
|
|
|
|
|
import { ProjectLimitModal } from "@/modules/projects/components/project-limit-modal";
|
|
|
|
|
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
|
|
|
|
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
|
|
|
|
import { Button } from "@/modules/ui/components/button";
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuCheckboxItem,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuGroup,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/modules/ui/components/dropdown-menu";
|
|
|
|
|
import { ModalButton } from "@/modules/ui/components/upgrade-prompt";
|
|
|
|
|
import packageJson from "../../../../../package.json";
|
|
|
|
|
|
|
|
|
|
interface NavigationProps {
|
|
|
|
|
@@ -49,8 +66,30 @@ interface NavigationProps {
|
|
|
|
|
isDevelopment: boolean;
|
|
|
|
|
membershipRole?: TOrganizationRole;
|
|
|
|
|
publicDomain: string;
|
|
|
|
|
isMultiOrgEnabled: boolean;
|
|
|
|
|
organizationProjectsLimit: number;
|
|
|
|
|
isLicenseActive: boolean;
|
|
|
|
|
isAccessControlAllowed: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isActiveProjectSetting = (pathname: string, settingId: string): boolean => {
|
|
|
|
|
if (pathname.includes("/settings/")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pattern = new RegExp(`/workspace/${settingId}(?:/|$)`);
|
|
|
|
|
return pattern.test(pathname);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isActiveOrganizationSetting = (pathname: string, settingId: string): boolean => {
|
|
|
|
|
if (pathname.includes("/(account)/")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pattern = new RegExp(`/settings/${settingId}(?:/|$)`);
|
|
|
|
|
return pattern.test(pathname);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const MainNavigation = ({
|
|
|
|
|
environment,
|
|
|
|
|
organization,
|
|
|
|
|
@@ -60,6 +99,10 @@ export const MainNavigation = ({
|
|
|
|
|
isFormbricksCloud,
|
|
|
|
|
isDevelopment,
|
|
|
|
|
publicDomain,
|
|
|
|
|
isMultiOrgEnabled,
|
|
|
|
|
organizationProjectsLimit,
|
|
|
|
|
isLicenseActive,
|
|
|
|
|
isAccessControlAllowed,
|
|
|
|
|
}: NavigationProps) => {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
@@ -69,7 +112,8 @@ export const MainNavigation = ({
|
|
|
|
|
const [latestVersion, setLatestVersion] = useState("");
|
|
|
|
|
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
|
|
|
|
|
|
|
|
|
const { isManager, isOwner, isBilling } = getAccessFlags(membershipRole);
|
|
|
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
|
const { isManager, isOwner, isBilling, isMember } = getAccessFlags(membershipRole);
|
|
|
|
|
|
|
|
|
|
const isOwnerOrManager = isManager || isOwner;
|
|
|
|
|
|
|
|
|
|
@@ -153,6 +197,143 @@ export const MainNavigation = ({
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const [isWorkspaceDropdownOpen, setIsWorkspaceDropdownOpen] = useState(false);
|
|
|
|
|
const [isOrganizationDropdownOpen, setIsOrganizationDropdownOpen] = useState(false);
|
|
|
|
|
const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
|
const [organizations, setOrganizations] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
|
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
|
|
|
|
|
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(false);
|
|
|
|
|
const [workspaceLoadError, setWorkspaceLoadError] = useState<string | null>(null);
|
|
|
|
|
const [organizationLoadError, setOrganizationLoadError] = useState<string | null>(null);
|
|
|
|
|
const [openCreateProjectModal, setOpenCreateProjectModal] = useState(false);
|
|
|
|
|
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState(false);
|
|
|
|
|
const [openProjectLimitModal, setOpenProjectLimitModal] = useState(false);
|
|
|
|
|
|
|
|
|
|
const projectSettings = [
|
|
|
|
|
{
|
|
|
|
|
id: "general",
|
|
|
|
|
label: t("common.general"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/general`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "look",
|
|
|
|
|
label: t("common.look_and_feel"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/look`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "app-connection",
|
|
|
|
|
label: t("common.website_and_app_connection"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/app-connection`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "integrations",
|
|
|
|
|
label: t("common.integrations"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/integrations`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "teams",
|
|
|
|
|
label: t("common.team_access"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/teams`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "languages",
|
|
|
|
|
label: t("common.survey_languages"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/languages`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "tags",
|
|
|
|
|
label: t("common.tags"),
|
|
|
|
|
href: `/environments/${environment.id}/workspace/tags`,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const organizationSettings = [
|
|
|
|
|
{
|
|
|
|
|
id: "general",
|
|
|
|
|
label: t("common.general"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/general`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "teams",
|
|
|
|
|
label: t("common.members_and_teams"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/teams`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "api-keys",
|
|
|
|
|
label: t("common.api_keys"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/api-keys`,
|
|
|
|
|
hidden: !isOwnerOrManager,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "domain",
|
|
|
|
|
label: t("common.domain"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/domain`,
|
|
|
|
|
hidden: isFormbricksCloud,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "billing",
|
|
|
|
|
label: t("common.billing"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/billing`,
|
|
|
|
|
hidden: !isFormbricksCloud,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "enterprise",
|
|
|
|
|
label: t("common.enterprise_license"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/enterprise`,
|
|
|
|
|
hidden: isFormbricksCloud || isMember,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isWorkspaceDropdownOpen || projects.length > 0 || isLoadingProjects || workspaceLoadError) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsLoadingProjects(true);
|
|
|
|
|
setWorkspaceLoadError(null);
|
|
|
|
|
getProjectsForSwitcherAction({ organizationId: organization.id }).then((result) => {
|
|
|
|
|
if (result?.data) {
|
|
|
|
|
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
setProjects(sorted);
|
|
|
|
|
} else {
|
|
|
|
|
setWorkspaceLoadError(getFormattedErrorMessage(result) || t("common.failed_to_load_workspaces"));
|
|
|
|
|
}
|
|
|
|
|
setIsLoadingProjects(false);
|
|
|
|
|
});
|
|
|
|
|
}, [isWorkspaceDropdownOpen, projects.length, isLoadingProjects, workspaceLoadError, organization.id, t]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (
|
|
|
|
|
!isOrganizationDropdownOpen ||
|
|
|
|
|
organizations.length > 0 ||
|
|
|
|
|
isLoadingOrganizations ||
|
|
|
|
|
organizationLoadError
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsLoadingOrganizations(true);
|
|
|
|
|
setOrganizationLoadError(null);
|
|
|
|
|
getOrganizationsForSwitcherAction({ organizationId: organization.id }).then((result) => {
|
|
|
|
|
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")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
setIsLoadingOrganizations(false);
|
|
|
|
|
});
|
|
|
|
|
}, [
|
|
|
|
|
isOrganizationDropdownOpen,
|
|
|
|
|
organizations.length,
|
|
|
|
|
isLoadingOrganizations,
|
|
|
|
|
organizationLoadError,
|
|
|
|
|
organization.id,
|
|
|
|
|
t,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
async function loadReleases() {
|
|
|
|
|
const res = await getLatestStableFbReleaseAction();
|
|
|
|
|
@@ -184,6 +365,71 @@ export const MainNavigation = ({
|
|
|
|
|
|
|
|
|
|
const mainNavigationLink = `/environments/${environment.id}/${isBilling ? "settings/billing/" : "surveys/"}`;
|
|
|
|
|
|
|
|
|
|
const handleProjectChange = (projectId: string) => {
|
|
|
|
|
if (projectId === project.id) return;
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
router.push(`/workspaces/${projectId}/`);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOrganizationChange = (organizationId: string) => {
|
|
|
|
|
if (organizationId === organization.id) return;
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
router.push(`/organizations/${organizationId}/`);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSettingNavigation = (href: string) => {
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
router.push(href);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleProjectCreate = () => {
|
|
|
|
|
if (projects.length >= organizationProjectsLimit) {
|
|
|
|
|
setOpenProjectLimitModal(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOpenCreateProjectModal(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const projectLimitModalButtons = (): [ModalButton, ModalButton] => {
|
|
|
|
|
if (isFormbricksCloud) {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
text: t("environments.settings.billing.upgrade"),
|
|
|
|
|
href: `/environments/${environment.id}/settings/billing`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
text: t("common.cancel"),
|
|
|
|
|
onClick: () => setOpenProjectLimitModal(false),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
text: t("environments.settings.billing.upgrade"),
|
|
|
|
|
href: isLicenseActive
|
|
|
|
|
? `/environments/${environment.id}/settings/enterprise`
|
|
|
|
|
: "https://formbricks.com/upgrade-self-hosted-license",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
text: t("common.cancel"),
|
|
|
|
|
onClick: () => setOpenProjectLimitModal(false),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const switcherTriggerClasses = cn(
|
|
|
|
|
"w-full border-t px-3 py-3 text-left transition-colors duration-200 hover:bg-slate-50 focus:outline-none",
|
|
|
|
|
isCollapsed ? "flex items-center justify-center" : ""
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const switcherIconClasses =
|
|
|
|
|
"flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-600";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{project && (
|
|
|
|
|
@@ -263,35 +509,215 @@ export const MainNavigation = ({
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* User Switch */}
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<DropdownMenu onOpenChange={setIsWorkspaceDropdownOpen}>
|
|
|
|
|
<DropdownMenuTrigger asChild id="workspaceDropdownTrigger" className={switcherTriggerClasses}>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex w-full cursor-pointer items-center gap-3",
|
|
|
|
|
isCollapsed && "justify-center"
|
|
|
|
|
)}>
|
|
|
|
|
<span className={switcherIconClasses}>
|
|
|
|
|
<HotelIcon className="h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
</span>
|
|
|
|
|
{!isCollapsed && !isTextVisible && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="grow overflow-hidden">
|
|
|
|
|
<p className="truncate text-sm font-bold text-slate-700">{project.name}</p>
|
|
|
|
|
<p className="text-sm text-slate-500">{t("common.workspace")}</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} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent side="right" sideOffset={10} alignOffset={5} align="end">
|
|
|
|
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
|
|
|
|
<HotelIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
{t("common.change_workspace")}
|
|
|
|
|
</div>
|
|
|
|
|
{isLoadingProjects && (
|
|
|
|
|
<div className="flex items-center justify-center py-2">
|
|
|
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!isLoadingProjects && workspaceLoadError && (
|
|
|
|
|
<div className="px-2 py-4">
|
|
|
|
|
<p className="mb-2 text-sm text-red-600">{workspaceLoadError}</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceLoadError(null);
|
|
|
|
|
setProjects([]);
|
|
|
|
|
}}
|
|
|
|
|
className="text-xs text-slate-600 underline hover:text-slate-800">
|
|
|
|
|
{t("common.try_again")}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!isLoadingProjects && !workspaceLoadError && (
|
|
|
|
|
<>
|
|
|
|
|
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
|
|
|
|
|
{projects.map((proj) => (
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
key={proj.id}
|
|
|
|
|
checked={proj.id === project.id}
|
|
|
|
|
onClick={() => handleProjectChange(proj.id)}
|
|
|
|
|
className="cursor-pointer">
|
|
|
|
|
{proj.name}
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
))}
|
|
|
|
|
</DropdownMenuGroup>
|
|
|
|
|
{isOwnerOrManager && (
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
onClick={handleProjectCreate}
|
|
|
|
|
className="w-full cursor-pointer justify-between">
|
|
|
|
|
<span>{t("common.add_new_workspace")}</span>
|
|
|
|
|
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
<DropdownMenuGroup>
|
|
|
|
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
|
|
|
|
<Cog className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
{t("common.workspace_configuration")}
|
|
|
|
|
</div>
|
|
|
|
|
{projectSettings.map((setting) => (
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
key={setting.id}
|
|
|
|
|
checked={isActiveProjectSetting(pathname, setting.id)}
|
|
|
|
|
onClick={() => handleSettingNavigation(setting.href)}
|
|
|
|
|
className="cursor-pointer">
|
|
|
|
|
{setting.label}
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
))}
|
|
|
|
|
</DropdownMenuGroup>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
|
|
|
|
<DropdownMenu onOpenChange={setIsOrganizationDropdownOpen}>
|
|
|
|
|
<DropdownMenuTrigger
|
|
|
|
|
asChild
|
|
|
|
|
id="organizationDropdownTriggerSidebar"
|
|
|
|
|
className={switcherTriggerClasses}>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex w-full cursor-pointer items-center gap-3",
|
|
|
|
|
isCollapsed && "justify-center"
|
|
|
|
|
)}>
|
|
|
|
|
<span className={switcherIconClasses}>
|
|
|
|
|
<Building2Icon className="h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
</span>
|
|
|
|
|
{!isCollapsed && !isTextVisible && (
|
|
|
|
|
<>
|
|
|
|
|
<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} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</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>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
<DropdownMenuGroup>
|
|
|
|
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
|
|
|
|
<SettingsIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
|
|
|
|
{t("common.organization_settings")}
|
|
|
|
|
</div>
|
|
|
|
|
{organizationSettings.map((setting) => {
|
|
|
|
|
if (setting.hidden) return null;
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
key={setting.id}
|
|
|
|
|
checked={isActiveOrganizationSetting(pathname, setting.id)}
|
|
|
|
|
onClick={() => handleSettingNavigation(setting.href)}
|
|
|
|
|
className="cursor-pointer">
|
|
|
|
|
{setting.label}
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</DropdownMenuGroup>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger
|
|
|
|
|
asChild
|
|
|
|
|
id="userDropdownTrigger"
|
|
|
|
|
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
|
|
|
|
className={cn(switcherTriggerClasses, "rounded-br-xl")}>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex cursor-pointer flex-row items-center gap-3",
|
|
|
|
|
isCollapsed ? "justify-center px-2" : "px-4"
|
|
|
|
|
"flex w-full cursor-pointer items-center gap-3",
|
|
|
|
|
isCollapsed && "justify-center"
|
|
|
|
|
)}>
|
|
|
|
|
<ProfileAvatar userId={user.id} />
|
|
|
|
|
<span className={switcherIconClasses}>
|
|
|
|
|
<ProfileAvatar userId={user.id} />
|
|
|
|
|
</span>
|
|
|
|
|
{!isCollapsed && !isTextVisible && (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(isTextVisible ? "opacity-0" : "opacity-100", "grow overflow-hidden")}>
|
|
|
|
|
<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 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 className="text-sm text-slate-700">{t("common.account")}</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} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
@@ -303,8 +729,6 @@ export const MainNavigation = ({
|
|
|
|
|
sideOffset={10}
|
|
|
|
|
alignOffset={5}
|
|
|
|
|
align="end">
|
|
|
|
|
{/* Dropdown Items */}
|
|
|
|
|
|
|
|
|
|
{dropdownNavigation.map((link) => (
|
|
|
|
|
<Link
|
|
|
|
|
href={link.href}
|
|
|
|
|
@@ -318,7 +742,6 @@ export const MainNavigation = ({
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
{/* Logout */}
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const loginUrl = `${publicDomain}/auth/login`;
|
|
|
|
|
@@ -341,6 +764,28 @@ export const MainNavigation = ({
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
)}
|
|
|
|
|
{openProjectLimitModal && (
|
|
|
|
|
<ProjectLimitModal
|
|
|
|
|
open={openProjectLimitModal}
|
|
|
|
|
setOpen={setOpenProjectLimitModal}
|
|
|
|
|
buttons={projectLimitModalButtons()}
|
|
|
|
|
projectLimit={organizationProjectsLimit}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{openCreateProjectModal && (
|
|
|
|
|
<CreateProjectModal
|
|
|
|
|
open={openCreateProjectModal}
|
|
|
|
|
setOpen={setOpenCreateProjectModal}
|
|
|
|
|
organizationId={organization.id}
|
|
|
|
|
isAccessControlAllowed={isAccessControlAllowed}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{openCreateOrganizationModal && (
|
|
|
|
|
<CreateOrganizationModal
|
|
|
|
|
open={openCreateOrganizationModal}
|
|
|
|
|
setOpen={setOpenCreateOrganizationModal}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|