Rewrite context routes to use useQueryLoader pattern

Replace loaderFromQueryLoader/loadQuery in route definitions with
useQueryLoader inside dedicated Loader components, matching the
compliance-page pattern. Use index routes instead of redirect hacks.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-19 18:03:40 +01:00
parent e1133b1e9a
commit 89882d368c
5 changed files with 116 additions and 96 deletions

View File

@@ -5,7 +5,7 @@ import { Outlet } from "react-router";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
export default function ContextLayout() { export default function ContextLayoutLoader() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const prefix = `/organizations/${organizationId}/context`; const prefix = `/organizations/${organizationId}/context`;
@@ -21,7 +21,7 @@ export default function ContextLayout() {
)} )}
/> />
<Tabs> <Tabs>
<TabLink to={`${prefix}/overview`}>{__("Context")}</TabLink> <TabLink to={prefix} end>{__("Context")}</TabLink>
<TabLink to={`${prefix}/meetings`}>{__("Meetings")}</TabLink> <TabLink to={`${prefix}/meetings`}>{__("Meetings")}</TabLink>
</Tabs> </Tabs>
<Outlet /> <Outlet />

View File

@@ -1,11 +1,15 @@
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { useEffect } from "react";
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { ContextPageLoaderQuery } from "#/__generated__/core/ContextPageLoaderQuery.graphql"; import type { ContextPageLoaderQuery } from "#/__generated__/core/ContextPageLoaderQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import ContextPage from "./ContextPage"; import ContextPage from "./ContextPage";
export const contextPageQuery = graphql` const contextPageQuery = graphql`
query ContextPageLoaderQuery($organizationId: ID!) { query ContextPageLoaderQuery($organizationId: ID!) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
... on Organization { ... on Organization {
@@ -15,12 +19,31 @@ export const contextPageQuery = graphql`
} }
`; `;
type Props = { function ContextPageQueryLoader() {
queryRef: PreloadedQuery<ContextPageLoaderQuery>; const organizationId = useOrganizationId();
}; const [queryRef, loadQuery] = useQueryLoader<ContextPageLoaderQuery>(contextPageQuery);
export default function ContextPageLoader(props: Props) { useEffect(() => {
const data = usePreloadedQuery(contextPageQuery, props.queryRef); if (!queryRef) {
loadQuery({ organizationId });
}
});
if (!queryRef) return <LinkCardSkeleton />;
return <ContextPageInner queryRef={queryRef} />;
}
function ContextPageInner({ queryRef }: { queryRef: PreloadedQuery<ContextPageLoaderQuery> }) {
const data = usePreloadedQuery(contextPageQuery, queryRef);
return <ContextPage organization={data.organization} />; return <ContextPage organization={data.organization} />;
} }
export default function ContextPageLoader() {
return (
<CoreRelayProvider>
<ContextPageQueryLoader />
</CoreRelayProvider>
);
}

View File

@@ -0,0 +1,32 @@
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { useParams } from "react-router";
import type { MeetingDetailPageQuery } from "#/__generated__/core/MeetingDetailPageQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import MeetingDetailPage, { meetingDetailPageQuery } from "./MeetingDetailPage";
function MeetingDetailPageQueryLoader() {
const { meetingId } = useParams<{ meetingId: string }>();
const [queryRef, loadQuery] = useQueryLoader<MeetingDetailPageQuery>(meetingDetailPageQuery);
useEffect(() => {
if (!queryRef && meetingId) {
loadQuery({ meetingId });
}
});
if (!queryRef) return <PageSkeleton />;
return <MeetingDetailPage queryRef={queryRef} />;
}
export default function MeetingDetailPageLoader() {
return (
<CoreRelayProvider>
<MeetingDetailPageQueryLoader />
</CoreRelayProvider>
);
}

View File

@@ -0,0 +1,32 @@
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import type { MeetingsPageQuery } from "#/__generated__/core/MeetingsPageQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import MeetingsPage, { meetingsPageQuery } from "./MeetingsPage";
function MeetingsPageQueryLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<MeetingsPageQuery>(meetingsPageQuery);
useEffect(() => {
if (!queryRef) {
loadQuery({ organizationId });
}
});
if (!queryRef) return <LinkCardSkeleton />;
return <MeetingsPage queryRef={queryRef} />;
}
export default function MeetingsPageLoader() {
return (
<CoreRelayProvider>
<MeetingsPageQueryLoader />
</CoreRelayProvider>
);
}

View File

@@ -1,113 +1,46 @@
import { lazy } from "@probo/react-lazy"; import { lazy } from "@probo/react-lazy";
import { import type { AppRoute } from "@probo/routes";
type AppRoute,
loaderFromQueryLoader,
withQueryRef,
} from "@probo/routes";
import { Fragment } from "react";
import { loadQuery } from "react-relay";
import { type LoaderFunctionArgs, redirect } from "react-router";
import type { ContextPageLoaderQuery } from "#/__generated__/core/ContextPageLoaderQuery.graphql";
import type { MeetingDetailPageQuery } from "#/__generated__/core/MeetingDetailPageQuery.graphql";
import type { MeetingsPageQuery } from "#/__generated__/core/MeetingsPageQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton"; import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { coreEnvironment } from "#/environments";
import { contextPageQuery } from "#/pages/organizations/context/ContextPageLoader";
import { meetingDetailPageQuery } from "#/pages/organizations/meetings/MeetingDetailPage";
import { meetingsPageQuery } from "#/pages/organizations/meetings/MeetingsPage";
const meetingTabs = (prefix: string) => {
return [
{
path: `${prefix}`,
loader: ({
params: { organizationId, meetingId },
}: LoaderFunctionArgs) => {
const basePath = `/organizations/${organizationId}/context/meetings/${meetingId}`;
const redirectPath = `${basePath}/minutes`;
// eslint-disable-next-line
throw redirect(redirectPath);
},
Component: Fragment,
},
{
path: `${prefix}minutes`,
Fallback: LinkCardSkeleton,
Component: lazy(
() => import("../pages/organizations/meetings/tabs/MeetingMinutesTab"),
),
},
];
};
export const contextRoutes = [ export const contextRoutes = [
{ {
path: "context", path: "context",
Fallback: PageSkeleton, Fallback: PageSkeleton,
Component: lazy( Component: lazy(
() => import("#/pages/organizations/context/ContextLayout"), () => import("#/pages/organizations/context/ContextLayoutLoader"),
), ),
children: [ children: [
{ {
path: "", index: true,
loader: ({ Fallback: LinkCardSkeleton,
params: { organizationId }, Component: lazy(
}: LoaderFunctionArgs) => { () => import("#/pages/organizations/context/ContextPageLoader"),
// eslint-disable-next-line
throw redirect(`/organizations/${organizationId}/context/overview`);
},
Component: Fragment,
},
{
path: "overview",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery<ContextPageLoaderQuery>(
coreEnvironment,
contextPageQuery,
{ organizationId },
),
),
Component: withQueryRef(
lazy(
() => import("#/pages/organizations/context/ContextPageLoader"),
),
), ),
}, },
{ {
path: "meetings", path: "meetings",
Fallback: PageSkeleton, Fallback: LinkCardSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) => Component: lazy(
loadQuery<MeetingsPageQuery>( () => import("#/pages/organizations/meetings/MeetingsPageLoader"),
coreEnvironment,
meetingsPageQuery,
{ organizationId },
),
),
Component: withQueryRef(
lazy(
() => import("#/pages/organizations/meetings/MeetingsPage"),
),
), ),
}, },
{ {
path: "meetings/:meetingId", path: "meetings/:meetingId",
Fallback: PageSkeleton, Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ meetingId }) => Component: lazy(
loadQuery<MeetingDetailPageQuery>( () => import("#/pages/organizations/meetings/MeetingDetailPageLoader"),
coreEnvironment,
meetingDetailPageQuery,
{ meetingId },
),
), ),
Component: withQueryRef( children: [
lazy( {
() => import("../pages/organizations/meetings/MeetingDetailPage"), index: true,
), Fallback: LinkCardSkeleton,
), Component: lazy(
children: [...meetingTabs("")], () => import("#/pages/organizations/meetings/tabs/MeetingMinutesTab"),
),
},
],
}, },
], ],
}, },