@@ -30,7 +30,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql";
|
||||
|
||||
import { TeamSwitcher } from "@/components/TeamSwitcher";
|
||||
const staticData = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
@@ -84,12 +84,8 @@ const AppSidebarQuery = graphql`
|
||||
query AppSidebarQuery {
|
||||
viewer {
|
||||
id
|
||||
organization {
|
||||
name
|
||||
logoUrl
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
...TeamSwitcher_organizations
|
||||
...NavUser_viewer
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -101,37 +97,18 @@ function AppSidebarContent({
|
||||
queryRef: PreloadedQuery<AppSidebarQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery(AppSidebarQuery, queryRef);
|
||||
const organization = data.viewer.organization;
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<img
|
||||
src={organization.logoUrl}
|
||||
alt={organization.name}
|
||||
className="size-8 rounded-lg object-cover"
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{organization.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">Enterprise</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<TeamSwitcher organizations={data.viewer} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={staticData.navMain} />
|
||||
<NavSecondary items={staticData.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={staticData.user} />
|
||||
<NavUser viewer={data.viewer} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
@@ -162,7 +139,23 @@ function AppSidebarFallback(props: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavSecondary items={staticData.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={staticData.user} />
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<div className="h-8 w-8 rounded-lg bg-muted animate-pulse" />
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
@@ -25,17 +26,22 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
NavUser_viewer$key,
|
||||
NavUser_viewer$data,
|
||||
} from "./__generated__/NavUser_viewer.graphql";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
export const navUserFragment = graphql`
|
||||
fragment NavUser_viewer on User {
|
||||
id
|
||||
fullName
|
||||
email
|
||||
}
|
||||
`;
|
||||
|
||||
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
|
||||
const { isMobile } = useSidebar();
|
||||
const currentUser = useFragment(navUserFragment, viewer);
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@@ -47,12 +53,18 @@ export function NavUser({
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{currentUser.fullName.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{currentUser.fullName.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-semibold">
|
||||
{currentUser.fullName}
|
||||
</span>
|
||||
<span className="truncate text-xs">{currentUser.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@@ -66,12 +78,18 @@ export function NavUser({
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{currentUser.fullName.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{currentUser.fullName.substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-semibold">
|
||||
{currentUser.fullName}
|
||||
</span>
|
||||
<span className="truncate text-xs">{currentUser.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
139
apps/console/src/components/TeamSwitcher.tsx
Normal file
139
apps/console/src/components/TeamSwitcher.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
import { useOrganization } from "@/contexts/OrganizationContext";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
TeamSwitcher_organizations$key,
|
||||
TeamSwitcher_organizations$data,
|
||||
} from "./__generated__/TeamSwitcher_organizations.graphql";
|
||||
|
||||
export const teamSwitcherFragment = graphql`
|
||||
fragment TeamSwitcher_organizations on User {
|
||||
organizations {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Extended type to include plan field until Relay compiler generates the types
|
||||
type Organization = TeamSwitcher_organizations$data["organizations"][0] & {
|
||||
plan?: string;
|
||||
};
|
||||
|
||||
export function TeamSwitcher({
|
||||
organizations,
|
||||
}: {
|
||||
organizations: TeamSwitcher_organizations$key;
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
const data = useFragment(teamSwitcherFragment, organizations);
|
||||
const { currentOrganization, setCurrentOrganization } = useOrganization();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data.organizations &&
|
||||
data.organizations.length > 0 &&
|
||||
!currentOrganization
|
||||
) {
|
||||
// Type assertion to include plan field
|
||||
setCurrentOrganization(data.organizations[0] as Organization);
|
||||
}
|
||||
}, [data.organizations, currentOrganization, setCurrentOrganization]);
|
||||
|
||||
// If no organizations or data is still loading
|
||||
if (!currentOrganization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert logoUrl to a React component
|
||||
const LogoComponent = ({
|
||||
org,
|
||||
className,
|
||||
}: {
|
||||
org: Organization;
|
||||
className?: string;
|
||||
}) => {
|
||||
if (org.logoUrl) {
|
||||
return <img src={org.logoUrl} alt={org.name} className={className} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<LogoComponent org={currentOrganization} className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{currentOrganization.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">Free</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Organizations
|
||||
</DropdownMenuLabel>
|
||||
{data.organizations.map((org: Organization, index: number) => (
|
||||
<DropdownMenuItem
|
||||
key={org.id}
|
||||
onClick={() => setCurrentOrganization(org)}
|
||||
className="gap-2 p-2"
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||
<LogoComponent org={org} className="size-4 shrink-0" />
|
||||
</div>
|
||||
{org.name}
|
||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 p-2">
|
||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||
<Plus className="size-4" />
|
||||
</div>
|
||||
<div className="font-medium text-muted-foreground">
|
||||
Add organization
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<717a5c6c939c65b7e0790f84e2c41bcd>>
|
||||
* @generated SignedSource<<d060c85c907876f442133c643b917eee>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,16 +9,12 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type AppSidebarQuery$variables = Record<PropertyKey, never>;
|
||||
export type AppSidebarQuery$data = {
|
||||
readonly viewer: {
|
||||
readonly id: string;
|
||||
readonly organization: {
|
||||
readonly createdAt: any;
|
||||
readonly logoUrl: string;
|
||||
readonly name: string;
|
||||
readonly updatedAt: any;
|
||||
};
|
||||
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer" | "TeamSwitcher_organizations">;
|
||||
};
|
||||
};
|
||||
export type AppSidebarQuery = {
|
||||
@@ -33,34 +29,6 @@ var v0 = {
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v1 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -79,19 +47,14 @@ return {
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
"kind": "FragmentSpread",
|
||||
"name": "TeamSwitcher_organizations"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "NavUser_viewer"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -120,16 +83,40 @@ return {
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"name": "organizations",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v0/*: any*/)
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -137,16 +124,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "e50b9b44daf91d53b07f8912e6c2088f",
|
||||
"cacheID": "48137f101a20a5ce116cbf46c2a8cd63",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "AppSidebarQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query AppSidebarQuery {\n viewer {\n id\n organization {\n name\n logoUrl\n createdAt\n updatedAt\n id\n }\n }\n}\n"
|
||||
"text": "query AppSidebarQuery {\n viewer {\n id\n ...TeamSwitcher_organizations\n ...NavUser_viewer\n }\n}\n\nfragment NavUser_viewer on User {\n id\n fullName\n email\n}\n\nfragment TeamSwitcher_organizations on User {\n organizations {\n id\n name\n logoUrl\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d450e65862def2f7ebd6f4b8d0475be0";
|
||||
(node as any).hash = "d5e28d934b5913169dd822042f4eca07";
|
||||
|
||||
export default node;
|
||||
|
||||
58
apps/console/src/components/__generated__/NavUser_viewer.graphql.ts
generated
Normal file
58
apps/console/src/components/__generated__/NavUser_viewer.graphql.ts
generated
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @generated SignedSource<<0965f25c8dedb59a9316e4df4d773af6>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type NavUser_viewer$data = {
|
||||
readonly email: string;
|
||||
readonly fullName: string;
|
||||
readonly id: string;
|
||||
readonly " $fragmentType": "NavUser_viewer";
|
||||
};
|
||||
export type NavUser_viewer$key = {
|
||||
readonly " $data"?: NavUser_viewer$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "NavUser_viewer",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "User",
|
||||
"abstractKey": null
|
||||
};
|
||||
|
||||
(node as any).hash = "7ee85a272f543b514c5a6a3148fd1613";
|
||||
|
||||
export default node;
|
||||
71
apps/console/src/components/__generated__/TeamSwitcher_organizations.graphql.ts
generated
Normal file
71
apps/console/src/components/__generated__/TeamSwitcher_organizations.graphql.ts
generated
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @generated SignedSource<<8ecb9304d0a498d483d1a8c251f8c49a>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type TeamSwitcher_organizations$data = {
|
||||
readonly organizations: ReadonlyArray<{
|
||||
readonly id: string;
|
||||
readonly logoUrl: string;
|
||||
readonly name: string;
|
||||
}>;
|
||||
readonly " $fragmentType": "TeamSwitcher_organizations";
|
||||
};
|
||||
export type TeamSwitcher_organizations$key = {
|
||||
readonly " $data"?: TeamSwitcher_organizations$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"TeamSwitcher_organizations">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "TeamSwitcher_organizations",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organizations",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "User",
|
||||
"abstractKey": null
|
||||
};
|
||||
|
||||
(node as any).hash = "318f68b82b5b87500ec99ffcdd2929ab";
|
||||
|
||||
export default node;
|
||||
Reference in New Issue
Block a user