Fix breadcrumb make UI blink

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-20 08:06:28 +01:00
parent 5ebe7a685b
commit 81b01f06a5
12 changed files with 812 additions and 177 deletions

View File

@@ -123,7 +123,7 @@ function App() {
} }
/> />
<Route <Route
path="/frameworks/:frameworkId/:controlId" path="/frameworks/:frameworkId/controls/:controlId"
element={ element={
<Suspense> <Suspense>
<ErrorBoundaryWithLocation> <ErrorBoundaryWithLocation>

View File

@@ -1,39 +0,0 @@
import React, { createContext, useContext, useState, useCallback } from "react";
type BreadcrumbContextType = {
segments: Record<string, string>;
setBreadcrumbSegment: (path: string, display: string) => void;
};
const BreadcrumbContext = createContext<BreadcrumbContextType | undefined>(
undefined,
);
export function BreadcrumbProvider({
children,
}: {
children: React.ReactNode;
}) {
const [segments, setSegments] = useState<Record<string, string>>({});
const setBreadcrumbSegment = useCallback((path: string, display: string) => {
setSegments((prev) => ({
...prev,
[path]: display,
}));
}, []);
return (
<BreadcrumbContext.Provider value={{ segments, setBreadcrumbSegment }}>
{children}
</BreadcrumbContext.Provider>
);
}
export function useBreadcrumb() {
const context = useContext(BreadcrumbContext);
if (context === undefined) {
throw new Error("useBreadcrumb must be used within a BreadcrumbProvider");
}
return context;
}

View File

@@ -1,4 +1,11 @@
import { Link, Outlet, useLocation } from "react-router"; import {
Link,
Outlet,
Route,
Routes,
useLocation,
useParams,
} from "react-router";
import { AppSidebar } from "@/components/AppSidebar"; import { AppSidebar } from "@/components/AppSidebar";
import React from "react"; import React from "react";
import { import {
@@ -15,33 +22,15 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import {
BreadcrumbProvider,
useBreadcrumb,
} from "@/contexts/BreadcrumbContext";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { graphql } from "react-relay";
import { useLazyLoadQuery } from "react-relay";
import { ConsoleLayoutBreadcrumbFrameworkOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbFrameworkOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbPeopleOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbPeopleOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbVendorOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbVendorOverviewQuery.graphql";
import { ConsoleLayoutBreadcrumbControlOverviewQuery } from "./__generated__/ConsoleLayoutBreadcrumbControlOverviewQuery.graphql";
function BreadcrumbNavigation() { function BreadcrumbHome() {
const location = useLocation();
const pathSegments = location.pathname.split("/").filter(Boolean);
const { segments } = useBreadcrumb();
const getBreadcrumbText = (segment: string, path: string) => {
// Check if we have a custom segment name
const fullPath = pathSegments
.slice(0, pathSegments.indexOf(segment) + 1)
.join("/");
// Look for patterns like 'vendors/:id' in our segments
const patternPath = fullPath.replace(/\/[^/]+$/, "/:id");
if (segments[patternPath]) {
return segments[patternPath];
}
// Default to capitalized segment
return segment.charAt(0).toUpperCase() + segment.slice(1);
};
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
@@ -50,51 +39,238 @@ function BreadcrumbNavigation() {
<Link to="/">Home</Link> <Link to="/">Home</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
{pathSegments.map((segment, index) => { <Outlet />
const path = `/${pathSegments.slice(0, index + 1).join("/")}`;
const isLast = index === pathSegments.length - 1;
return (
<React.Fragment key={path}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>
{getBreadcrumbText(segment, path)}
</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link to={path}>{getBreadcrumbText(segment, path)}</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</React.Fragment>
);
})}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
); );
} }
export default function ConsoleLayout() { function BreadcrumbFrameworkList() {
return ( return (
<BreadcrumbProvider> <>
<SidebarProvider> <BreadcrumbSeparator />
<AppSidebar /> <BreadcrumbItem>
<SidebarInset> <BreadcrumbLink asChild>
<header className="flex h-16 shrink-0 items-center gap-2"> <Link to="/frameworks">Frameworks</Link>
<div className="flex items-center gap-2 px-4"> </BreadcrumbLink>
<SidebarTrigger className="-ml-1" /> </BreadcrumbItem>
<Separator orientation="vertical" className="mr-2 h-4" /> <Outlet />
<BreadcrumbNavigation /> </>
</div> );
</header> }
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<Outlet /> function BreadcrumbFrameworkOverview() {
<Toaster /> const { frameworkId } = useParams();
</div> const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbFrameworkOverviewQuery>(
</SidebarInset> graphql`
</SidebarProvider> query ConsoleLayoutBreadcrumbFrameworkOverviewQuery($frameworkId: ID!) {
</BreadcrumbProvider> framework: node(id: $frameworkId) {
id
... on Framework {
name
}
}
}
`,
{ frameworkId: frameworkId! },
{ fetchPolicy: "store-or-network" },
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}>
{data.framework.name}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbPeopleList() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/peoples">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() {
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/vendors">Vendors</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
function BreadcrumbVendorOverview() {
const { vendorId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbVendorOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbVendorOverviewQuery($vendorId: ID!) {
vendor: node(id: $vendorId) {
id
... on Vendor {
name
}
}
}
`,
{ vendorId: vendorId! },
{ fetchPolicy: "store-or-network" },
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/vendors/${data.vendor.id}`}>{data.vendor.name}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
</>
);
}
function BreadcrumbControlOverview() {
const { frameworkId, controlId } = useParams();
const data = useLazyLoadQuery<ConsoleLayoutBreadcrumbControlOverviewQuery>(
graphql`
query ConsoleLayoutBreadcrumbControlOverviewQuery(
$frameworkId: ID!
$controlId: ID!
) {
framework: node(id: $frameworkId) {
id
... on Framework {
name
}
}
control: node(id: $controlId) {
id
... on Control {
name
}
}
}
`,
{ frameworkId: frameworkId!, controlId: controlId! },
{ fetchPolicy: "store-or-network" },
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/frameworks/${data.framework.id}`}>
{data.framework.name}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
to={`/frameworks/${data.framework.id}/controls/${data.control.id}`}
>
{data.control.name}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
export default function ConsoleLayout() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Routes>
<Route path="/" element={<BreadcrumbHome />}>
<Route path="/frameworks" element={<BreadcrumbFrameworkList />}>
<Route
path="/frameworks/:frameworkId"
element={<BreadcrumbFrameworkOverview />}
/>
<Route
path="/frameworks/:frameworkId/controls/:controlId"
element={<BreadcrumbControlOverview />}
/>
</Route>
<Route path="/peoples" element={<BreadcrumbPeopleList />}>
<Route
path="/peoples/:peopleId"
element={<BreadcrumbPeopleOverview />}
/>
</Route>
<Route path="/vendors" element={<BreadcrumbVendorList />}>
<Route
path="/vendors/:vendorId"
element={<BreadcrumbVendorOverview />}
/>
</Route>
</Route>
</Routes>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<Outlet />
<Toaster />
</div>
</SidebarInset>
</SidebarProvider>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,18 @@ import { CheckCircle2 } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
const controlOverviewPageQuery = graphql` const controlOverviewPageQuery = graphql`
query ControlOverviewPageQuery($controlId: ID!) { query ControlOverviewPageQuery($frameworkId: ID!, $controlId: ID!) {
node(id: $controlId) { framework: node(id: $frameworkId) {
id
... on Framework {
name
}
}
control: node(id: $controlId) {
id id
... on Control { ... on Control {
name name
@@ -45,21 +52,15 @@ function ControlOverviewPageContent({
controlOverviewPageQuery, controlOverviewPageQuery,
queryRef, queryRef,
); );
const { setBreadcrumbSegment } = useBreadcrumb(); const control = data.control;
const control = data.node; const framework = data.framework;
const tasks = control.tasks?.edges.map((edge) => edge?.node) ?? []; const tasks = control?.tasks?.edges.map((edge) => edge?.node) ?? [];
useEffect(() => {
if (control?.name) {
setBreadcrumbSegment("frameworks/:frameworkId/:controlId", control.name);
}
}, [control?.name, setBreadcrumbSegment]);
return ( return (
<div className="min-h-screen bg-white p-6 space-y-6"> <div className="min-h-screen bg-white p-6 space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">{control.name}</h1> <h1 className="text-2xl font-semibold">{control?.name}</h1>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm"> <div className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm">
30 min 30 min
@@ -72,7 +73,7 @@ function ControlOverviewPageContent({
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 max-w-3xl">{control.description}</p> <p className="text-gray-600 max-w-3xl">{control?.description}</p>
</div> </div>
<Card className="bg-gray-50 border border-gray-200"> <Card className="bg-gray-50 border border-gray-200">
@@ -80,52 +81,64 @@ function ControlOverviewPageContent({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-4 h-4 rounded-full bg-white flex items-center justify-center border border-gray-200"> <div className="w-4 h-4 rounded-full bg-white flex items-center justify-center border border-gray-200">
<div <div
className={`w-2 h-2 rounded-full ${control.state === "IMPLEMENTED" ? "bg-green-500" : "bg-gray-300"}`} className={`w-2 h-2 rounded-full ${
control?.state === "IMPLEMENTED"
? "bg-green-500"
: "bg-gray-300"
}`}
/> />
</div> </div>
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
{control.state === "IMPLEMENTED" ? "Validated" : "Not validated"} {control?.state === "IMPLEMENTED" ? "Validated" : "Not validated"}
</span> </span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<div> <div>
<h2 className="text-xl font-semibold mb-4">Tasks</h2> <h2 className="text-xl font-semibold mb-6">Tasks</h2>
<div className="space-y-2"> <div className="space-y-2">
{tasks.map((task) => ( {tasks.map((task) => (
<div <div
key={task?.id} key={task?.id}
className="flex items-start gap-3 p-4 bg-white border border-gray-200 rounded-lg hover:shadow-sm transition-shadow" className="flex items-center gap-3 py-4 px-2 hover:bg-gray-50 group"
> >
<div <div
className={`mt-1 w-5 h-5 rounded-full border-2 flex items-center justify-center cursor-pointer ${task?.state === "DONE" ? "border-green-500 bg-green-50" : "border-gray-300"}`} className={`w-5 h-5 rounded border flex items-center justify-center cursor-pointer ${
task?.state === "DONE"
? "border-gray-400 bg-gray-100"
: "border-gray-300"
}`}
> >
{task?.state === "DONE" && ( {task?.state === "DONE" && (
<CheckCircle2 className="w-4 h-4 text-green-500" /> <CheckCircle2 className="w-4 h-4 text-gray-500" />
)} )}
</div> </div>
<div className="flex-1"> <div className="flex-1 flex items-center justify-between">
<div className="flex items-start justify-between"> <div>
<div> <h3
<h3 className={`text-sm ${
className={`text-sm font-medium ${task?.state === "DONE" ? "text-gray-500 line-through" : "text-gray-900"}`} task?.state === "DONE"
> ? "text-gray-500 line-through"
{task?.name} : "text-gray-900"
</h3> }`}
<p >
className={`text-sm mt-1 ${task?.state === "DONE" ? "text-gray-400" : "text-gray-600"}`} {task?.name}
> </h3>
{task?.description} </div>
</p> <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
</div> <div className="text-gray-400 text-sm">06.00 - 07.30</div>
<div className="flex items-center gap-2"> <button className="text-gray-400 hover:text-gray-600">
<div <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24">
className={`px-2 py-1 rounded-full text-xs ${task?.state === "DONE" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}`} <path
> stroke="currentColor"
{task?.state.toLowerCase()} strokeLinecap="round"
</div> strokeLinejoin="round"
</div> strokeWidth="2"
d="M12 12h.01M12 6h.01M12 18h.01"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -158,14 +171,14 @@ function ControlOverviewPageFallback() {
} }
export default function ControlOverviewPage() { export default function ControlOverviewPage() {
const { controlId } = useParams(); const { frameworkId, controlId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>(
controlOverviewPageQuery, controlOverviewPageQuery,
); );
useEffect(() => { useEffect(() => {
loadQuery({ controlId: controlId! }); loadQuery({ frameworkId: frameworkId!, controlId: controlId! });
}, [loadQuery, controlId]); }, [loadQuery, frameworkId, controlId]);
return ( return (
<> <>

View File

@@ -12,7 +12,6 @@ import { Progress } from "@/components/ui/progress";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql"; import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
const FrameworkOverviewPageQuery = graphql` const FrameworkOverviewPageQuery = graphql`
@@ -44,7 +43,6 @@ function FrameworkOverviewPageContent({
queryRef: PreloadedQuery<FrameworkOverviewPageQueryType>; queryRef: PreloadedQuery<FrameworkOverviewPageQueryType>;
}) { }) {
const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef); const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb();
const framework = data.node; const framework = data.node;
const controls = framework.controls?.edges.map((edge) => edge?.node) ?? []; const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
const [hoveredCard, setHoveredCard] = useState<number | null>(null); const [hoveredCard, setHoveredCard] = useState<number | null>(null);
@@ -57,12 +55,6 @@ function FrameworkOverviewPageContent({
const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isTooltipHovered, setIsTooltipHovered] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
if (framework?.name) {
setBreadcrumbSegment("frameworks/:id", framework.name);
}
}, [framework?.name, setBreadcrumbSegment]);
// Group controls by their category // Group controls by their category
const controlsByCategory = controls.reduce( const controlsByCategory = controls.reduce(
(acc, control) => { (acc, control) => {
@@ -198,7 +190,7 @@ function FrameworkOverviewPageContent({
onClick={() => { onClick={() => {
if (control?.id) { if (control?.id) {
navigate( navigate(
`/frameworks/${framework.id}/${control.id}`, `/frameworks/${framework.id}/controls/${control.id}`,
); );
} }
}} }}

View File

@@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button";
import { Link } from "react-router"; import { Link } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql"; import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.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";
@@ -132,7 +133,8 @@ function PeopleListContent({
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [deletePeople] = useMutation(deletePeopleMutation); const [deletePeople] =
useMutation<PeopleListPageDeletePeopleMutation>(deletePeopleMutation);
const { const {
data: peoplesConnection, data: peoplesConnection,

View File

@@ -17,7 +17,6 @@ import { Suspense, useEffect, useState, useCallback } from "react";
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql"; import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const peopleOverviewPageQuery = graphql` const peopleOverviewPageQuery = graphql`
@@ -88,7 +87,6 @@ function PeopleOverviewPageContent({
queryRef: PreloadedQuery<PeopleOverviewPageQueryType>; queryRef: PreloadedQuery<PeopleOverviewPageQueryType>;
}) { }) {
const data = usePreloadedQuery(peopleOverviewPageQuery, queryRef); const data = usePreloadedQuery(peopleOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb();
const [editedFields, setEditedFields] = useState<Set<string>>(new Set()); const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
fullName: data.node.fullName || "", fullName: data.node.fullName || "",
@@ -159,12 +157,6 @@ function PeopleOverviewPageContent({
setEditedFields(new Set()); setEditedFields(new Set());
}; };
useEffect(() => {
if (data.node?.fullName) {
setBreadcrumbSegment("peoples/:id", data.node.fullName);
}
}, [data.node?.fullName, setBreadcrumbSegment]);
return ( return (
<> <>
<div className="space-y-6 p-4 md:p-6 lg:p-8"> <div className="space-y-6 p-4 md:p-6 lg:p-8">

View File

@@ -17,9 +17,7 @@ import { Suspense, useEffect, useState, useCallback, useMemo } from "react";
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql"; import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { UpdateVendorInput } from "./__generated__/VendorOverviewPageUpdateVendorMutation.graphql";
const vendorOverviewPageQuery = graphql` const vendorOverviewPageQuery = graphql`
query VendorOverviewPageQuery($vendorId: ID!) { query VendorOverviewPageQuery($vendorId: ID!) {
@@ -112,7 +110,6 @@ function VendorOverviewPageContent({
queryRef: PreloadedQuery<VendorOverviewPageQueryType>; queryRef: PreloadedQuery<VendorOverviewPageQueryType>;
}) { }) {
const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef); const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb();
const [editedFields, setEditedFields] = useState<Set<string>>(new Set()); const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: data.node.name || "", name: data.node.name || "",
@@ -207,12 +204,6 @@ function VendorOverviewPageContent({
setEditedFields(new Set()); setEditedFields(new Set());
}; };
useEffect(() => {
if (data.node?.name) {
setBreadcrumbSegment("vendors/:id", data.node.name);
}
}, [data.node?.name, setBreadcrumbSegment]);
return ( return (
<> <>
<div className="space-y-6 p-4 md:p-6 lg:p-8"> <div className="space-y-6 p-4 md:p-6 lg:p-8">