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";
export default function ContextLayout() {
export default function ContextLayoutLoader() {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const prefix = `/organizations/${organizationId}/context`;
@@ -21,7 +21,7 @@ export default function ContextLayout() {
)}
/>
<Tabs>
<TabLink to={`${prefix}/overview`}>{__("Context")}</TabLink>
<TabLink to={prefix} end>{__("Context")}</TabLink>
<TabLink to={`${prefix}/meetings`}>{__("Meetings")}</TabLink>
</Tabs>
<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 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";
export const contextPageQuery = graphql`
const contextPageQuery = graphql`
query ContextPageLoaderQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
@@ -15,12 +19,31 @@ export const contextPageQuery = graphql`
}
`;
type Props = {
queryRef: PreloadedQuery<ContextPageLoaderQuery>;
};
function ContextPageQueryLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<ContextPageLoaderQuery>(contextPageQuery);
export default function ContextPageLoader(props: Props) {
const data = usePreloadedQuery(contextPageQuery, props.queryRef);
useEffect(() => {
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} />;
}
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 {
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 { AppRoute } from "@probo/routes";
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 { 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 = [
{
path: "context",
Fallback: PageSkeleton,
Component: lazy(
() => import("#/pages/organizations/context/ContextLayout"),
() => import("#/pages/organizations/context/ContextLayoutLoader"),
),
children: [
{
path: "",
loader: ({
params: { organizationId },
}: LoaderFunctionArgs) => {
// 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"),
),
index: true,
Fallback: LinkCardSkeleton,
Component: lazy(
() => import("#/pages/organizations/context/ContextPageLoader"),
),
},
{
path: "meetings",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery<MeetingsPageQuery>(
coreEnvironment,
meetingsPageQuery,
{ organizationId },
),
),
Component: withQueryRef(
lazy(
() => import("#/pages/organizations/meetings/MeetingsPage"),
),
Fallback: LinkCardSkeleton,
Component: lazy(
() => import("#/pages/organizations/meetings/MeetingsPageLoader"),
),
},
{
path: "meetings/:meetingId",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ meetingId }) =>
loadQuery<MeetingDetailPageQuery>(
coreEnvironment,
meetingDetailPageQuery,
{ meetingId },
),
Component: lazy(
() => import("#/pages/organizations/meetings/MeetingDetailPageLoader"),
),
Component: withQueryRef(
lazy(
() => import("../pages/organizations/meetings/MeetingDetailPage"),
),
),
children: [...meetingTabs("")],
children: [
{
index: true,
Fallback: LinkCardSkeleton,
Component: lazy(
() => import("#/pages/organizations/meetings/tabs/MeetingMinutesTab"),
),
},
],
},
],
},