Allow organization creation on signup

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-26 14:48:08 +01:00
parent bdf4273dab
commit def17c6a1a
7 changed files with 214 additions and 64 deletions

View File

@@ -24,7 +24,7 @@ posthog.init(process.env.POSTHOG_KEY!, {
}); });
const OrganizationSelectionPage = lazy( const OrganizationSelectionPage = lazy(
() => import("./pages/OrganizationSelectionPage") () => import("./pages/OrganizationSelectionPage"),
); );
const HomePage = lazy(() => import("./pages/HomePage")); const HomePage = lazy(() => import("./pages/HomePage"));
const NotFoundPage = lazy(() => import("./pages/NotFoundPage")); const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
@@ -35,14 +35,14 @@ const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage")); const SettingsPage = lazy(() => import("./pages/SettingsPage"));
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage")); const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
const FrameworkOverviewPage = lazy( const FrameworkOverviewPage = lazy(
() => import("./pages/FrameworkOverviewPage") () => import("./pages/FrameworkOverviewPage"),
); );
const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage")); const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage"));
const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage")); const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage"));
const LoginPage = lazy(() => import("./pages/LoginPage")); const LoginPage = lazy(() => import("./pages/LoginPage"));
const RegisterPage = lazy(() => import("./pages/RegisterPage")); const RegisterPage = lazy(() => import("./pages/RegisterPage"));
const CreateOrganizationPage = lazy( const CreateOrganizationPage = lazy(
() => import("./pages/CreateOrganizationPage") () => import("./pages/CreateOrganizationPage"),
); );
function App() { function App() {

View File

@@ -32,19 +32,25 @@ import {
import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql"; import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql";
import { TeamSwitcher } from "@/components/TeamSwitcher"; import { TeamSwitcher } from "@/components/TeamSwitcher";
function getNavItems(organizationId: string) { function getNavItems(organizationId?: string) {
// Always return the same structure, but with or without URLs depending on whether an organization is selected
return { return {
navMain: [ navMain: [
{ {
title: "Frameworks", title: "Frameworks",
url: `/organizations/${organizationId}/frameworks`, url: organizationId
? `/organizations/${organizationId}/frameworks`
: undefined,
icon: BookOpen, icon: BookOpen,
}, },
{ {
title: "Organizations", title: "Organizations",
icon: Building, icon: Building,
url: `/organizations/${organizationId}/peoples`, url: organizationId
items: [ ? `/organizations/${organizationId}/peoples`
: undefined,
items: organizationId
? [
{ {
title: "Peoples", title: "Peoples",
url: `/organizations/${organizationId}/peoples`, url: `/organizations/${organizationId}/peoples`,
@@ -55,11 +61,14 @@ function getNavItems(organizationId: string) {
url: `/organizations/${organizationId}/vendors`, url: `/organizations/${organizationId}/vendors`,
icon: ToyBrick, icon: ToyBrick,
}, },
], ]
: [],
}, },
{ {
title: "Settings", title: "Settings",
url: `/organizations/${organizationId}/settings`, url: organizationId
? `/organizations/${organizationId}/settings`
: undefined,
icon: Settings, icon: Settings,
}, },
], ],
@@ -96,7 +105,7 @@ function AppSidebarContent({
}) { }) {
const { organizationId } = useParams(); const { organizationId } = useParams();
const data = usePreloadedQuery(AppSidebarQuery, queryRef); const data = usePreloadedQuery(AppSidebarQuery, queryRef);
const navItems = getNavItems(organizationId!); const navItems = getNavItems(organizationId);
return ( return (
<Sidebar variant="inset" {...props}> <Sidebar variant="inset" {...props}>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { ChevronRight, type LucideIcon } from "lucide-react"; import { ChevronRight, type LucideIcon } from "lucide-react";
import { Link, useLocation } from "react-router"; import { Link, useLocation, useParams } from "react-router";
import { import {
Collapsible, Collapsible,
@@ -35,15 +35,62 @@ export function NavMain({
}[]; }[];
}) { }) {
const location = useLocation(); const location = useLocation();
const { organizationId } = useParams();
const noOrganizationSelected = organizationId === undefined;
if (noOrganizationSelected) {
return (
<SidebarGroup>
<SidebarGroupLabel>
<div className="h-4 w-28 rounded-md bg-gray-200 animate-pulse" />
</SidebarGroupLabel>
<SidebarMenu className="space-y-1.5">
{/* First item - simple item */}
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-gray-200" />
<div className="h-4 w-32 rounded-md bg-gray-200 ml-2" />
</SidebarMenuButton>
</SidebarMenuItem>
{/* Second item - with dropdown */}
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-gray-200" />
<div className="h-4 w-28 rounded-md bg-gray-200 ml-2" />
<div className="ml-auto">
<div className="h-4 w-4 rounded-md bg-gray-200" />
</div>
</SidebarMenuButton>
<SidebarMenuSub>
{[1, 2].map((i) => (
<SidebarMenuSubItem key={i}>
<SidebarMenuSubButton className="animate-pulse pl-8">
<div className="h-3 w-20 rounded-md bg-gray-200" />
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</SidebarMenuItem>
{/* Third item - simple item */}
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-gray-200" />
<div className="h-4 w-24 rounded-md bg-gray-200 ml-2" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}
// Determine if an item is active based on the current route
const isItemActive = (item: { url?: string; items?: { url: string }[] }) => { const isItemActive = (item: { url?: string; items?: { url: string }[] }) => {
// If the item has a URL and it matches the current path
if (item.url && location.pathname.startsWith(item.url)) { if (item.url && location.pathname.startsWith(item.url)) {
return true; return true;
} }
// If the item has sub-items, check if any of them match the current path
if (item.items?.length) { if (item.items?.length) {
return item.items.some((subItem) => return item.items.some((subItem) =>
location.pathname.startsWith(subItem.url), location.pathname.startsWith(subItem.url),

View File

@@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { type LucideIcon } from "lucide-react"; import { type LucideIcon } from "lucide-react";
import { useLocation } from "react-router";
import { import {
SidebarGroup, SidebarGroup,
@@ -19,17 +20,53 @@ export function NavSecondary({
icon: LucideIcon; icon: LucideIcon;
}[]; }[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) { } & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
const location = useLocation();
const noOrganizationSelected = location.pathname === "/";
if (noOrganizationSelected) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{[1, 2].map((i) => (
<SidebarMenuItem key={i}>
<SidebarMenuButton size="sm" className="animate-pulse">
<div className="h-3 w-3 rounded-lg bg-gray-200" />
<div className="h-3 w-16 rounded-lg bg-gray-200 ml-2" />
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}
return ( return (
<SidebarGroup {...props}> <SidebarGroup {...props}>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm"> <SidebarMenuButton
asChild={item.url !== "#"}
size="sm"
tooltip={item.title}
className={
item.url === "#" ? "opacity-50 cursor-not-allowed" : ""
}
>
{item.url !== "#" ? (
<a href={item.url}> <a href={item.url}>
<item.icon /> <item.icon />
<span>{item.title}</span> <span>{item.title}</span>
</a> </a>
) : (
<>
<item.icon />
<span>{item.title}</span>
</>
)}
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}

View File

@@ -9,7 +9,7 @@ import {
Sparkles, Sparkles,
} from "lucide-react"; } from "lucide-react";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate, useParams } from "react-router";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { import {
@@ -43,12 +43,31 @@ export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
const currentUser = useFragment(navUserFragment, viewer); const currentUser = useFragment(navUserFragment, viewer);
const { logout } = useAuth(); const { logout } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { organizationId } = useParams();
const noOrganizationSelected = !organizationId;
const handleLogout = async () => { const handleLogout = async () => {
await logout(); await logout();
navigate("/login"); navigate("/login");
}; };
if (noOrganizationSelected) {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="animate-pulse">
<div className="h-8 w-8 rounded-lg bg-gray-200" />
<div className="flex-1 space-y-1">
<div className="h-4 w-3/4 rounded-lg bg-gray-200" />
<div className="h-3 w-1/2 rounded-lg bg-gray-200" />
</div>
<div className="ml-auto h-4 w-4 rounded-lg bg-gray-200" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
}
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ChevronsUpDown, Plus } from "lucide-react"; import { ChevronsUpDown, Plus, Building } from "lucide-react";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { Link, useNavigate, useParams } from "react-router"; import { Link, useNavigate, useParams } from "react-router";
@@ -67,26 +67,41 @@ export function TeamSwitcher({
const { organizationId } = useParams(); const { organizationId } = useParams();
const [currentOrganization, setCurrentOrganization] = const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null); useState<Organization | null>(null);
const hasOrganizations =
data.organizations && data.organizations.edges.length > 0;
useEffect(() => { useEffect(() => {
if (data.organizations && data.organizations.edges.length > 0) { if (hasOrganizations) {
const org = data.organizations.edges.find( const org = data.organizations.edges.find(
(edge) => edge.node.id === organizationId (edge) => edge.node.id === organizationId,
); );
if (org) { if (org) {
setCurrentOrganization(org.node); setCurrentOrganization(org.node);
} }
} }
}, [data.organizations, organizationId]); }, [data.organizations, organizationId, hasOrganizations]);
if (!currentOrganization) {
return null;
}
const handleOrganizationSwitch = (org: Organization) => { const handleOrganizationSwitch = (org: Organization) => {
navigate(`/organizations/${org.id}`); navigate(`/organizations/${org.id}`);
}; };
if (!hasOrganizations) {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="animate-pulse">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-gray-200" />
<div className="flex-1 space-y-1">
<div className="h-4 w-3/4 rounded-lg bg-gray-200" />
<div className="h-3 w-1/2 rounded-lg bg-gray-200" />
</div>
<div className="ml-auto h-4 w-4 rounded-lg bg-gray-200" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
}
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
@@ -94,18 +109,42 @@ export function TeamSwitcher({
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<SidebarMenuButton <SidebarMenuButton
size="lg" size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" className={`data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground ${
!currentOrganization
? "border border-dashed border-gray-400"
: ""
}`}
> >
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
{currentOrganization ? (
<LogoComponent org={currentOrganization} className="size-4" /> <LogoComponent org={currentOrganization} className="size-4" />
) : (
<Building className="size-4 text-gray-400" />
)}
</div> </div>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold"> <span
{currentOrganization.name} className={`truncate font-semibold ${
!currentOrganization ? "text-gray-500" : ""
}`}
>
{currentOrganization
? currentOrganization.name
: "Select Organization"}
</span>
<span
className={`truncate text-xs ${
!currentOrganization ? "text-gray-400" : ""
}`}
>
{currentOrganization ? "Free" : "No organization selected"}
</span> </span>
<span className="truncate text-xs">Free</span>
</div> </div>
<ChevronsUpDown className="ml-auto" /> <ChevronsUpDown
className={`ml-auto ${
!currentOrganization ? "text-gray-400" : ""
}`}
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
@@ -117,14 +156,20 @@ export function TeamSwitcher({
<DropdownMenuLabel className="text-xs text-muted-foreground"> <DropdownMenuLabel className="text-xs text-muted-foreground">
Organizations Organizations
</DropdownMenuLabel> </DropdownMenuLabel>
{data.organizations.edges.map((edge, index) => ( {hasOrganizations &&
data.organizations.edges.map((edge, index) => (
<DropdownMenuItem <DropdownMenuItem
key={edge.node.id} key={edge.node.id}
onClick={() => handleOrganizationSwitch(edge.node)} onClick={() => handleOrganizationSwitch(edge.node)}
className="gap-2 p-2" className={`gap-2 p-2 ${
edge.node.id === organizationId ? "bg-muted" : ""
}`}
> >
<div className="flex size-6 items-center justify-center rounded-sm border"> <div className="flex size-6 items-center justify-center rounded-sm border">
<LogoComponent org={edge.node} className="size-4 shrink-0" /> <LogoComponent
org={edge.node}
className="size-4 shrink-0"
/>
</div> </div>
{edge.node.name} {edge.node.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut> <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>

View File

@@ -77,13 +77,6 @@ func RegisterHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.Handler
return return
} }
organizationID, _ := gid.ParseGID("AZSfP_xAcAC5IAAAAAAltA")
err = usrmgrSvc.AddUserToOrganization(r.Context(), user.ID, organizationID)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to add user to organization: %v", err), http.StatusInternalServerError)
return
}
// Log the user in // Log the user in
session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password) session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password)
if err != nil { if err != nil {