From a9ebeef0fa0f8efc80a0f6a050a5585e101690a1 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 19 Mar 2026 17:42:14 +0100 Subject: [PATCH] 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 --- apps/console/src/hooks/graph/MeetingGraph.ts | 111 -------- .../iam/organizations/_components/Sidebar.tsx | 12 +- .../organizations/context/ContextLayout.tsx | 30 +++ .../organizations/context/ContextPage.tsx | 254 ++++++++++++++++++ .../context/ContextPageLoader.tsx | 26 ++ .../meetings/MeetingDetailPage.tsx | 44 ++- .../organizations/meetings/MeetingsPage.tsx | 202 +++----------- .../dialogs/UpdateMeetingMinutesDialog.tsx | 30 ++- apps/console/src/routes.tsx | 4 +- apps/console/src/routes/contextRoutes.ts | 114 ++++++++ apps/console/src/routes/meetingsRoutes.ts | 68 ----- e2e/console/organization_test.go | 21 +- pkg/cmd/context/context.go | 34 +++ pkg/cmd/context/get/get.go | 155 +++++++++++ pkg/cmd/context/update/update.go | 151 +++++++++++ pkg/cmd/root/root.go | 2 + pkg/coredata/migrations/20260319T140000Z.sql | 6 + pkg/coredata/organization_context.go | 42 ++- pkg/probo/organization_service.go | 44 ++- pkg/server/api/console/v1/schema.graphql | 12 +- .../console/v1/types/organization_context.go | 6 +- pkg/server/api/console/v1/v1_resolver.go | 8 +- pkg/server/api/mcp/v1/schema.resolvers.go | 40 +++ pkg/server/api/mcp/v1/specification.yaml | 90 +++++++ .../api/mcp/v1/types/organization_context.go | 30 +++ 25 files changed, 1156 insertions(+), 380 deletions(-) delete mode 100644 apps/console/src/hooks/graph/MeetingGraph.ts create mode 100644 apps/console/src/pages/organizations/context/ContextLayout.tsx create mode 100644 apps/console/src/pages/organizations/context/ContextPage.tsx create mode 100644 apps/console/src/pages/organizations/context/ContextPageLoader.tsx create mode 100644 apps/console/src/routes/contextRoutes.ts delete mode 100644 apps/console/src/routes/meetingsRoutes.ts create mode 100644 pkg/cmd/context/context.go create mode 100644 pkg/cmd/context/get/get.go create mode 100644 pkg/cmd/context/update/update.go create mode 100644 pkg/coredata/migrations/20260319T140000Z.sql create mode 100644 pkg/server/api/mcp/v1/types/organization_context.go diff --git a/apps/console/src/hooks/graph/MeetingGraph.ts b/apps/console/src/hooks/graph/MeetingGraph.ts deleted file mode 100644 index a734b377d..000000000 --- a/apps/console/src/hooks/graph/MeetingGraph.ts +++ /dev/null @@ -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( - 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( - 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"), - }); -} diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx index 6406483e1..0b16ad080 100644 --- a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -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 (
    - {organization.canListMeetings && ( + {organization.canGetContext && ( )} {organization.canListTasks && ( diff --git a/apps/console/src/pages/organizations/context/ContextLayout.tsx b/apps/console/src/pages/organizations/context/ContextLayout.tsx new file mode 100644 index 000000000..3cfe0106b --- /dev/null +++ b/apps/console/src/pages/organizations/context/ContextLayout.tsx @@ -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 ( +
    + + + {__("Context")} + {__("Meetings")} + + +
    + ); +} diff --git a/apps/console/src/pages/organizations/context/ContextPage.tsx b/apps/console/src/pages/organizations/context/ContextPage.tsx new file mode 100644 index 000000000..043f6f949 --- /dev/null +++ b/apps/console/src/pages/organizations/context/ContextPage.tsx @@ -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 ( +
    + {sections.map(section => ( + + ))} +
    + ); +} + +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( + 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 ( + + {isEditing + ? ( +
    +
    +

    {section.title}

    +

    + {section.description} +

    +
    +