Transform meetings page into context page with tabs

Add structured organization context with 5 markdown sections (Product, Architecture, Team, Processes, Customers) editable inline. Meetings are now a tab within the context page. Moved all GraphQL queries from hooks/graph/MeetingGraph.ts into colocated components following new best practices. Updated database schema, backend services, GraphQL resolvers, and MCP API to support the new context fields and structure.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-19 17:42:14 +01:00
parent c2a1843c67
commit a9ebeef0fa
25 changed files with 1156 additions and 380 deletions

View File

@@ -1,111 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import type { MeetingGraphDeleteMutation } from "#/__generated__/core/MeetingGraphDeleteMutation.graphql";
import type { MeetingGraphUpdateMutation } from "#/__generated__/core/MeetingGraphUpdateMutation.graphql";
import { useMutationWithToasts } from "../useMutationWithToasts";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
export const meetingsQuery = graphql`
query MeetingGraphListQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
canCreateMeeting: permission(action: "core:meeting:create")
}
...MeetingsPageListFragment
}
}
`;
export const meetingNodeQuery = graphql`
query MeetingGraphNodeQuery($meetingId: ID!) {
node(id: $meetingId) {
...MeetingDetailPageMeetingFragment
}
}
`;
export const MeetingsConnectionKey = "MeetingsListQuery_meetings";
const deleteMeetingMutation = graphql`
mutation MeetingGraphDeleteMutation($input: DeleteMeetingInput!) {
deleteMeeting(input: $input) {
deletedMeetingId @deleteRecord
}
}
`;
export function useDeleteMeetingMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<MeetingGraphDeleteMutation>(
deleteMeetingMutation,
{
successMessage: __("Meeting deleted successfully."),
errorMessage: __("Failed to delete meeting"),
},
);
}
const updateMeetingMutation = graphql`
mutation MeetingGraphUpdateMutation($input: UpdateMeetingInput!) {
updateMeeting(input: $input) {
meeting {
id
name
date
minutes
attendees {
id
fullName
}
}
}
}
`;
export function useUpdateMeetingMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<MeetingGraphUpdateMutation>(
updateMeetingMutation,
{
successMessage: __("Meeting updated successfully."),
errorMessage: __("Failed to update meeting"),
},
);
}
const createMeetingMutation = graphql`
mutation MeetingGraphCreateMutation(
$input: CreateMeetingInput!
$connections: [ID!]!
) {
createMeeting(input: $input) {
meetingEdge @prependEdge(connections: $connections) {
node {
id
name
date
minutes
attendees {
id
fullName
}
}
}
}
}
`;
export function useCreateMeetingMutation() {
const { __ } = useTranslate();
return useMutationWithToasts(createMeetingMutation, {
successMessage: __("Meeting created successfully."),
errorMessage: __("Failed to create meeting"),
});
}

View File

@@ -3,7 +3,6 @@ import {
IconBank,
IconBook,
IconBox,
IconCalendar1,
IconCircleProgress,
IconClock,
IconFire3,
@@ -15,6 +14,7 @@ import {
IconMedal,
IconPageCheck,
IconPageTextLine,
IconPageTextSolid,
IconSettingsGear2,
IconShield,
IconStore,
@@ -29,7 +29,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
const fragment = graphql`
fragment SidebarFragment on Organization {
canListMeetings: permission(action: "core:meeting:list")
canGetContext: permission(action: "core:organization-context:get")
canListTasks: permission(action: "core:task:list")
canListMeasures: permission(action: "core:measure:list")
canListRisks: permission(action: "core:risk:list")
@@ -67,11 +67,11 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
return (
<ul className="space-y-[2px]">
{organization.canListMeetings && (
{organization.canGetContext && (
<SidebarItem
label={__("Meetings")}
icon={IconCalendar1}
to={`${prefix}/meetings`}
label={__("Context")}
icon={IconPageTextSolid}
to={`${prefix}/context`}
/>
)}
{organization.canListTasks && (

View File

@@ -0,0 +1,30 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { PageHeader, TabLink, Tabs } from "@probo/ui";
import { Outlet } from "react-router";
import { useOrganizationId } from "#/hooks/useOrganizationId";
export default function ContextLayout() {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const prefix = `/organizations/${organizationId}/context`;
usePageTitle(__("Context"));
return (
<div className="space-y-6">
<PageHeader
title={__("Context")}
description={__(
"Structured company information and meetings for AI assistants and compliance workflows.",
)}
/>
<Tabs>
<TabLink to={`${prefix}/overview`}>{__("Context")}</TabLink>
<TabLink to={`${prefix}/meetings`}>{__("Meetings")}</TabLink>
</Tabs>
<Outlet />
</div>
);
}

View File

@@ -0,0 +1,254 @@
import { useTranslate } from "@probo/i18n";
import {
Button,
Card,
IconCheckmark1,
IconCrossLargeX,
IconPencil,
Markdown,
Textarea,
} from "@probo/ui";
import { useRef, useState } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { ContextPage_UpdateMutation } from "#/__generated__/core/ContextPage_UpdateMutation.graphql";
import type { ContextPageFragment$key } from "#/__generated__/core/ContextPageFragment.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
/* eslint-disable relay/unused-fields */
const fragment = graphql`
fragment ContextPageFragment on Organization {
id
canUpdateContext: permission(action: "core:organization-context:update")
context {
product
architecture
team
processes
customers
}
}
`;
/* eslint-enable relay/unused-fields */
const updateMutation = graphql`
mutation ContextPage_UpdateMutation(
$input: UpdateOrganizationContextInput!
) {
updateOrganizationContext(input: $input) {
context {
organizationId
product
architecture
team
processes
customers
}
}
}
`;
type SectionKey = "product" | "architecture" | "team" | "processes" | "customers";
type SectionConfig = {
key: SectionKey;
title: string;
description: string;
placeholder: string;
};
type Props = {
organization: ContextPageFragment$key;
};
export default function ContextPage(props: Props) {
const { __ } = useTranslate();
const organization = useFragment(fragment, props.organization);
const sections: SectionConfig[] = [
{
key: "product",
title: __("Product"),
description: __("Describe what your product does, its main features, and value proposition."),
placeholder: __("Describe your product in markdown format..."),
},
{
key: "architecture",
title: __("Architecture"),
description: __("Describe your technical architecture, infrastructure, and key design decisions."),
placeholder: __("Describe your architecture in markdown format..."),
},
{
key: "team",
title: __("Team"),
description: __("Describe your team structure, roles, and responsibilities."),
placeholder: __("Describe your team in markdown format..."),
},
{
key: "processes",
title: __("Processes"),
description: __("Describe your key processes, workflows, and operational procedures."),
placeholder: __("Describe your processes in markdown format..."),
},
{
key: "customers",
title: __("Customers"),
description: __("Describe your target market, customer segments, and use cases."),
placeholder: __("Describe your customers in markdown format..."),
},
];
return (
<div className="space-y-6">
{sections.map(section => (
<ContextSection
key={section.key}
section={section}
organizationId={organization.id}
value={organization.context?.[section.key] ?? null}
canEdit={organization.canUpdateContext}
/>
))}
</div>
);
}
function ContextSection({
section,
organizationId,
value,
canEdit,
}: {
section: SectionConfig;
organizationId: string;
value: string | null;
canEdit: boolean;
}) {
const { __ } = useTranslate();
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(value ?? "");
const [displayedValue, setDisplayedValue] = useState(value ?? "");
const justSavedRef = useRef(false);
const [updateContext, isUpdating]
= useMutationWithToasts<ContextPage_UpdateMutation>(
updateMutation,
{
successMessage: __("Context updated successfully"),
errorMessage: __("Failed to update context"),
},
);
const handleSave = async () => {
const valueToSave = text.trim();
const previousValue = value ?? "";
setDisplayedValue(valueToSave);
justSavedRef.current = true;
const valueToSend = valueToSave.length > 0 ? valueToSave : null;
await updateContext({
variables: {
input: {
organizationId,
[section.key]: valueToSend,
},
},
onError: () => {
setDisplayedValue(previousValue);
justSavedRef.current = false;
},
onCompleted: (_, errors) => {
if (errors?.length) {
setDisplayedValue(previousValue);
justSavedRef.current = false;
}
setIsEditing(false);
},
});
};
const handleCancel = () => {
setText(value ?? "");
setIsEditing(false);
};
return (
<Card padded>
{isEditing
? (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold">{section.title}</h3>
<p className="text-xs text-txt-tertiary mt-1">
{section.description}
</p>
</div>
<Textarea
value={text}
onChange={e => setText(e.target.value)}
autogrow
className="min-h-32 font-mono text-sm"
placeholder={section.placeholder}
/>
<div className="flex gap-2 justify-end">
<Button
variant="secondary"
icon={IconCrossLargeX}
onClick={handleCancel}
disabled={isUpdating}
>
{__("Cancel")}
</Button>
<Button
icon={IconCheckmark1}
onClick={() => void handleSave()}
disabled={isUpdating}
>
{__("Save")}
</Button>
</div>
</div>
)
: (
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold">{section.title}</h3>
<p className="text-xs text-txt-tertiary mt-1">
{section.description}
</p>
</div>
{canEdit && (
<Button
variant="quaternary"
icon={IconPencil}
onClick={() => {
setText(value ?? "");
setIsEditing(true);
}}
>
{__("Edit")}
</Button>
)}
</div>
<div className="w-full">
{displayedValue
? (
<div className="prose prose-sm max-w-none w-full [&_.prose]:max-w-none">
<Markdown content={displayedValue} />
</div>
)
: (
<div className="text-txt-tertiary text-sm italic">
{__("No content yet. Click Edit to add one.")}
</div>
)}
</div>
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,26 @@
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { ContextPageLoaderQuery } from "#/__generated__/core/ContextPageLoaderQuery.graphql";
import ContextPage from "./ContextPage";
export const contextPageQuery = graphql`
query ContextPageLoaderQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
...ContextPageFragment
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<ContextPageLoaderQuery>;
};
export default function ContextPageLoader(props: Props) {
const data = usePreloadedQuery(contextPageQuery, props.queryRef);
return <ContextPage organization={data.organization} />;
}

View File

@@ -16,12 +16,10 @@ import type { PreloadedQuery } from "react-relay";
import { graphql, useFragment, usePreloadedQuery } from "react-relay";
import { Link, Outlet, useNavigate } from "react-router";
import type { MeetingDetailPageDeleteMutation } from "#/__generated__/core/MeetingDetailPageDeleteMutation.graphql";
import type { MeetingDetailPageMeetingFragment$key } from "#/__generated__/core/MeetingDetailPageMeetingFragment.graphql";
import type { MeetingGraphNodeQuery } from "#/__generated__/core/MeetingGraphNodeQuery.graphql";
import {
meetingNodeQuery,
useDeleteMeetingMutation,
} from "#/hooks/graph/MeetingGraph";
import type { MeetingDetailPageQuery } from "#/__generated__/core/MeetingDetailPageQuery.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import {
@@ -29,6 +27,14 @@ import {
type UpdateMeetingMinutesDialogRef,
} from "./dialogs/UpdateMeetingMinutesDialog";
export const meetingDetailPageQuery = graphql`
query MeetingDetailPageQuery($meetingId: ID!) {
node(id: $meetingId) {
...MeetingDetailPageMeetingFragment
}
}
`;
const meetingFragment = graphql`
fragment MeetingDetailPageMeetingFragment on Meeting {
id
@@ -45,12 +51,32 @@ const meetingFragment = graphql`
}
`;
const deleteMeetingMutation = graphql`
mutation MeetingDetailPageDeleteMutation($input: DeleteMeetingInput!) {
deleteMeeting(input: $input) {
deletedMeetingId @deleteRecord
}
}
`;
function useDeleteMeetingMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<MeetingDetailPageDeleteMutation>(
deleteMeetingMutation,
{
successMessage: __("Meeting deleted successfully."),
errorMessage: __("Failed to delete meeting"),
},
);
}
type Props = {
queryRef: PreloadedQuery<MeetingGraphNodeQuery>;
queryRef: PreloadedQuery<MeetingDetailPageQuery>;
};
export default function MeetingDetailPage(props: Props) {
const node = usePreloadedQuery(meetingNodeQuery, props.queryRef).node;
const node = usePreloadedQuery(meetingDetailPageQuery, props.queryRef).node;
const meeting = useFragment<MeetingDetailPageMeetingFragment$key>(
meetingFragment,
node,
@@ -75,7 +101,7 @@ export default function MeetingDetailPage(props: Props) {
input: { meetingId: meeting.id },
},
onSuccess: () => {
void navigate(`/organizations/${organizationId}/meetings`);
void navigate(`/organizations/${organizationId}/context/meetings`);
},
}),
{
@@ -101,7 +127,7 @@ export default function MeetingDetailPage(props: Props) {
items={[
{
label: __("Meetings"),
to: `/organizations/${organizationId}/meetings`,
to: `/organizations/${organizationId}/context/meetings`,
},
{
label: meeting.name,

View File

@@ -1,5 +1,4 @@
import { formatDate, sprintf } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
@@ -7,22 +6,15 @@ import {
Button,
Card,
DropdownItem,
IconCheckmark1,
IconCrossLargeX,
IconPencil,
IconPlusLarge,
IconTrashCan,
Markdown,
PageHeader,
Tbody,
Td,
Textarea,
Th,
Thead,
Tr,
useConfirm,
} from "@probo/ui";
import { useRef, useState } from "react";
import {
type PreloadedQuery,
useFragment,
@@ -32,19 +24,27 @@ import {
import { Link } from "react-router";
import { graphql } from "relay-runtime";
import type { MeetingGraphListQuery } from "#/__generated__/core/MeetingGraphListQuery.graphql";
import type { MeetingsPage_UpdateSummaryMutation } from "#/__generated__/core/MeetingsPage_UpdateSummaryMutation.graphql";
import type { MeetingsPageDeleteMutation } from "#/__generated__/core/MeetingsPageDeleteMutation.graphql";
import type { MeetingsPageListFragment$key } from "#/__generated__/core/MeetingsPageListFragment.graphql";
import type { MeetingsPageQuery } from "#/__generated__/core/MeetingsPageQuery.graphql";
import type { MeetingsPageRowFragment$key } from "#/__generated__/core/MeetingsPageRowFragment.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable";
import {
meetingsQuery,
useDeleteMeetingMutation,
} from "#/hooks/graph/MeetingGraph";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { CreateMeetingDialog } from "./dialogs/CreateMeetingDialog";
export const meetingsPageQuery = graphql`
query MeetingsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
canCreateMeeting: permission(action: "core:meeting:create")
}
...MeetingsPageListFragment
}
}
`;
const meetingsFragment = graphql`
fragment MeetingsPageListFragment on Organization
@refetchable(queryName: "MeetingsListQuery")
@@ -59,9 +59,7 @@ const meetingsFragment = graphql`
last: { type: "Int", defaultValue: null }
) {
id
context {
summary
}
canCreateMeeting: permission(action: "core:meeting:create")
meetings(
first: $first
after: $after
@@ -80,14 +78,34 @@ const meetingsFragment = graphql`
}
`;
const deleteMeetingMutation = graphql`
mutation MeetingsPageDeleteMutation($input: DeleteMeetingInput!) {
deleteMeeting(input: $input) {
deletedMeetingId @deleteRecord
}
}
`;
function useDeleteMeetingMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<MeetingsPageDeleteMutation>(
deleteMeetingMutation,
{
successMessage: __("Meeting deleted successfully."),
errorMessage: __("Failed to delete meeting"),
},
);
}
type Props = {
queryRef: PreloadedQuery<MeetingGraphListQuery>;
queryRef: PreloadedQuery<MeetingsPageQuery>;
};
export default function MeetingsPage(props: Props) {
const { __ } = useTranslate();
const organization = usePreloadedQuery(
meetingsQuery,
meetingsPageQuery,
props.queryRef,
).organization;
@@ -102,151 +120,15 @@ export default function MeetingsPage(props: Props) {
.filter(Boolean);
const connectionId = pagination.data.meetings.__id;
usePageTitle(__("Meetings"));
const [isEditing, setIsEditing] = useState(false);
const summary = pagination.data.context?.summary || "";
const [summaryText, setSummaryText] = useState(summary);
// Local state to track the displayed summary (updated immediately on save)
const [displayedSummary, setDisplayedSummary] = useState(summary);
// Track if we just saved to prevent useEffect from overwriting our update
const justSavedRef = useRef(false);
const updateSummaryMutation = graphql`
mutation MeetingsPage_UpdateSummaryMutation(
$input: UpdateOrganizationContextInput!
) {
updateOrganizationContext(input: $input) {
context {
organizationId
summary
}
}
}
`;
const [updateSummary, isUpdating]
= useMutationWithToasts<MeetingsPage_UpdateSummaryMutation>(
updateSummaryMutation,
{
successMessage: __("Summary updated successfully"),
errorMessage: __("Failed to update summary"),
},
);
const handleSave = async () => {
const valueToSave = summaryText.trim();
const previousValue = pagination.data.context?.summary || "";
setDisplayedSummary(valueToSave);
justSavedRef.current = true;
const valueToSend = valueToSave.length > 0 ? valueToSave : "";
await updateSummary({
variables: {
input: {
organizationId: organization.id,
summary: valueToSend || null,
},
},
onError: () => {
// Roll back optimistic update on error
setDisplayedSummary(previousValue);
justSavedRef.current = false;
},
onCompleted: (_, errors) => {
if (errors?.length) {
// Roll back optimistic update on GraphQL error
setDisplayedSummary(previousValue);
justSavedRef.current = false;
}
setIsEditing(false);
},
});
};
const handleCancel = () => {
setSummaryText(pagination.data.context?.summary || "");
setIsEditing(false);
};
return (
<div className="space-y-6">
<Card padded>
{isEditing
? (
<div className="space-y-4">
<Textarea
value={summaryText}
onChange={e => setSummaryText(e.target.value)}
autogrow
className="min-h-32 font-mono text-sm"
placeholder={__("Enter meetings summary in markdown format")}
/>
<div className="flex gap-2 justify-end">
<Button
variant="secondary"
icon={IconCrossLargeX}
onClick={handleCancel}
disabled={isUpdating}
>
{__("Cancel")}
</Button>
<Button
icon={IconCheckmark1}
onClick={() => void handleSave()}
disabled={isUpdating}
>
{__("Save")}
</Button>
</div>
</div>
)
: (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-txt-secondary">
{__("Summary")}
</h3>
{organization.canCreateMeeting && (
<Button
variant="quaternary"
icon={IconPencil}
onClick={() => setIsEditing(true)}
>
{__("Edit")}
</Button>
)}
</div>
<div className="w-full">
{displayedSummary
? (
<div className="prose prose-sm max-w-none w-full [&_.prose]:max-w-none">
<Markdown content={displayedSummary} />
</div>
)
: (
<div className="text-txt-tertiary text-sm italic">
{__("No summary yet. Click Edit to add one.")}
</div>
)}
</div>
</div>
)}
</Card>
<PageHeader
title={__("Meetings")}
description={__(
"Track and manage your organization's meetings and their minutes.",
)}
>
{organization.canCreateMeeting && (
{pagination.data.canCreateMeeting && (
<div className="flex justify-end">
<CreateMeetingDialog connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
</CreateMeetingDialog>
)}
</PageHeader>
</div>
)}
{meetingNodes.length > 0
? (
<SortableTable {...pagination}>
@@ -336,7 +218,7 @@ function MeetingRow({
};
return (
<Tr to={`/organizations/${organizationId}/meetings/${meeting.id}`}>
<Tr to={`/organizations/${organizationId}/context/meetings/${meeting.id}`}>
<Td className="w-40">{formatDate(meeting.date)}</Td>
<Td className="min-w-0">
<div className="flex gap-4 items-center">{meeting.name}</div>

View File

@@ -10,11 +10,30 @@ import {
useDialogRef,
} from "@probo/ui";
import { forwardRef, useImperativeHandle } from "react";
import { graphql } from "relay-runtime";
import { z } from "zod";
import type { MeetingDetailPageMeetingFragment$data } from "#/__generated__/core/MeetingDetailPageMeetingFragment.graphql";
import { useUpdateMeetingMutation } from "#/hooks/graph/MeetingGraph";
import type { UpdateMeetingMinutesDialogMutation } from "#/__generated__/core/UpdateMeetingMinutesDialogMutation.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const updateMeetingMutation = graphql`
mutation UpdateMeetingMinutesDialogMutation($input: UpdateMeetingInput!) {
updateMeeting(input: $input) {
meeting {
id
name
date
minutes
attendees {
id
fullName
}
}
}
}
`;
type Props = {
meeting: MeetingDetailPageMeetingFragment$data;
@@ -34,7 +53,14 @@ export const UpdateMeetingMinutesDialog = forwardRef<
>(function UpdateMeetingMinutesDialog({ meeting }, ref) {
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const [updateMeeting, isUpdating] = useUpdateMeetingMutation();
const [updateMeeting, isUpdating]
= useMutationWithToasts<UpdateMeetingMinutesDialogMutation>(
updateMeetingMutation,
{
successMessage: __("Meeting updated successfully."),
errorMessage: __("Failed to update meeting"),
},
);
const { handleSubmit, register, reset } = useFormWithSchema(minutesSchema, {
defaultValues: {
minutes: meeting.minutes || "",

View File

@@ -19,12 +19,12 @@ import { compliancePageRoutes } from "./pages/organizations/compliance-page/rout
import { CurrentUser } from "./providers/CurrentUser";
import { assetRoutes } from "./routes/assetRoutes";
import { auditRoutes } from "./routes/auditRoutes";
import { contextRoutes } from "./routes/contextRoutes";
import { dataRoutes } from "./routes/dataRoutes";
import { documentsRoutes } from "./routes/documentsRoutes";
import { findingRoutes } from "./routes/findingRoutes";
import { frameworkRoutes } from "./routes/frameworkRoutes";
import { measureRoutes } from "./routes/measureRoutes";
import { meetingsRoutes } from "./routes/meetingsRoutes";
import { obligationRoutes } from "./routes/obligationRoutes";
import { processingActivityRoutes } from "./routes/processingActivityRoutes";
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes";
@@ -222,7 +222,7 @@ const routes = [
...assetRoutes,
...dataRoutes,
...auditRoutes,
...meetingsRoutes,
...contextRoutes,
...findingRoutes,
...obligationRoutes,
...rightsRequestRoutes,

View File

@@ -0,0 +1,114 @@
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 { 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"),
),
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"),
),
),
},
{
path: "meetings",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery<MeetingsPageQuery>(
coreEnvironment,
meetingsPageQuery,
{ organizationId },
),
),
Component: withQueryRef(
lazy(
() => import("#/pages/organizations/meetings/MeetingsPage"),
),
),
},
{
path: "meetings/:meetingId",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ meetingId }) =>
loadQuery<MeetingDetailPageQuery>(
coreEnvironment,
meetingDetailPageQuery,
{ meetingId },
),
),
Component: withQueryRef(
lazy(
() => import("../pages/organizations/meetings/MeetingDetailPage"),
),
),
children: [...meetingTabs("")],
},
],
},
] satisfies AppRoute[];

View File

@@ -1,68 +0,0 @@
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 { MeetingGraphListQuery } from "#/__generated__/core/MeetingGraphListQuery.graphql";
import type { MeetingGraphNodeQuery } from "#/__generated__/core/MeetingGraphNodeQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { coreEnvironment } from "#/environments";
import { meetingNodeQuery, meetingsQuery } from "#/hooks/graph/MeetingGraph";
const meetingTabs = (prefix: string) => {
return [
{
path: `${prefix}`,
loader: ({
params: { organizationId, meetingId },
}: LoaderFunctionArgs) => {
const basePath = `/organizations/${organizationId}/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 meetingsRoutes = [
{
path: "meetings",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery<MeetingGraphListQuery>(coreEnvironment, meetingsQuery, {
organizationId,
}),
),
Component: withQueryRef(
lazy(() => import("#/pages/organizations/meetings/MeetingsPage")),
),
},
{
path: "meetings/:meetingId",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ meetingId }) =>
loadQuery<MeetingGraphNodeQuery>(coreEnvironment, meetingNodeQuery, {
meetingId,
}),
),
Component: withQueryRef(
lazy(() => import("../pages/organizations/meetings/MeetingDetailPage")),
),
children: [...meetingTabs("")],
},
] satisfies AppRoute[];