import { Link, Navigate, Outlet, useParams } from "react-router"; import { DropdownSeparator, IconArrowBoxLeft, IconBank, IconBook, IconCircleQuestionmark, IconClock, IconCrossLargeX, IconFire3, IconGroup1, IconInboxEmpty, IconPageTextLine, IconSettingsGear2, IconCheckmark1, IconStore, IconTodo, IconListStack, IconBox, IconShield, IconRotateCw, IconCircleProgress, Layout, SidebarItem, UserDropdown as UserDropdownRoot, UserDropdownItem, Skeleton, Dropdown, Button, DropdownItem, IconChevronGrabberVertical, IconPlusLarge, IconChevronDown, Avatar, IconPeopleAdd, Badge, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { graphql } from "relay-runtime"; import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; import type { MainLayout_OrganizationSelector_viewer$key } from "./__generated__/MainLayout_OrganizationSelector_viewer.graphql"; import { Suspense, useState } from "react"; import { useToast } from "@probo/ui"; import { ErrorBoundary } from "react-error-boundary"; import { PageError } from "/components/PageError"; import { buildEndpoint } from "/providers/RelayProviders"; const MainLayoutQuery = graphql` query MainLayoutQuery($organizationId: ID!) { viewer { id user { fullName email } ...MainLayout_OrganizationSelector_viewer } organization: node(id: $organizationId) { ... on Organization { id name logoUrl } } } `; const OrganizationSelectorFragment = graphql` fragment MainLayout_OrganizationSelector_viewer on Viewer @refetchable(queryName: "MainLayoutOrganizationSelectorPaginationQuery") @argumentDefinitions( first: { type: "Int", defaultValue: 25 } after: { type: "CursorKey" } ) { organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) @connection(key: "MainLayout_OrganizationSelector_organizations") { edges { node { id name logoUrl } } pageInfo { hasNextPage endCursor } } invitations(first: 1, filter: {onlyPending: true}) { totalCount } } `; /** * Site layout with a header and a sidebar */ export function MainLayout() { const { organizationId } = useParams(); const { __ } = useTranslate(); const prefix = `/organizations/${organizationId}`; if (!organizationId) { return ; } return ( }> }> > } sidebar={ } > ); } function UserDropdown({ organizationId }: { organizationId: string }) { const { __ } = useTranslate(); const { toast } = useToast(); const user = useLazyLoadQuery(MainLayoutQuery, { organizationId }).viewer .user; const handleLogout: React.MouseEventHandler = async ( e ) => { e.preventDefault(); fetch(buildEndpoint("/api/console/v1/auth/logout"), { method: "DELETE", headers: { "Content-Type": "application/json", }, body: JSON.stringify({}), }) .then(async (res) => { if (!res.ok) { const error = await res.json(); throw new Error(error.message || __("Failed to login")); } window.location.reload(); }) .catch((e) => { toast({ title: __("Error"), description: e.message as string, variant: "error", }); }); }; return ( ); } function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) { const data = useLazyLoadQuery(MainLayoutQuery, { organizationId }); return ; } function OrganizationSelector({ viewer, currentOrganization }: { viewer: MainLayout_OrganizationSelector_viewer$key; currentOrganization: MainLayoutQueryType["response"]["organization"]; }) { const [isLoadingMore, setIsLoadingMore] = useState(false); const { __ } = useTranslate(); const { data, loadNext, hasNext } = usePaginationFragment( OrganizationSelectorFragment, viewer ); const organizations = data.organizations.edges.map((edge) => edge.node); const pendingInvitationsCount = data.invitations.totalCount; const handleLoadMore = (e?: React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (hasNext && !isLoadingMore) { setIsLoadingMore(true); loadNext(25, { onComplete: () => setIsLoadingMore(false), }); } }; return ( {currentOrganization?.name || ""} } > {organizations.map((organization) => ( {organization.name} ))} {hasNext && ( { e.preventDefault(); e.stopPropagation(); }} className="mx-auto" icon={IconChevronDown} disabled={isLoadingMore} > {isLoadingMore ? __("Loading...") : __("Show More")} )} {pendingInvitationsCount > 0 && ( {__("Invitations")} {pendingInvitationsCount} )} {__("Add organization")} {pendingInvitationsCount > 0 && ( {pendingInvitationsCount} )} ); }