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",
"babel-plugin-relay": "^18.2.0",
"esbuild": "^0.25.0",
"eslint": "^9.21.0",
"relay-compiler": "^18.2.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,7 @@
import { useState, useEffect } from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import { graphql, useFragment } from "react-relay";
import { useOrganization } from "@/contexts/OrganizationContext";
import { Link } from "react-router";
import { Link, useNavigate, useParams } from "react-router";
import {
DropdownMenu,
@@ -54,20 +53,22 @@ export function TeamSwitcher({
}) {
const { isMobile } = useSidebar();
const data = useFragment(teamSwitcherFragment, organizations);
const { currentOrganization, setCurrentOrganization } = useOrganization();
const navigate = useNavigate();
const { organizationId } = useParams();
const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null);
useEffect(() => {
console.log(data.organizations);
if (
data.organizations &&
data.organizations.edges.length > 0 &&
!currentOrganization
) {
// Type assertion to include plan field
setCurrentOrganization(data.organizations.edges[0].node);
if (data.organizations && data.organizations.edges.length > 0) {
// Find the current organization based on the URL parameter
const org = data.organizations.edges.find(
(edge) => edge.node.id === organizationId
);
if (org) {
setCurrentOrganization(org.node);
}
}
}, [data.organizations, currentOrganization, setCurrentOrganization]);
}, [data.organizations, organizationId]);
// If no organizations or data is still loading
if (!currentOrganization) {
@@ -88,6 +89,11 @@ export function TeamSwitcher({
return null;
};
const handleOrganizationSwitch = (org: Organization) => {
// Navigate to the selected organization
navigate(`/organizations/${org.id}`);
};
return (
<SidebarMenu>
<SidebarMenuItem>
@@ -121,7 +127,7 @@ export function TeamSwitcher({
{data.organizations.edges.map((edge, index) => (
<DropdownMenuItem
key={edge.node.id}
onClick={() => setCurrentOrganization(edge.node)}
onClick={() => handleOrganizationSwitch(edge.node)}
className="gap-2 p-2"
>
<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 { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql";
import { ConsoleLayoutOrganizationQuery } from "./__generated__/ConsoleLayoutOrganizationQuery.graphql";
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 (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/">Home</Link>
<Link to={`/organizations/${organizationId}`}>
{data.organization?.name || "Home"}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{children}
@@ -46,12 +64,15 @@ function BreadcrumbHome({ children }: { children: React.ReactNode }) {
}
function BreadcrumbFrameworkList() {
const { organizationId } = useParams();
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/frameworks">Frameworks</Link>
<Link to={`/organizations/${organizationId}/frameworks`}>
Frameworks
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
@@ -60,7 +81,7 @@ function BreadcrumbFrameworkList() {
}
function BreadcrumbFrameworkOverview() {
const { frameworkId } = useParams();
const { organizationId, frameworkId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbFrameworkOverviewQuery($frameworkId: ID!) {
@@ -81,7 +102,9 @@ function BreadcrumbFrameworkOverview() {
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}>
<Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`}
>
{data.framework.name}
</Link>
</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() {
const { organizationId } = useParams();
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/vendors">Vendors</Link>
<Link to={`/organizations/${organizationId}/vendors`}>Vendors</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
@@ -164,7 +130,7 @@ function BreadcrumbVendorList() {
}
function BreadcrumbVendorOverview() {
const { vendorId } = useParams();
const { organizationId, vendorId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbVendorOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbVendorOverviewQuery($vendorId: ID!) {
@@ -185,15 +151,91 @@ function BreadcrumbVendorOverview() {
<BreadcrumbSeparator />
<BreadcrumbItem>
<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>
</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() {
const { frameworkId, controlId } = useParams();
const { organizationId, frameworkId, controlId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbControlOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbControlOverviewQuery(
@@ -223,7 +265,9 @@ function BreadcrumbControlOverview() {
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}>
<Link
to={`/organizations/${organizationId}/frameworks/${data.framework.id}`}
>
{data.framework.name}
</Link>
</BreadcrumbLink>
@@ -232,7 +276,7 @@ function BreadcrumbControlOverview() {
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
to={`/frameworks/${data.framework.id}/controls/${data.control.id}`}
to={`/organizations/${organizationId}/frameworks/${data.framework.id}/controls/${data.control.id}`}
>
{data.control.name}
</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() {
const { organizationId } = useParams();
return (
<SidebarProvider>
<AppSidebar />
@@ -268,36 +300,29 @@ export default function ConsoleLayout() {
<Separator orientation="vertical" className="mr-2 h-4" />
<BreadcrumbHome>
<Routes>
<Route path="/frameworks" element={<BreadcrumbFrameworkList />}>
<Route path="frameworks" element={<BreadcrumbFrameworkList />}>
<Route
path="/frameworks/:frameworkId"
path=":frameworkId"
element={<BreadcrumbFrameworkOverview />}
/>
<Route
path="/frameworks/:frameworkId/controls/:controlId"
path=":frameworkId/controls/:controlId"
element={<BreadcrumbControlOverview />}
/>
</Route>
<Route path="/peoples" element={<BreadcrumbPeopleList />}>
<Route path="peoples" element={<BreadcrumbPeopleList />}>
<Route
path="/peoples/:peopleId"
path=":peopleId"
element={<BreadcrumbPeopleOverview />}
/>
<Route
path="/peoples/create"
element={<BreadcrumbCreatePeople />}
/>
<Route path="create" element={<BreadcrumbCreatePeople />} />
</Route>
<Route path="/vendors" element={<BreadcrumbVendorList />}>
<Route path="vendors" element={<BreadcrumbVendorList />}>
<Route
path="/vendors/:vendorId"
path=":vendorId"
element={<BreadcrumbVendorOverview />}
/>
</Route>
<Route
path="/organizations/create"
element={<BreadcrumbCreateOrganization />}
/>
</Routes>
</BreadcrumbHome>
</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";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { useOrganization } from "@/contexts/OrganizationContext";
import {
Card,
CardHeader,
@@ -84,7 +83,7 @@ function EditableField({
export default function CreateOrganizationPage() {
const navigate = useNavigate();
const { setCurrentOrganization } = useOrganization();
const { toast } = useToast();
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
viewerQuery,
{}
@@ -93,7 +92,6 @@ export default function CreateOrganizationPage() {
useMutation<CreateOrganizationPageCreateOrganizationMutation>(
createOrganizationMutation
);
const { toast } = useToast();
const [formData, setFormData] = useState({
name: "",
});
@@ -122,13 +120,12 @@ export default function CreateOrganizationPage() {
},
onCompleted: (response) => {
const newOrg = response.createOrganization.organizationEdge.node;
setCurrentOrganization(newOrg);
toast({
title: "Success",
description: "Organization created successfully",
variant: "default",
});
navigate("/");
navigate(`/organizations/${newOrg.id}`);
},
onError: (error) => {
toast({

View File

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

View File

@@ -6,10 +6,9 @@ import {
useQueryLoader,
} from "react-relay";
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 { Helmet } from "react-helmet-async";
import { useOrganization } from "@/contexts/OrganizationContext";
const FrameworkListPageQuery = graphql`
query FrameworkListPageQuery($organizationId: ID!) {
@@ -86,6 +85,7 @@ function FrameworkListPageContent({
queryRef: PreloadedQuery<FrameworkListPageQueryType>;
}) {
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef);
const { organizationId } = useParams();
const frameworks =
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">
{frameworks.map((framework) => {
const validatedControls = framework.controls.edges.filter(
(edge) => edge?.node?.state === "IMPLEMENTED",
(edge) => edge?.node?.state === "IMPLEMENTED"
).length;
const totalControls = framework.controls.edges.length;
return (
<Link key={framework.id} to={`/frameworks/${framework.id}`}>
<Link
key={framework.id}
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
>
<FrameworkCard
title={framework.name}
description={framework.description}
@@ -161,14 +164,14 @@ function FrameworkListPageFallback() {
export default function FrameworkListPage() {
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
FrameworkListPageQuery,
FrameworkListPageQuery
);
const { currentOrganization } = useOrganization();
const { organizationId } = useParams();
useEffect(() => {
loadQuery({ organizationId: currentOrganization!.id });
}, [loadQuery, currentOrganization]);
loadQuery({ organizationId: organizationId! });
}, [loadQuery, organizationId]);
return (
<>

View File

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

View File

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

View File

@@ -22,16 +22,6 @@ export default function LoginPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) {
toast({
title: "Error",
description: "Email and password are required",
variant: "destructive",
});
return;
}
setIsLoading(true);
try {
@@ -40,24 +30,30 @@ export default function LoginPage() {
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ email, password }),
credentials: "include",
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Login failed");
const error = await response.json();
throw new Error(error.message || "Failed to login");
}
// Check authentication status after login
await checkAuth();
// Redirect to the original page or home after successful login
navigate(from, { replace: true });
} catch (error) {
const authenticated = await checkAuth();
if (authenticated) {
toast({
title: "Success",
description: "Logged in successfully",
variant: "default",
});
navigate("/");
} else {
throw new Error("Authentication failed");
}
} catch (error: any) {
toast({
title: "Error",
description: error instanceof Error ? error.message : "Login failed",
description: error.message || "Failed to login",
variant: "destructive",
});
} finally {
@@ -66,58 +62,86 @@ export default function LoginPage() {
};
return (
<>
<Helmet>
<title>Log in - Probo</title>
</Helmet>
<div className="space-y-6">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Log in</h1>
<p className="text-gray-500 dark:text-gray-400">
Enter your credentials to access your account
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<div className="flex flex-col space-y-6">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Login to your account
</h1>
<p className="text-sm text-muted-foreground">
Enter your email below to login to your account
</p>
</div>
<div className="grid gap-6">
<form onSubmit={handleSubmit}>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
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 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>
<div className="text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Don't have an account?{" "}
<Link to="/register" className="underline">
Sign up here
</Link>
</p>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-muted/40 px-2 text-muted-foreground">
Or continue with
</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 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,
usePaginationFragment,
} from "react-relay";
import { useSearchParams } from "react-router";
import { useSearchParams, useParams } from "react-router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
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 { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25;
@@ -141,6 +140,7 @@ function PeopleListContent({
const [isPending, startTransition] = useTransition();
const [deletePeople] =
useMutation<PeopleListPageDeletePeopleMutation>(deletePeopleMutation);
const { organizationId } = useParams();
const {
data: peoplesConnection,
@@ -177,7 +177,7 @@ function PeopleListContent({
style={{ borderRadius: "0.5rem" }}
className="gap-2"
>
<Link to="/peoples/create">
<Link to={`/organizations/${organizationId}/peoples/create`}>
<UserPlus className="h-4 w-4" />
Add a people
</Link>
@@ -188,7 +188,7 @@ function PeopleListContent({
{peoples.map((person) => (
<Link
key={person?.id}
to={`/peoples/${person?.id}`}
to={`/organizations/${organizationId}/peoples/${person?.id}`}
className="block"
>
<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] =
useQueryLoader<PeopleListPageQueryType>(peopleListPageQuery);
const { currentOrganization } = useOrganization();
const { organizationId } = useParams();
useEffect(() => {
const after = searchParams.get("after");
const before = searchParams.get("before");
loadQuery({
organizationId: currentOrganization!.id,
organizationId: organizationId!,
first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined,
});
}, [loadQuery, currentOrganization]);
}, [loadQuery, organizationId, searchParams]);
if (!queryRef) {
return <PeopleListPageFallback />;

View File

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

View File

@@ -7,7 +7,7 @@ import {
useMutation,
usePaginationFragment,
} from "react-relay";
import { useSearchParams } from "react-router";
import { useSearchParams, useParams } from "react-router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Store, ChevronRight, Trash2 } from "lucide-react";
@@ -22,7 +22,6 @@ import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPa
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
import { useToast } from "@/hooks/use-toast";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25;
@@ -199,6 +198,7 @@ function VendorListContent({
useMutation<VendorListPageCreateVendorMutation>(createVendorMutation);
const [deleteVendor] =
useMutation<VendorListPageDeleteVendorMutation>(deleteVendorMutation);
const { organizationId } = useParams();
const {
data: vendorsConnection,
@@ -300,7 +300,7 @@ function VendorListContent({
{vendors.map((vendor) => (
<Link
key={vendor?.id}
to={`/vendors/${vendor?.id}`}
to={`/organizations/${organizationId}/vendors/${vendor?.id}`}
className="block"
>
<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] =
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
const { currentOrganization } = useOrganization();
const { organizationId } = useParams();
useEffect(() => {
const after = searchParams.get("after");
const before = searchParams.get("before");
loadQuery({
organizationId: currentOrganization!.id,
organizationId: organizationId!,
first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined,
});
}, [loadQuery, currentOrganization]);
}, [loadQuery, organizationId, searchParams]);
if (!queryRef) {
return <VendorListFallback />;

View File

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

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