Migrate current organization from in memory to url state

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-26 12:02:51 +01:00
parent 50aeed787f
commit c6cff715d5
21 changed files with 2150 additions and 832 deletions

View File

@@ -53,6 +53,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"babel-plugin-relay": "^18.2.0", "babel-plugin-relay": "^18.2.0",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^9.21.0",
"relay-compiler": "^18.2.0", "relay-compiler": "^18.2.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3" "typescript": "^5.7.3"

View File

@@ -4,7 +4,13 @@ import { lazy, StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import { RelayEnvironmentProvider } from "react-relay"; import { RelayEnvironmentProvider } from "react-relay";
import { BrowserRouter, Route, Routes, useLocation } from "react-router"; import {
BrowserRouter,
Route,
Routes,
useLocation,
Navigate,
} from "react-router";
import "App.css"; import "App.css";
import ErrorBoundary from "./components/ErrorBoundary"; import ErrorBoundary from "./components/ErrorBoundary";
import ConsoleLayout from "./layouts/ConsoleLayout"; import ConsoleLayout from "./layouts/ConsoleLayout";
@@ -12,7 +18,6 @@ import AuthLayout from "./layouts/AuthLayout";
import { RelayEnvironment } from "./RelayEnvironment"; import { RelayEnvironment } from "./RelayEnvironment";
import { AuthProvider } from "./contexts/AuthContext"; import { AuthProvider } from "./contexts/AuthContext";
import { ProtectedRoute } from "./components/ProtectedRoute"; import { ProtectedRoute } from "./components/ProtectedRoute";
import { OrganizationProvider } from "./contexts/OrganizationContext";
posthog.init(process.env.POSTHOG_KEY!, { posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST, api_host: process.env.POSTHOG_HOST,
@@ -24,6 +29,9 @@ posthog.init(process.env.POSTHOG_KEY!, {
}, },
}); });
const OrganizationSelectionPage = lazy(
() => 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"));
const PeopleListPage = lazy(() => import("./pages/PeopleListPage")); const PeopleListPage = lazy(() => import("./pages/PeopleListPage"));
@@ -52,181 +60,194 @@ function App() {
<HelmetProvider> <HelmetProvider>
<BrowserRouter> <BrowserRouter>
<AuthProvider> <AuthProvider>
<OrganizationProvider> <Routes>
<Routes> {/* Authentication Routes - Accessible without login */}
{/* Authentication Routes - Accessible without login */} <Route
path="/login"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route <Route
path="/login" index
element={ element={
<ErrorBoundaryWithLocation> <Suspense>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<LoginPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
<Route
path="/register"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<RegisterPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
{/* Protected Routes - Require authentication */}
<Route element={<ProtectedRoute />}>
<Route
path="/*"
element={
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<ConsoleLayout /> <LoginPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
<Route
path="/register"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<RegisterPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
{/* Protected Routes - Require authentication */}
<Route element={<ProtectedRoute />}>
{/* Organization Selection Route */}
<Route
path="/"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<OrganizationSelectionPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
{/* Organization Creation Route */}
<Route
path="/organizations/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreateOrganizationPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
{/* Organization-specific Routes */}
<Route
path="/organizations/:organizationId/*"
element={
<ErrorBoundaryWithLocation>
<ConsoleLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<HomePage />
</ErrorBoundaryWithLocation>
</Suspense>
} }
> />
<Route <Route
index path="peoples"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<HomePage /> <PeopleListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="peoples" path="peoples/create"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PeopleListPage /> <CreatePeoplePage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="peoples/create" path="peoples/:peopleId"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<CreatePeoplePage /> <PeopleOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="peoples/:peopleId" path="vendors"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<PeopleOverviewPage /> <VendorListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="vendors" path="frameworks"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<VendorListPage /> <FrameworkListPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="frameworks" path="frameworks/:frameworkId"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<FrameworkListPage /> <FrameworkOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="frameworks/:frameworkId" path="frameworks/:frameworkId/controls/:controlId"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<FrameworkOverviewPage /> <ControlOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="frameworks/:frameworkId/controls/:controlId" path="vendors/:vendorId"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<ControlOverviewPage /> <VendorOverviewPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="vendors/:vendorId" path="settings"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>
<VendorOverviewPage /> <SettingsPage />
</ErrorBoundaryWithLocation> </ErrorBoundaryWithLocation>
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="settings" path="*"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <NotFoundPage />
<SettingsPage /> </Suspense>
</ErrorBoundaryWithLocation> }
</Suspense> />
}
/>
<Route
path="organizations/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreateOrganizationPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="*"
element={
<Suspense>
<NotFoundPage />
</Suspense>
}
/>
</Route>
</Route> </Route>
</Routes> </Route>
</OrganizationProvider> </Routes>
</AuthProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>
</HelmetProvider> </HelmetProvider>

View File

@@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Suspense, useEffect } from "react"; import { Suspense, useEffect } from "react";
import { useParams } from "react-router";
import { import {
BookOpen, BookOpen,
Users, Users,
@@ -31,55 +32,52 @@ import {
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
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";
import { url } from "inspector";
const staticData = { function getNavItems(organizationId: string) {
user: { return {
name: "shadcn", navMain: [
email: "m@example.com", {
avatar: "/avatars/shadcn.jpg", title: "Frameworks",
}, url: `/organizations/${organizationId}/frameworks`,
navMain: [ icon: BookOpen,
{ },
title: "Frameworks", {
url: "/frameworks", title: "Organizations",
icon: BookOpen, icon: Building,
}, url: `/organizations/${organizationId}/peoples`,
{ items: [
title: "Organizations", {
icon: Building, title: "Peoples",
url: "/peoples", url: `/organizations/${organizationId}/peoples`,
items: [ icon: Users,
{ },
title: "Peoples", {
url: "/peoples", title: "Vendors",
icon: Users, url: `/organizations/${organizationId}/vendors`,
}, icon: ToyBrick,
{ },
title: "Vendors", ],
url: "/vendors", },
icon: ToyBrick, {
}, title: "Settings",
], url: `/organizations/${organizationId}/settings`,
}, icon: Settings,
{ },
title: "Settings", ],
url: "/settings", navSecondary: [
icon: Settings, {
}, title: "Support",
], url: "#",
navSecondary: [ icon: LifeBuoy,
{ },
title: "Support", {
url: "#", title: "Feedback",
icon: LifeBuoy, url: "#",
}, icon: Send,
{ },
title: "Feedback", ],
url: "#", };
icon: Send, }
},
],
};
const AppSidebarQuery = graphql` const AppSidebarQuery = graphql`
query AppSidebarQuery { query AppSidebarQuery {
@@ -97,7 +95,9 @@ function AppSidebarContent({
}: React.ComponentProps<typeof Sidebar> & { }: React.ComponentProps<typeof Sidebar> & {
queryRef: PreloadedQuery<AppSidebarQueryType>; queryRef: PreloadedQuery<AppSidebarQueryType>;
}) { }) {
const { organizationId } = useParams();
const data = usePreloadedQuery(AppSidebarQuery, queryRef); const data = usePreloadedQuery(AppSidebarQuery, queryRef);
const navItems = getNavItems(organizationId!);
return ( return (
<Sidebar variant="inset" {...props}> <Sidebar variant="inset" {...props}>
@@ -105,8 +105,8 @@ function AppSidebarContent({
<TeamSwitcher organizations={data.viewer} /> <TeamSwitcher organizations={data.viewer} />
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={staticData.navMain} /> <NavMain items={navItems.navMain} />
<NavSecondary items={staticData.navSecondary} className="mt-auto" /> <NavSecondary items={navItems.navSecondary} className="mt-auto" />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavUser viewer={data.viewer} /> <NavUser viewer={data.viewer} />
@@ -121,39 +121,37 @@ function AppSidebarFallback(props: React.ComponentProps<typeof Sidebar>) {
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton size="lg" asChild> <SidebarMenuButton size="lg">
<a href="#"> <div className="h-8 w-8 rounded-lg bg-sidebar-muted" />
<div className="size-8 rounded-lg bg-muted animate-pulse" /> <div className="flex-1 space-y-1">
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="h-4 w-3/4 rounded-lg bg-sidebar-muted" />
<span className="truncate font-semibold"> <div className="h-3 w-1/2 rounded-lg bg-sidebar-muted" />
<div className="h-4 w-24 animate-pulse rounded bg-muted" /> </div>
</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</a>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={staticData.navMain} /> <SidebarMenu>
<NavSecondary items={staticData.navSecondary} className="mt-auto" /> {Array.from({ length: 3 }).map((_, i) => (
<SidebarMenuItem key={i}>
<SidebarMenuButton>
<div className="h-4 w-4 rounded-lg bg-sidebar-muted" />
<div className="h-4 w-24 rounded-lg bg-sidebar-muted" />
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton size="lg" asChild> <SidebarMenuButton size="lg">
<a href="#"> <div className="h-8 w-8 rounded-lg bg-sidebar-muted" />
<div className="h-8 w-8 rounded-lg bg-muted animate-pulse" /> <div className="flex-1 space-y-1">
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="h-4 w-3/4 rounded-lg bg-sidebar-muted" />
<span className="truncate font-semibold"> <div className="h-3 w-1/2 rounded-lg bg-sidebar-muted" />
<div className="h-4 w-24 animate-pulse rounded bg-muted" /> </div>
</span>
<span className="truncate text-xs">
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
</span>
</div>
</a>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>

View File

@@ -9,6 +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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
@@ -26,6 +27,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
useSidebar, useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useAuth } from "@/contexts/AuthContext";
import { import {
NavUser_viewer$key, NavUser_viewer$key,
NavUser_viewer$data, NavUser_viewer$data,
@@ -42,6 +44,13 @@ export const navUserFragment = graphql`
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) { export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const currentUser = useFragment(navUserFragment, viewer); const currentUser = useFragment(navUserFragment, viewer);
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate("/login");
};
return ( return (
<SidebarMenu> <SidebarMenu>
@@ -116,7 +125,7 @@ export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem onClick={handleLogout}>
<LogOut /> <LogOut />
Log out Log out
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -3,8 +3,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ChevronsUpDown, Plus } from "lucide-react"; import { ChevronsUpDown, Plus } from "lucide-react";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { useOrganization } from "@/contexts/OrganizationContext"; import { Link, useNavigate, useParams } from "react-router";
import { Link } from "react-router";
import { import {
DropdownMenu, DropdownMenu,
@@ -54,20 +53,22 @@ export function TeamSwitcher({
}) { }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const data = useFragment(teamSwitcherFragment, organizations); const data = useFragment(teamSwitcherFragment, organizations);
const { currentOrganization, setCurrentOrganization } = useOrganization(); const navigate = useNavigate();
const { organizationId } = useParams();
const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null);
useEffect(() => { useEffect(() => {
console.log(data.organizations); if (data.organizations && data.organizations.edges.length > 0) {
// Find the current organization based on the URL parameter
if ( const org = data.organizations.edges.find(
data.organizations && (edge) => edge.node.id === organizationId
data.organizations.edges.length > 0 && );
!currentOrganization if (org) {
) { setCurrentOrganization(org.node);
// Type assertion to include plan field }
setCurrentOrganization(data.organizations.edges[0].node);
} }
}, [data.organizations, currentOrganization, setCurrentOrganization]); }, [data.organizations, organizationId]);
// If no organizations or data is still loading // If no organizations or data is still loading
if (!currentOrganization) { if (!currentOrganization) {
@@ -88,6 +89,11 @@ export function TeamSwitcher({
return null; return null;
}; };
const handleOrganizationSwitch = (org: Organization) => {
// Navigate to the selected organization
navigate(`/organizations/${org.id}`);
};
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
@@ -121,7 +127,7 @@ export function TeamSwitcher({
{data.organizations.edges.map((edge, index) => ( {data.organizations.edges.map((edge, index) => (
<DropdownMenuItem <DropdownMenuItem
key={edge.node.id} key={edge.node.id}
onClick={() => setCurrentOrganization(edge.node)} onClick={() => handleOrganizationSwitch(edge.node)}
className="gap-2 p-2" className="gap-2 p-2"
> >
<div className="flex size-6 items-center justify-center rounded-sm border"> <div className="flex size-6 items-center justify-center rounded-sm border">

View File

@@ -1,43 +0,0 @@
import React, { createContext, useContext, useState, ReactNode } from "react";
interface Organization {
id: string;
name: string;
logoUrl: string;
}
interface OrganizationContextType {
currentOrganization: Organization | null;
setCurrentOrganization: (organization: Organization) => void;
}
const OrganizationContext = createContext<OrganizationContextType | undefined>(
undefined,
);
export function useOrganization() {
const context = useContext(OrganizationContext);
if (context === undefined) {
throw new Error(
"useOrganization must be used within an OrganizationProvider",
);
}
return context;
}
interface OrganizationProviderProps {
children: ReactNode;
}
export function OrganizationProvider({ children }: OrganizationProviderProps) {
const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null);
return (
<OrganizationContext.Provider
value={{ currentOrganization, setCurrentOrganization }}
>
{children}
</OrganizationContext.Provider>
);
}

View File

@@ -29,14 +29,32 @@ import { ConsoleLayoutBreadcrumbFrameworkOverviewQuery } from "./__generated__/C
import { ConsoleLayoutBreadcrumbPeopleOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbPeopleOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbPeopleOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbPeopleOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql"; import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql";
import { ConsoleLayoutOrganizationQuery } from "./__generated__/ConsoleLayoutOrganizationQuery.graphql";
function BreadcrumbHome({ children }: { children: React.ReactNode }) { function BreadcrumbHome({ children }: { children: React.ReactNode }) {
const { organizationId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutOrganizationQuery>(
graphql`
query ConsoleLayoutOrganizationQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
name
}
}
}
`,
{ organizationId: organizationId! },
{ fetchPolicy: "store-or-network" }
);
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to="/">Home</Link> <Link to={`/organizations/${organizationId}`}>
{data.organization?.name || "Home"}
</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
{children} {children}
@@ -46,12 +64,15 @@ function BreadcrumbHome({ children }: { children: React.ReactNode }) {
} }
function BreadcrumbFrameworkList() { function BreadcrumbFrameworkList() {
const { organizationId } = useParams();
return ( return (
<> <>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to="/frameworks">Frameworks</Link> <Link to={`/organizations/${organizationId}/frameworks`}>
Frameworks
</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<Outlet /> <Outlet />
@@ -60,7 +81,7 @@ function BreadcrumbFrameworkList() {
} }
function BreadcrumbFrameworkOverview() { function BreadcrumbFrameworkOverview() {
const { frameworkId } = useParams(); const { organizationId, frameworkId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>( const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>(
graphql` graphql`
query ConsoleLayoutBreadcrumbFrameworkOverviewQuery($frameworkId: ID!) { query ConsoleLayoutBreadcrumbFrameworkOverviewQuery($frameworkId: ID!) {
@@ -81,7 +102,9 @@ function BreadcrumbFrameworkOverview() {
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}> <Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`}
>
{data.framework.name} {data.framework.name}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
@@ -91,71 +114,14 @@ function BreadcrumbFrameworkOverview() {
); );
} }
function BreadcrumbPeopleList() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/peoples">People</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbCreatePeople() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/peoples/create">Create People</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbPeopleOverview() {
const { peopleId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbPeopleOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbPeopleOverviewQuery($peopleId: ID!) {
people: node(id: $peopleId) {
id
... on People {
fullName
}
}
}
`,
{ peopleId: peopleId! },
{ fetchPolicy: "store-or-network" }
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/peoples/${data.people.id}`}>{data.people.fullName}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbVendorList() { function BreadcrumbVendorList() {
const { organizationId } = useParams();
return ( return (
<> <>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to="/vendors">Vendors</Link> <Link to={`/organizations/${organizationId}/vendors`}>Vendors</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<Outlet /> <Outlet />
@@ -164,7 +130,7 @@ function BreadcrumbVendorList() {
} }
function BreadcrumbVendorOverview() { function BreadcrumbVendorOverview() {
const { vendorId } = useParams(); const { organizationId, vendorId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbVendorOverviewQuery>( const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbVendorOverviewQuery>(
graphql` graphql`
query ConsoleLayoutBreadcrumbVendorOverviewQuery($vendorId: ID!) { query ConsoleLayoutBreadcrumbVendorOverviewQuery($vendorId: ID!) {
@@ -185,15 +151,91 @@ function BreadcrumbVendorOverview() {
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to={`/vendors/${data.vendor.id}`}>{data.vendor.name}</Link> <Link
to={`/organizations/${organizationId}/vendors/${data.vendor.id}`}
>
{data.vendor.name}
</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbPeopleList() {
const { organizationId } = useParams();
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/organizations/${organizationId}/peoples`}>People</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbCreatePeople() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create</BreadcrumbPage>
</BreadcrumbItem>
</>
);
}
function BreadcrumbPeopleOverview() {
const { organizationId, peopleId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbPeopleOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbPeopleOverviewQuery($peopleId: ID!) {
people: node(id: $peopleId) {
id
... on People {
fullName
}
}
}
`,
{ peopleId: peopleId! },
{ fetchPolicy: "store-or-network" }
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
to={`/organizations/${organizationId}/peoples/${data.people.id}`}
>
{data.people.fullName}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbCreateOrganization() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create Organization</BreadcrumbPage>
</BreadcrumbItem>
</> </>
); );
} }
function BreadcrumbControlOverview() { function BreadcrumbControlOverview() {
const { frameworkId, controlId } = useParams(); const { organizationId, frameworkId, controlId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbControlOverviewQuery>( const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbControlOverviewQuery>(
graphql` graphql`
query ConsoleLayoutBreadcrumbControlOverviewQuery( query ConsoleLayoutBreadcrumbControlOverviewQuery(
@@ -223,7 +265,9 @@ function BreadcrumbControlOverview() {
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}> <Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`}
>
{data.framework.name} {data.framework.name}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
@@ -232,7 +276,7 @@ function BreadcrumbControlOverview() {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
to={`/frameworks/${data.framework.id}/controls/${data.control.id}`} to={`/organizations/${organizationId}/frameworks/${data.framework.id}/controls/${data.control.id}`}
> >
{data.control.name} {data.control.name}
</Link> </Link>
@@ -243,21 +287,9 @@ function BreadcrumbControlOverview() {
); );
} }
function BreadcrumbCreateOrganization() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/organizations/create">Create Organization</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
export default function ConsoleLayout() { export default function ConsoleLayout() {
const { organizationId } = useParams();
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
@@ -268,36 +300,29 @@ export default function ConsoleLayout() {
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<BreadcrumbHome> <BreadcrumbHome>
<Routes> <Routes>
<Route path="/frameworks" element={<BreadcrumbFrameworkList />}> <Route path="frameworks" element={<BreadcrumbFrameworkList />}>
<Route <Route
path="/frameworks/:frameworkId" path=":frameworkId"
element={<BreadcrumbFrameworkOverview />} element={<BreadcrumbFrameworkOverview />}
/> />
<Route <Route
path="/frameworks/:frameworkId/controls/:controlId" path=":frameworkId/controls/:controlId"
element={<BreadcrumbControlOverview />} element={<BreadcrumbControlOverview />}
/> />
</Route> </Route>
<Route path="/peoples" element={<BreadcrumbPeopleList />}> <Route path="peoples" element={<BreadcrumbPeopleList />}>
<Route <Route
path="/peoples/:peopleId" path=":peopleId"
element={<BreadcrumbPeopleOverview />} element={<BreadcrumbPeopleOverview />}
/> />
<Route <Route path="create" element={<BreadcrumbCreatePeople />} />
path="/peoples/create"
element={<BreadcrumbCreatePeople />}
/>
</Route> </Route>
<Route path="/vendors" element={<BreadcrumbVendorList />}> <Route path="vendors" element={<BreadcrumbVendorList />}>
<Route <Route
path="/vendors/:vendorId" path=":vendorId"
element={<BreadcrumbVendorOverview />} element={<BreadcrumbVendorOverview />}
/> />
</Route> </Route>
<Route
path="/organizations/create"
element={<BreadcrumbCreateOrganization />}
/>
</Routes> </Routes>
</BreadcrumbHome> </BreadcrumbHome>
</div> </div>

View File

@@ -0,0 +1,124 @@
/**
* @generated SignedSource<<93800eee21fa3f30cc45da37370359fd>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ConsoleLayoutOrganizationQuery$variables = {
organizationId: string;
};
export type ConsoleLayoutOrganizationQuery$data = {
readonly organization: {
readonly name?: string;
};
};
export type ConsoleLayoutOrganizationQuery = {
response: ConsoleLayoutOrganizationQuery$data;
variables: ConsoleLayoutOrganizationQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ConsoleLayoutOrganizationQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ConsoleLayoutOrganizationQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "e275faa366380b1b423251e6a06ab743",
"id": null,
"metadata": {},
"name": "ConsoleLayoutOrganizationQuery",
"operationKind": "query",
"text": "query ConsoleLayoutOrganizationQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n name\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "3310c40eb8f1e3874139c934fb08cbb1";
export default node;

View File

@@ -8,7 +8,6 @@ import {
} from "react-relay"; } from "react-relay";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useOrganization } from "@/contexts/OrganizationContext";
import { import {
Card, Card,
CardHeader, CardHeader,
@@ -84,7 +83,7 @@ function EditableField({
export default function CreateOrganizationPage() { export default function CreateOrganizationPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setCurrentOrganization } = useOrganization(); const { toast } = useToast();
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>( const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
viewerQuery, viewerQuery,
{} {}
@@ -93,7 +92,6 @@ export default function CreateOrganizationPage() {
useMutation<CreateOrganizationPageCreateOrganizationMutation>( useMutation<CreateOrganizationPageCreateOrganizationMutation>(
createOrganizationMutation createOrganizationMutation
); );
const { toast } = useToast();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
}); });
@@ -122,13 +120,12 @@ export default function CreateOrganizationPage() {
}, },
onCompleted: (response) => { onCompleted: (response) => {
const newOrg = response.createOrganization.organizationEdge.node; const newOrg = response.createOrganization.organizationEdge.node;
setCurrentOrganization(newOrg);
toast({ toast({
title: "Success", title: "Success",
description: "Organization created successfully", description: "Organization created successfully",
variant: "default", variant: "default",
}); });
navigate("/"); navigate(`/organizations/${newOrg.id}`);
}, },
onError: (error) => { onError: (error) => {
toast({ toast({

View File

@@ -1,5 +1,5 @@
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate, useParams } from "react-router";
import { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
@@ -18,7 +18,6 @@ import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql"; import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
import { useOrganization } from "@/contexts/OrganizationContext";
const createPeopleMutation = graphql` const createPeopleMutation = graphql`
mutation CreatePeoplePageCreatePeopleMutation( mutation CreatePeoplePageCreatePeopleMutation(
@@ -75,7 +74,7 @@ function EditableField({
function CreatePeoplePageContent() { function CreatePeoplePageContent() {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrganization } = useOrganization(); const { organizationId } = useParams();
const [createPeople] = const [createPeople] =
useMutation<CreatePeoplePageCreatePeopleMutation>(createPeopleMutation); useMutation<CreatePeoplePageCreatePeopleMutation>(createPeopleMutation);
@@ -97,7 +96,7 @@ function CreatePeoplePageContent() {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const peopleConnectionId = ConnectionHandler.getConnectionID( const peopleConnectionId = ConnectionHandler.getConnectionID(
currentOrganization!.id, organizationId!,
"PeopleListPage_peoples" "PeopleListPage_peoples"
); );
@@ -105,7 +104,7 @@ function CreatePeoplePageContent() {
variables: { variables: {
connections: [peopleConnectionId], connections: [peopleConnectionId],
input: { input: {
organizationId: currentOrganization!.id, organizationId: organizationId!,
fullName: formData.fullName, fullName: formData.fullName,
primaryEmailAddress: formData.primaryEmailAddress, primaryEmailAddress: formData.primaryEmailAddress,
additionalEmailAddresses: formData.additionalEmailAddresses, additionalEmailAddresses: formData.additionalEmailAddresses,

View File

@@ -6,10 +6,9 @@ import {
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Link } from "react-router"; import { Link, useParams } from "react-router";
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql"; import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useOrganization } from "@/contexts/OrganizationContext";
const FrameworkListPageQuery = graphql` const FrameworkListPageQuery = graphql`
query FrameworkListPageQuery($organizationId: ID!) { query FrameworkListPageQuery($organizationId: ID!) {
@@ -86,6 +85,7 @@ function FrameworkListPageContent({
queryRef: PreloadedQuery<FrameworkListPageQueryType>; queryRef: PreloadedQuery<FrameworkListPageQueryType>;
}) { }) {
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef); const data = usePreloadedQuery(FrameworkListPageQuery, queryRef);
const { organizationId } = useParams();
const frameworks = const frameworks =
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? []; data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
@@ -101,12 +101,15 @@ function FrameworkListPageContent({
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
{frameworks.map((framework) => { {frameworks.map((framework) => {
const validatedControls = framework.controls.edges.filter( const validatedControls = framework.controls.edges.filter(
(edge) => edge?.node?.state === "IMPLEMENTED", (edge) => edge?.node?.state === "IMPLEMENTED"
).length; ).length;
const totalControls = framework.controls.edges.length; const totalControls = framework.controls.edges.length;
return ( return (
<Link key={framework.id} to={`/frameworks/${framework.id}`}> <Link
key={framework.id}
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
>
<FrameworkCard <FrameworkCard
title={framework.name} title={framework.name}
description={framework.description} description={framework.description}
@@ -161,14 +164,14 @@ function FrameworkListPageFallback() {
export default function FrameworkListPage() { export default function FrameworkListPage() {
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
FrameworkListPageQuery, FrameworkListPageQuery
); );
const { currentOrganization } = useOrganization(); const { organizationId } = useParams();
useEffect(() => { useEffect(() => {
loadQuery({ organizationId: currentOrganization!.id }); loadQuery({ organizationId: organizationId! });
}, [loadQuery, currentOrganization]); }, [loadQuery, organizationId]);
return ( return (
<> <>

View File

@@ -54,19 +54,17 @@ function FrameworkOverviewPageContent({
} | null>(null); } | null>(null);
const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isTooltipHovered, setIsTooltipHovered] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { organizationId } = useParams();
// Group controls by their category // Group controls by their category
const controlsByCategory = controls.reduce( const controlsByCategory = controls.reduce((acc, control) => {
(acc, control) => { if (!control?.category) return acc;
if (!control?.category) return acc; if (!acc[control.category]) {
if (!acc[control.category]) { acc[control.category] = [];
acc[control.category] = []; }
} acc[control.category].push(control);
acc[control.category].push(control); return acc;
return acc; }, {} as Record<string, typeof controls>);
},
{} as Record<string, typeof controls>,
);
const controlCards = Object.entries(controlsByCategory).map( const controlCards = Object.entries(controlsByCategory).map(
([category, controls]) => ({ ([category, controls]) => ({
@@ -74,11 +72,11 @@ function FrameworkOverviewPageContent({
controls, controls,
completed: controls.filter((c) => c?.state === "IMPLEMENTED").length, completed: controls.filter((c) => c?.state === "IMPLEMENTED").length,
total: controls.length, total: controls.length,
}), })
); );
const totalImplemented = controls.filter( const totalImplemented = controls.filter(
(c) => c?.state === "IMPLEMENTED", (c) => c?.state === "IMPLEMENTED"
).length; ).length;
return ( return (
@@ -190,7 +188,7 @@ function FrameworkOverviewPageContent({
onClick={() => { onClick={() => {
if (control?.id) { if (control?.id) {
navigate( navigate(
`/frameworks/${framework.id}/controls/${control.id}`, `/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}`
); );
} }
}} }}
@@ -274,7 +272,7 @@ function FrameworkOverviewPageContent({
</div> </div>
</div> </div>
</div>, </div>,
document.body, document.body
)} )}
</div> </div>
); );
@@ -308,7 +306,7 @@ function FrameworkOverviewPageFallback() {
export default function FrameworkOverviewPage() { export default function FrameworkOverviewPage() {
const { frameworkId } = useParams(); const { frameworkId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
FrameworkOverviewPageQuery, FrameworkOverviewPageQuery
); );
useEffect(() => { useEffect(() => {

View File

@@ -5,116 +5,112 @@ import {
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { Link } from "react-router"; import { Link, useParams } from "react-router";
import type { HomePageQuery as HomePageQueryType } from "./__generated__/HomePageQuery.graphql"; import type { HomePageQuery as HomePageQueryType } from "./__generated__/HomePageQuery.graphql";
export const HomePageQuery = graphql` export const HomePageQuery = graphql`
query HomePageQuery { query HomePageQuery($organizationId: ID!) {
viewer { organization: node(id: $organizationId) {
id ... on Organization {
organizations { id
edges { name
node { createdAt
name updatedAt
createdAt vendors {
updatedAt edges {
vendors { node {
edges { id
node { name
id createdAt
name updatedAt
createdAt
updatedAt
}
}
} }
peoples { }
edges { }
node { peoples {
id edges {
fullName node {
primaryEmailAddress id
additionalEmailAddresses fullName
createdAt primaryEmailAddress
updatedAt additionalEmailAddresses
} createdAt
} updatedAt
} }
frameworks { }
pageInfo { }
hasNextPage frameworks {
hasPreviousPage pageInfo {
startCursor hasNextPage
endCursor hasPreviousPage
} startCursor
edges { endCursor
cursor }
node { edges {
id cursor
name node {
description id
controls { name
edges { description
node { controls {
id edges {
name node {
state id
stateTransisions { name
edges { state
node { stateTransisions {
id edges {
toState node {
fromState id
createdAt toState
updatedAt fromState
} createdAt
} updatedAt
} }
tasks { }
edges { }
node { tasks {
id edges {
name node {
state id
evidences { name
edges { state
node { evidences {
id edges {
state node {
fileUrl id
stateTransisions { state
edges { fileUrl
node { stateTransisions {
id edges {
fromState node {
toState id
reason fromState
createdAt toState
updatedAt reason
} createdAt
} updatedAt
} }
createdAt
updatedAt
} }
} }
createdAt
updatedAt
} }
stateTransisions {
edges {
node {
id
toState
fromState
reason
createdAt
updatedAt
}
}
}
createdAt
updatedAt
} }
} }
stateTransisions {
edges {
node {
id
toState
fromState
reason
createdAt
updatedAt
}
}
}
createdAt
updatedAt
} }
} }
} }
@@ -130,10 +126,15 @@ export const HomePageQuery = graphql`
`; `;
export default function HomePage() { export default function HomePage() {
const { organizationId } = useParams();
const [queryRef, loadQuery] = const [queryRef, loadQuery] =
useQueryLoader<HomePageQueryType>(HomePageQuery); useQueryLoader<HomePageQueryType>(HomePageQuery);
useEffect(() => loadQuery({}), [loadQuery]); useEffect(() => {
if (organizationId) {
loadQuery({ organizationId });
}
}, [organizationId, loadQuery]);
if (!queryRef) { if (!queryRef) {
return <HomePageFallback />; return <HomePageFallback />;
@@ -155,6 +156,7 @@ function HomePageContent({
}: { }: {
queryRef: PreloadedQuery<HomePageQueryType>; queryRef: PreloadedQuery<HomePageQueryType>;
}) { }) {
const { organizationId } = useParams();
const data = usePreloadedQuery(HomePageQuery, queryRef); const data = usePreloadedQuery(HomePageQuery, queryRef);
return ( return (
@@ -166,7 +168,9 @@ function HomePageContent({
</div> </div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"> <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min">
<pre>{JSON.stringify(data, null, 2)}</pre> <pre>{JSON.stringify(data, null, 2)}</pre>
<Link to="/foobar">Go to FooPage</Link> <Link to={`/organizations/${organizationId}/frameworks`}>
Go to Frameworks
</Link>
</div> </div>
</div> </div>
); );

View File

@@ -22,16 +22,6 @@ export default function LoginPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!email || !password) {
toast({
title: "Error",
description: "Email and password are required",
variant: "destructive",
});
return;
}
setIsLoading(true); setIsLoading(true);
try { try {
@@ -40,24 +30,30 @@ export default function LoginPage() {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
credentials: "include",
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
credentials: "include",
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const error = await response.json();
throw new Error(errorData.message || "Login failed"); throw new Error(error.message || "Failed to login");
} }
// Check authentication status after login const authenticated = await checkAuth();
await checkAuth(); if (authenticated) {
toast({
// Redirect to the original page or home after successful login title: "Success",
navigate(from, { replace: true }); description: "Logged in successfully",
} catch (error) { variant: "default",
});
navigate("/");
} else {
throw new Error("Authentication failed");
}
} catch (error: any) {
toast({ toast({
title: "Error", title: "Error",
description: error instanceof Error ? error.message : "Login failed", description: error.message || "Failed to login",
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
@@ -66,58 +62,86 @@ export default function LoginPage() {
}; };
return ( return (
<> <div className="flex flex-col space-y-6">
<Helmet> <div className="flex flex-col space-y-2 text-center">
<title>Log in - Probo</title> <h1 className="text-2xl font-semibold tracking-tight">
</Helmet> Login to your account
</h1>
<div className="space-y-6"> <p className="text-sm text-muted-foreground">
<div className="space-y-2 text-center"> Enter your email below to login to your account
<h1 className="text-3xl font-bold">Log in</h1> </p>
<p className="text-gray-500 dark:text-gray-400"> </div>
Enter your credentials to access your account <div className="grid gap-6">
</p> <form onSubmit={handleSubmit}>
</div> <div className="grid gap-4">
<div className="grid gap-2">
<form onSubmit={handleSubmit} className="space-y-4"> <Label htmlFor="email">Email</Label>
<div className="space-y-2"> <Input
<Label htmlFor="email">Email</Label> id="email"
<Input placeholder="name@example.com"
id="email" type="email"
type="email" autoCapitalize="none"
placeholder="name@example.com" autoComplete="email"
value={email} autoCorrect="off"
onChange={(e) => setEmail(e.target.value)} disabled={isLoading}
required value={email}
/> onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
to="/forgot-password"
className="text-sm font-medium text-primary underline-offset-4 hover:underline"
>
Forgot password?
</Link>
</div>
<Input
id="password"
type="password"
autoCapitalize="none"
autoComplete="current-password"
autoCorrect="off"
disabled={isLoading}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button disabled={isLoading}>
{isLoading ? "Logging in..." : "Login"}
</Button>
</div> </div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Logging in..." : "Log in with email"}
</Button>
</form> </form>
<div className="relative">
<div className="text-center"> <div className="absolute inset-0 flex items-center">
<p className="text-sm text-gray-500 dark:text-gray-400"> <span className="w-full border-t" />
Don't have an account?{" "} </div>
<Link to="/register" className="underline"> <div className="relative flex justify-center text-xs uppercase">
Sign up here <span className="bg-muted/40 px-2 text-muted-foreground">
</Link> Or continue with
</p> </span>
</div>
</div>
<div className="flex flex-col space-y-2">
<Button variant="outline" type="button" disabled={isLoading}>
GitHub
</Button>
<Button variant="outline" type="button" disabled={isLoading}>
Google
</Button>
</div> </div>
</div> </div>
</> <div className="text-center text-sm">
Don&apos;t have an account?{" "}
<Link
to="/register"
className="font-medium text-primary underline-offset-4 hover:underline"
>
Register
</Link>
</div>
</div>
); );
} }

View File

@@ -0,0 +1,136 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { graphql, useLazyLoadQuery } from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Plus } from "lucide-react";
import { OrganizationSelectionPageQuery } from "./__generated__/OrganizationSelectionPageQuery.graphql";
const organizationSelectionQuery = graphql`
query OrganizationSelectionPageQuery {
viewer {
id
organizations(first: 25) {
edges {
node {
id
name
logoUrl
}
}
}
}
}
`;
export default function OrganizationSelectionPage() {
const navigate = useNavigate();
const { toast } = useToast();
const data = useLazyLoadQuery<OrganizationSelectionPageQuery>(
organizationSelectionQuery,
{}
);
const organizations = data.viewer.organizations.edges.map(
(edge) => edge.node
);
useEffect(() => {
// If there's only one organization, redirect to it automatically
if (organizations.length === 1) {
navigate(`/organizations/${organizations[0].id}`);
}
}, [organizations, navigate]);
// If no organizations, show a message to create one
if (organizations.length === 0) {
return (
<div className="container mx-auto py-10">
<Helmet>
<title>Select Organization - Probo</title>
</Helmet>
<div className="flex flex-col items-center justify-center h-[70vh]">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Welcome to Probo</CardTitle>
<CardDescription>
You don't have any organizations yet. Create your first one to
get started.
</CardDescription>
</CardHeader>
<CardFooter>
<Button
className="w-full"
onClick={() => navigate("/organizations/create")}
>
<Plus className="mr-2 h-4 w-4" /> Create Organization
</Button>
</CardFooter>
</Card>
</div>
</div>
);
}
// If multiple organizations, show a selection screen
return (
<div className="container mx-auto py-10">
<Helmet>
<title>Select Organization - Probo</title>
</Helmet>
<div className="flex flex-col items-center">
<h1 className="text-2xl font-bold mb-6">Select an Organization</h1>
<div className="grid gap-4 w-full max-w-2xl">
{organizations.map((org) => (
<Card key={org.id} className="w-full">
<CardHeader className="flex flex-row items-center gap-4">
{org.logoUrl && (
<div className="flex h-12 w-12 items-center justify-center rounded-lg border">
<img src={org.logoUrl} alt={org.name} className="h-8 w-8" />
</div>
)}
<div>
<CardTitle>{org.name}</CardTitle>
</div>
</CardHeader>
<CardFooter>
<Button
className="w-full"
onClick={() => navigate(`/organizations/${org.id}`)}
>
Select
</Button>
</CardFooter>
</Card>
))}
<Card className="w-full">
<CardHeader>
<CardTitle>Create a New Organization</CardTitle>
<CardDescription>
Add a new organization to your account
</CardDescription>
</CardHeader>
<CardFooter>
<Button
variant="outline"
className="w-full"
onClick={() => navigate("/organizations/create")}
>
<Plus className="mr-2 h-4 w-4" /> Create Organization
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -7,7 +7,7 @@ import {
useMutation, useMutation,
usePaginationFragment, usePaginationFragment,
} from "react-relay"; } from "react-relay";
import { useSearchParams } from "react-router"; import { useSearchParams, useParams } from "react-router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { UserPlus, Trash2, ChevronRight } from "lucide-react"; import { UserPlus, Trash2, ChevronRight } from "lucide-react";
@@ -18,7 +18,6 @@ import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generat
import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql"; import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql"; import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql"; import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25; const ITEMS_PER_PAGE = 25;
@@ -141,6 +140,7 @@ function PeopleListContent({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [deletePeople] = const [deletePeople] =
useMutation<PeopleListPageDeletePeopleMutation>(deletePeopleMutation); useMutation<PeopleListPageDeletePeopleMutation>(deletePeopleMutation);
const { organizationId } = useParams();
const { const {
data: peoplesConnection, data: peoplesConnection,
@@ -177,7 +177,7 @@ function PeopleListContent({
style={{ borderRadius: "0.5rem" }} style={{ borderRadius: "0.5rem" }}
className="gap-2" className="gap-2"
> >
<Link to="/peoples/create"> <Link to={`/organizations/${organizationId}/peoples/create`}>
<UserPlus className="h-4 w-4" /> <UserPlus className="h-4 w-4" />
Add a people Add a people
</Link> </Link>
@@ -188,7 +188,7 @@ function PeopleListContent({
{peoples.map((person) => ( {peoples.map((person) => (
<Link <Link
key={person?.id} key={person?.id}
to={`/peoples/${person?.id}`} to={`/organizations/${organizationId}/peoples/${person?.id}`}
className="block" className="block"
> >
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors"> <div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
@@ -307,20 +307,20 @@ export default function PeopleListPage() {
const [queryRef, loadQuery] = const [queryRef, loadQuery] =
useQueryLoader<PeopleListPageQueryType>(peopleListPageQuery); useQueryLoader<PeopleListPageQueryType>(peopleListPageQuery);
const { currentOrganization } = useOrganization(); const { organizationId } = useParams();
useEffect(() => { useEffect(() => {
const after = searchParams.get("after"); const after = searchParams.get("after");
const before = searchParams.get("before"); const before = searchParams.get("before");
loadQuery({ loadQuery({
organizationId: currentOrganization!.id, organizationId: organizationId!,
first: before ? undefined : ITEMS_PER_PAGE, first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined, after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined, last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined, before: before || undefined,
}); });
}, [loadQuery, currentOrganization]); }, [loadQuery, organizationId, searchParams]);
if (!queryRef) { if (!queryRef) {
return <PeopleListPageFallback />; return <PeopleListPageFallback />;

View File

@@ -22,8 +22,8 @@ import {
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay"; } from "react-relay";
import { useParams } from "react-router";
import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql"; import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql";
import { useOrganization } from "@/contexts/OrganizationContext";
const settingsPageQuery = graphql` const settingsPageQuery = graphql`
query SettingsPageQuery($organizationID: ID!) { query SettingsPageQuery($organizationID: ID!) {
@@ -217,11 +217,11 @@ export default function SettingsPage() {
const [queryRef, loadQuery] = const [queryRef, loadQuery] =
useQueryLoader<SettingsPageQueryType>(settingsPageQuery); useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
const { currentOrganization } = useOrganization(); const { organizationId } = useParams();
useEffect(() => { useEffect(() => {
loadQuery({ organizationID: currentOrganization!.id }); loadQuery({ organizationID: organizationId! });
}, [loadQuery, currentOrganization]); }, [loadQuery, organizationId]);
if (!queryRef) { if (!queryRef) {
return <SettingsPageFallback />; return <SettingsPageFallback />;

View File

@@ -7,7 +7,7 @@ import {
useMutation, useMutation,
usePaginationFragment, usePaginationFragment,
} from "react-relay"; } from "react-relay";
import { useSearchParams } from "react-router"; import { useSearchParams, useParams } from "react-router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Store, ChevronRight, Trash2 } from "lucide-react"; import { Store, ChevronRight, Trash2 } from "lucide-react";
@@ -22,7 +22,6 @@ import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPa
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql"; import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql"; import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25; const ITEMS_PER_PAGE = 25;
@@ -199,6 +198,7 @@ function VendorListContent({
useMutation<VendorListPageCreateVendorMutation>(createVendorMutation); useMutation<VendorListPageCreateVendorMutation>(createVendorMutation);
const [deleteVendor] = const [deleteVendor] =
useMutation<VendorListPageDeleteVendorMutation>(deleteVendorMutation); useMutation<VendorListPageDeleteVendorMutation>(deleteVendorMutation);
const { organizationId } = useParams();
const { const {
data: vendorsConnection, data: vendorsConnection,
@@ -300,7 +300,7 @@ function VendorListContent({
{vendors.map((vendor) => ( {vendors.map((vendor) => (
<Link <Link
key={vendor?.id} key={vendor?.id}
to={`/vendors/${vendor?.id}`} to={`/organizations/${organizationId}/vendors/${vendor?.id}`}
className="block" className="block"
> >
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors"> <div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
@@ -425,20 +425,20 @@ export default function VendorListPage() {
const [queryRef, loadQuery] = const [queryRef, loadQuery] =
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery); useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
const { currentOrganization } = useOrganization(); const { organizationId } = useParams();
useEffect(() => { useEffect(() => {
const after = searchParams.get("after"); const after = searchParams.get("after");
const before = searchParams.get("before"); const before = searchParams.get("before");
loadQuery({ loadQuery({
organizationId: currentOrganization!.id, organizationId: organizationId!,
first: before ? undefined : ITEMS_PER_PAGE, first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined, after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined, last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined, before: before || undefined,
}); });
}, [loadQuery, currentOrganization]); }, [loadQuery, organizationId, searchParams]);
if (!queryRef) { if (!queryRef) {
return <VendorListFallback />; return <VendorListFallback />;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<f40d67f614a103849ba940fbd14f71af>> * @generated SignedSource<<72fcf5e9542bdba3b547fa523e55378d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -12,73 +12,52 @@ import { ConcreteRequest } from 'relay-runtime';
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID"; export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type TaskState = "DONE" | "TODO"; export type TaskState = "DONE" | "TODO";
export type HomePageQuery$variables = Record<PropertyKey, never>; export type HomePageQuery$variables = {
organizationId: string;
};
export type HomePageQuery$data = { export type HomePageQuery$data = {
readonly viewer: { readonly organization: {
readonly id: string; readonly createdAt?: any;
readonly organizations: { readonly frameworks?: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly cursor: any;
readonly node: { readonly node: {
readonly createdAt: any; readonly controls: {
readonly frameworks: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly cursor: any;
readonly node: { readonly node: {
readonly controls: { readonly id: string;
readonly name: string;
readonly state: ControlState;
readonly stateTransisions: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly createdAt: any;
readonly fromState: ControlState | null | undefined;
readonly id: string; readonly id: string;
readonly name: string; readonly toState: ControlState;
readonly state: ControlState; readonly updatedAt: any;
readonly stateTransisions: { };
}>;
};
readonly tasks: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly evidences: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly createdAt: any; readonly createdAt: any;
readonly fromState: ControlState | null | undefined; readonly fileUrl: string;
readonly id: string; readonly id: string;
readonly toState: ControlState; readonly state: EvidenceState;
readonly updatedAt: any;
};
}>;
};
readonly tasks: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly evidences: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly fileUrl: string;
readonly id: string;
readonly state: EvidenceState;
readonly stateTransisions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly fromState: EvidenceState | null | undefined;
readonly id: string;
readonly reason: string | null | undefined;
readonly toState: EvidenceState;
readonly updatedAt: any;
};
}>;
};
readonly updatedAt: any;
};
}>;
};
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly stateTransisions: { readonly stateTransisions: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly createdAt: any; readonly createdAt: any;
readonly fromState: TaskState | null | undefined; readonly fromState: EvidenceState | null | undefined;
readonly id: string; readonly id: string;
readonly reason: string | null | undefined; readonly reason: string | null | undefined;
readonly toState: TaskState; readonly toState: EvidenceState;
readonly updatedAt: any; readonly updatedAt: any;
}; };
}>; }>;
@@ -87,45 +66,62 @@ export type HomePageQuery$data = {
}; };
}>; }>;
}; };
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly stateTransisions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly fromState: TaskState | null | undefined;
readonly id: string;
readonly reason: string | null | undefined;
readonly toState: TaskState;
readonly updatedAt: any;
};
}>;
};
readonly updatedAt: any;
}; };
}>; }>;
}; };
readonly description: string;
readonly id: string;
readonly name: string;
}; };
}>; }>;
readonly pageInfo: {
readonly endCursor: any | null | undefined;
readonly hasNextPage: boolean;
readonly hasPreviousPage: boolean;
readonly startCursor: any | null | undefined;
};
}; };
readonly description: string;
readonly id: string;
readonly name: string;
};
}>;
readonly pageInfo: {
readonly endCursor: any | null | undefined;
readonly hasNextPage: boolean;
readonly hasPreviousPage: boolean;
readonly startCursor: any | null | undefined;
};
};
readonly id?: string;
readonly name?: string;
readonly peoples?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly additionalEmailAddresses: ReadonlyArray<string>;
readonly createdAt: any;
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
readonly updatedAt: any;
};
}>;
};
readonly updatedAt?: any;
readonly vendors?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly id: string;
readonly name: string; readonly name: string;
readonly peoples: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly additionalEmailAddresses: ReadonlyArray<string>;
readonly createdAt: any;
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
readonly updatedAt: any;
};
}>;
};
readonly updatedAt: any; readonly updatedAt: any;
readonly vendors: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: any;
readonly id: string;
readonly name: string;
readonly updatedAt: any;
};
}>;
};
}; };
}>; }>;
}; };
@@ -137,35 +133,49 @@ export type HomePageQuery = {
}; };
const node: ConcreteRequest = (function(){ const node: ConcreteRequest = (function(){
var v0 = { var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
v1 = { v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "name", "name": "name",
"storageKey": null "storageKey": null
}, },
v2 = { v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
v3 = { v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "updatedAt", "name": "updatedAt",
"storageKey": null "storageKey": null
}, },
v4 = { v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "VendorConnection", "concreteType": "VendorConnection",
@@ -189,10 +199,10 @@ v4 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/) (v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -202,7 +212,7 @@ v4 = {
], ],
"storageKey": null "storageKey": null
}, },
v5 = { v7 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PeopleConnection", "concreteType": "PeopleConnection",
@@ -226,7 +236,7 @@ v5 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -248,8 +258,8 @@ v5 = {
"name": "additionalEmailAddresses", "name": "additionalEmailAddresses",
"storageKey": null "storageKey": null
}, },
(v2/*: any*/), (v4/*: any*/),
(v3/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -259,35 +269,35 @@ v5 = {
], ],
"storageKey": null "storageKey": null
}, },
v6 = { v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "state", "name": "state",
"storageKey": null "storageKey": null
}, },
v7 = { v9 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "toState", "name": "toState",
"storageKey": null "storageKey": null
}, },
v8 = { v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fromState", "name": "fromState",
"storageKey": null "storageKey": null
}, },
v9 = { v11 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "reason", "name": "reason",
"storageKey": null "storageKey": null
}, },
v10 = { v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "FrameworkConnection", "concreteType": "FrameworkConnection",
@@ -357,8 +367,8 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v1/*: any*/), (v3/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -390,9 +400,9 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v1/*: any*/), (v3/*: any*/),
(v6/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -417,11 +427,11 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/) (v9/*: any*/),
(v10/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -455,9 +465,9 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v1/*: any*/), (v3/*: any*/),
(v6/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -482,8 +492,8 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v6/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -515,12 +525,12 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v8/*: any*/),
(v7/*: any*/),
(v9/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/) (v10/*: any*/),
(v9/*: any*/),
(v11/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -530,8 +540,8 @@ v10 = {
], ],
"storageKey": null "storageKey": null
}, },
(v2/*: any*/), (v4/*: any*/),
(v3/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -565,12 +575,12 @@ v10 = {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/) (v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -580,8 +590,8 @@ v10 = {
], ],
"storageKey": null "storageKey": null
}, },
(v2/*: any*/), (v4/*: any*/),
(v3/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -611,58 +621,32 @@ v10 = {
}; };
return { return {
"fragment": { "fragment": {
"argumentDefinitions": [], "argumentDefinitions": (v0/*: any*/),
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "HomePageQuery", "name": "HomePageQuery",
"selections": [ "selections": [
{ {
"alias": null, "alias": "organization",
"args": null, "args": (v1/*: any*/),
"concreteType": "User", "concreteType": null,
"kind": "LinkedField", "kind": "LinkedField",
"name": "viewer", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
{ {
"alias": null, "kind": "InlineFragment",
"args": null,
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [ "selections": [
{ (v2/*: any*/),
"alias": null, (v3/*: any*/),
"args": null, (v4/*: any*/),
"concreteType": "OrganizationEdge", (v5/*: any*/),
"kind": "LinkedField", (v6/*: any*/),
"name": "edges", (v7/*: any*/),
"plural": true, (v12/*: any*/)
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v10/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
], ],
"storageKey": null "type": "Organization",
"abstractKey": null
} }
], ],
"storageKey": null "storageKey": null
@@ -673,58 +657,38 @@ return {
}, },
"kind": "Request", "kind": "Request",
"operation": { "operation": {
"argumentDefinitions": [], "argumentDefinitions": (v0/*: any*/),
"kind": "Operation", "kind": "Operation",
"name": "HomePageQuery", "name": "HomePageQuery",
"selections": [ "selections": [
{ {
"alias": null, "alias": "organization",
"args": null, "args": (v1/*: any*/),
"concreteType": "User", "concreteType": null,
"kind": "LinkedField", "kind": "LinkedField",
"name": "viewer", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "OrganizationConnection", "kind": "ScalarField",
"kind": "LinkedField", "name": "__typename",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v10/*: any*/),
(v0/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null "storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v12/*: any*/)
],
"type": "Organization",
"abstractKey": null
} }
], ],
"storageKey": null "storageKey": null
@@ -732,16 +696,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "5a56fefd066703ca0ebac1ef3e9a7e1f", "cacheID": "29440183dcc30ec5e0bcce9c1910b92f",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "HomePageQuery", "name": "HomePageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query HomePageQuery {\n viewer {\n id\n organizations {\n edges {\n node {\n name\n createdAt\n updatedAt\n vendors {\n edges {\n node {\n id\n name\n createdAt\n updatedAt\n }\n }\n }\n peoples {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n createdAt\n updatedAt\n }\n }\n }\n frameworks {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n name\n state\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n createdAt\n updatedAt\n }\n }\n }\n tasks {\n edges {\n node {\n id\n name\n state\n evidences {\n edges {\n node {\n id\n state\n fileUrl\n stateTransisions {\n edges {\n node {\n id\n fromState\n toState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n }\n }\n }\n }\n id\n }\n }\n }\n }\n}\n" "text": "query HomePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n createdAt\n updatedAt\n vendors {\n edges {\n node {\n id\n name\n createdAt\n updatedAt\n }\n }\n }\n peoples {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n createdAt\n updatedAt\n }\n }\n }\n frameworks {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n name\n state\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n createdAt\n updatedAt\n }\n }\n }\n tasks {\n edges {\n node {\n id\n name\n state\n evidences {\n edges {\n node {\n id\n state\n fileUrl\n stateTransisions {\n edges {\n node {\n id\n fromState\n toState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "7a1699535af738f939d954a88c6daa9e"; (node as any).hash = "d4f516b436d096fe9c91ce532a75b022";
export default node; export default node;

View File

@@ -0,0 +1,138 @@
/**
* @generated SignedSource<<59c4e8be77fdb5ebcc76c2a611f38052>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type OrganizationSelectionPageQuery$variables = Record<PropertyKey, never>;
export type OrganizationSelectionPageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organizations: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string;
readonly name: string;
};
}>;
};
};
};
export type OrganizationSelectionPageQuery = {
response: OrganizationSelectionPageQuery$data;
variables: OrganizationSelectionPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = [
{
"alias": null,
"args": null,
"concreteType": "User",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 25
}
],
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "organizations(first:25)"
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationSelectionPageQuery",
"selections": (v1/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "OrganizationSelectionPageQuery",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "634e8f413a8b1f2685cfc1f4fd3c13cc",
"id": null,
"metadata": {},
"name": "OrganizationSelectionPageQuery",
"operationKind": "query",
"text": "query OrganizationSelectionPageQuery {\n viewer {\n id\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "786037ac4c8d61f3b7cf6b4536aadbbb";
export default node;

914
package-lock.json generated

File diff suppressed because it is too large Load Diff