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:
@@ -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"),
|
||||
});
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
254
apps/console/src/pages/organizations/context/ContextPage.tsx
Normal file
254
apps/console/src/pages/organizations/context/ContextPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
114
apps/console/src/routes/contextRoutes.ts
Normal file
114
apps/console/src/routes/contextRoutes.ts
Normal 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[];
|
||||
@@ -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[];
|
||||
@@ -145,7 +145,11 @@ func TestOrganization_UpdateContext(t *testing.T) {
|
||||
updateOrganizationContext(input: $input) {
|
||||
context {
|
||||
organizationId
|
||||
summary
|
||||
product
|
||||
architecture
|
||||
team
|
||||
processes
|
||||
customers
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +159,11 @@ func TestOrganization_UpdateContext(t *testing.T) {
|
||||
UpdateOrganizationContext struct {
|
||||
Context struct {
|
||||
OrganizationID string `json:"organizationId"`
|
||||
Summary *string `json:"summary"`
|
||||
Product *string `json:"product"`
|
||||
Architecture *string `json:"architecture"`
|
||||
Team *string `json:"team"`
|
||||
Processes *string `json:"processes"`
|
||||
Customers *string `json:"customers"`
|
||||
} `json:"context"`
|
||||
} `json:"updateOrganizationContext"`
|
||||
}
|
||||
@@ -163,14 +171,17 @@ func TestOrganization_UpdateContext(t *testing.T) {
|
||||
err := owner.Execute(query, map[string]any{
|
||||
"input": map[string]any{
|
||||
"organizationId": owner.GetOrganizationID().String(),
|
||||
"summary": "Our organization provides compliance solutions.",
|
||||
"product": "Our product provides compliance solutions.",
|
||||
"architecture": "Microservices architecture on AWS.",
|
||||
},
|
||||
}, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, owner.GetOrganizationID().String(), result.UpdateOrganizationContext.Context.OrganizationID)
|
||||
require.NotNil(t, result.UpdateOrganizationContext.Context.Summary)
|
||||
assert.Equal(t, "Our organization provides compliance solutions.", *result.UpdateOrganizationContext.Context.Summary)
|
||||
require.NotNil(t, result.UpdateOrganizationContext.Context.Product)
|
||||
assert.Equal(t, "Our product provides compliance solutions.", *result.UpdateOrganizationContext.Context.Product)
|
||||
require.NotNil(t, result.UpdateOrganizationContext.Context.Architecture)
|
||||
assert.Equal(t, "Microservices architecture on AWS.", *result.UpdateOrganizationContext.Context.Architecture)
|
||||
}
|
||||
|
||||
func TestOrganization_Get(t *testing.T) {
|
||||
|
||||
34
pkg/cmd/context/context.go
Normal file
34
pkg/cmd/context/context.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
"go.probo.inc/probo/pkg/cmd/context/get"
|
||||
"go.probo.inc/probo/pkg/cmd/context/update"
|
||||
)
|
||||
|
||||
func NewCmdContext(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "context <command>",
|
||||
Short: "Manage organization context",
|
||||
}
|
||||
|
||||
cmd.AddCommand(get.NewCmdGet(f))
|
||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
155
pkg/cmd/context/get/get.go
Normal file
155
pkg/cmd/context/get/get.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const getQuery = `
|
||||
query($id: ID!) {
|
||||
node(id: $id) {
|
||||
... on Organization {
|
||||
id
|
||||
name
|
||||
context {
|
||||
product
|
||||
architecture
|
||||
team
|
||||
processes
|
||||
customers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type getResponse struct {
|
||||
Node *struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Context *struct {
|
||||
Product *string `json:"product"`
|
||||
Architecture *string `json:"architecture"`
|
||||
Team *string `json:"team"`
|
||||
Processes *string `json:"processes"`
|
||||
Customers *string `json:"customers"`
|
||||
} `json:"context"`
|
||||
} `json:"node"`
|
||||
}
|
||||
|
||||
func NewCmdGet(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagOrg string
|
||||
flagOutput *string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get organization context",
|
||||
Example: ` prb context get --org <org-id>`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID := flagOrg
|
||||
if orgID == "" {
|
||||
orgID = hc.Organization
|
||||
}
|
||||
if orgID == "" {
|
||||
return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`")
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
data, err := client.Do(
|
||||
getQuery,
|
||||
map[string]any{"id": orgID},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp getResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
if resp.Node == nil {
|
||||
return fmt.Errorf("organization %s not found", orgID)
|
||||
}
|
||||
|
||||
if *flagOutput == cmdutil.OutputJSON {
|
||||
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node.Context)
|
||||
}
|
||||
|
||||
ctx := resp.Node.Context
|
||||
out := f.IOStreams.Out
|
||||
|
||||
bold := lipgloss.NewStyle().Bold(true)
|
||||
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242"))
|
||||
|
||||
sections := []struct {
|
||||
title string
|
||||
value *string
|
||||
}{
|
||||
{"Product", ctx.Product},
|
||||
{"Architecture", ctx.Architecture},
|
||||
{"Team", ctx.Team},
|
||||
{"Processes", ctx.Processes},
|
||||
{"Customers", ctx.Customers},
|
||||
}
|
||||
|
||||
for _, s := range sections {
|
||||
_, _ = fmt.Fprintf(out, "%s\n", bold.Render(s.title))
|
||||
if s.value != nil && *s.value != "" {
|
||||
_, _ = fmt.Fprintf(out, "%s\n\n", *s.value)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(out, "%s\n\n", label.Render("(empty)"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||
flagOutput = cmdutil.AddOutputFlag(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
151
pkg/cmd/context/update/update.go
Normal file
151
pkg/cmd/context/update/update.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const updateMutation = `
|
||||
mutation($input: UpdateOrganizationContextInput!) {
|
||||
updateOrganizationContext(input: $input) {
|
||||
context {
|
||||
organizationId
|
||||
product
|
||||
architecture
|
||||
team
|
||||
processes
|
||||
customers
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type updateResponse struct {
|
||||
UpdateOrganizationContext struct {
|
||||
Context struct {
|
||||
OrganizationID string `json:"organizationId"`
|
||||
Product *string `json:"product"`
|
||||
Architecture *string `json:"architecture"`
|
||||
Team *string `json:"team"`
|
||||
Processes *string `json:"processes"`
|
||||
Customers *string `json:"customers"`
|
||||
} `json:"context"`
|
||||
} `json:"updateOrganizationContext"`
|
||||
}
|
||||
|
||||
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagOrg string
|
||||
flagProduct string
|
||||
flagArchitecture string
|
||||
flagTeam string
|
||||
flagProcesses string
|
||||
flagCustomers string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update organization context",
|
||||
Example: ` prb context update --org <org-id> --product "We build compliance software"
|
||||
prb context update --org <org-id> --architecture "Monolith deployed on AWS ECS"`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID := flagOrg
|
||||
if orgID == "" {
|
||||
orgID = hc.Organization
|
||||
}
|
||||
if orgID == "" {
|
||||
return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`")
|
||||
}
|
||||
|
||||
input := map[string]any{
|
||||
"organizationId": orgID,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("product") {
|
||||
input["product"] = flagProduct
|
||||
}
|
||||
if cmd.Flags().Changed("architecture") {
|
||||
input["architecture"] = flagArchitecture
|
||||
}
|
||||
if cmd.Flags().Changed("team") {
|
||||
input["team"] = flagTeam
|
||||
}
|
||||
if cmd.Flags().Changed("processes") {
|
||||
input["processes"] = flagProcesses
|
||||
}
|
||||
if cmd.Flags().Changed("customers") {
|
||||
input["customers"] = flagCustomers
|
||||
}
|
||||
|
||||
if len(input) == 1 {
|
||||
return fmt.Errorf("at least one section flag is required (--product, --architecture, --team, --processes, --customers)")
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
data, err := client.Do(
|
||||
updateMutation,
|
||||
map[string]any{"input": input},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp updateResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Updated context for organization %s\n",
|
||||
resp.UpdateOrganizationContext.Context.OrganizationID,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||
cmd.Flags().StringVar(&flagProduct, "product", "", "Product description (markdown)")
|
||||
cmd.Flags().StringVar(&flagArchitecture, "architecture", "", "Architecture description (markdown)")
|
||||
cmd.Flags().StringVar(&flagTeam, "team", "", "Team description (markdown)")
|
||||
cmd.Flags().StringVar(&flagProcesses, "processes", "", "Processes description (markdown)")
|
||||
cmd.Flags().StringVar(&flagCustomers, "customers", "", "Customers description (markdown)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
"go.probo.inc/probo/pkg/cmd/completion"
|
||||
cmdconfig "go.probo.inc/probo/pkg/cmd/config"
|
||||
cmdcontext "go.probo.inc/probo/pkg/cmd/context"
|
||||
"go.probo.inc/probo/pkg/cmd/control"
|
||||
"go.probo.inc/probo/pkg/cmd/finding"
|
||||
"go.probo.inc/probo/pkg/cmd/framework"
|
||||
@@ -68,6 +69,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(browse.NewCmdBrowse(f))
|
||||
cmd.AddCommand(completion.NewCmdCompletion(f))
|
||||
cmd.AddCommand(cmdconfig.NewCmdConfig(f))
|
||||
cmd.AddCommand(cmdcontext.NewCmdContext(f))
|
||||
cmd.AddCommand(control.NewCmdControl(f))
|
||||
cmd.AddCommand(finding.NewCmdFinding(f))
|
||||
cmd.AddCommand(framework.NewCmdFramework(f))
|
||||
|
||||
6
pkg/coredata/migrations/20260319T140000Z.sql
Normal file
6
pkg/coredata/migrations/20260319T140000Z.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE organization_contexts ADD COLUMN product TEXT;
|
||||
ALTER TABLE organization_contexts ADD COLUMN architecture TEXT;
|
||||
ALTER TABLE organization_contexts ADD COLUMN team TEXT;
|
||||
ALTER TABLE organization_contexts ADD COLUMN processes TEXT;
|
||||
ALTER TABLE organization_contexts ADD COLUMN customers TEXT;
|
||||
ALTER TABLE organization_contexts DROP COLUMN summary;
|
||||
@@ -29,7 +29,11 @@ import (
|
||||
type (
|
||||
OrganizationContext struct {
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Summary *string `db:"summary"`
|
||||
Product *string `db:"product"`
|
||||
Architecture *string `db:"architecture"`
|
||||
Team *string `db:"team"`
|
||||
Processes *string `db:"processes"`
|
||||
Customers *string `db:"customers"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
@@ -44,7 +48,11 @@ func (oc *OrganizationContext) LoadByOrganizationID(
|
||||
q := `
|
||||
SELECT
|
||||
organization_id,
|
||||
summary,
|
||||
product,
|
||||
architecture,
|
||||
team,
|
||||
processes,
|
||||
customers,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -88,13 +96,21 @@ func (oc *OrganizationContext) Insert(
|
||||
INSERT INTO organization_contexts (
|
||||
organization_id,
|
||||
tenant_id,
|
||||
summary,
|
||||
product,
|
||||
architecture,
|
||||
team,
|
||||
processes,
|
||||
customers,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@summary,
|
||||
@product,
|
||||
@architecture,
|
||||
@team,
|
||||
@processes,
|
||||
@customers,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
@@ -103,7 +119,11 @@ INSERT INTO organization_contexts (
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": oc.OrganizationID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"summary": oc.Summary,
|
||||
"product": oc.Product,
|
||||
"architecture": oc.Architecture,
|
||||
"team": oc.Team,
|
||||
"processes": oc.Processes,
|
||||
"customers": oc.Customers,
|
||||
"created_at": oc.CreatedAt,
|
||||
"updated_at": oc.UpdatedAt,
|
||||
}
|
||||
@@ -124,7 +144,11 @@ func (oc *OrganizationContext) Update(
|
||||
q := `
|
||||
UPDATE organization_contexts
|
||||
SET
|
||||
summary = @summary,
|
||||
product = @product,
|
||||
architecture = @architecture,
|
||||
team = @team,
|
||||
processes = @processes,
|
||||
customers = @customers,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
%s
|
||||
@@ -135,7 +159,11 @@ WHERE
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": oc.OrganizationID,
|
||||
"summary": oc.Summary,
|
||||
"product": oc.Product,
|
||||
"architecture": oc.Architecture,
|
||||
"team": oc.Team,
|
||||
"processes": oc.Processes,
|
||||
"customers": oc.Customers,
|
||||
"updated_at": oc.UpdatedAt,
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,11 @@ type (
|
||||
|
||||
UpdateOrganizationContextRequest struct {
|
||||
OrganizationID gid.GID
|
||||
Summary **string
|
||||
Product **string
|
||||
Architecture **string
|
||||
Team **string
|
||||
Processes **string
|
||||
Customers **string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -72,7 +76,11 @@ func (uocr *UpdateOrganizationContextRequest) Validate() error {
|
||||
v := validator.New()
|
||||
|
||||
v.Check(uocr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
|
||||
v.Check(uocr.Summary, "summary", validator.SafeText(30_000))
|
||||
v.Check(uocr.Product, "product", validator.SafeText(30_000))
|
||||
v.Check(uocr.Architecture, "architecture", validator.SafeText(30_000))
|
||||
v.Check(uocr.Team, "team", validator.SafeText(30_000))
|
||||
v.Check(uocr.Processes, "processes", validator.SafeText(30_000))
|
||||
v.Check(uocr.Customers, "customers", validator.SafeText(30_000))
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
@@ -102,7 +110,7 @@ func (s OrganizationService) Get(
|
||||
return organization, nil
|
||||
}
|
||||
|
||||
func (s OrganizationService) GetContextSummary(
|
||||
func (s OrganizationService) GetContext(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
) (*coredata.OrganizationContext, error) {
|
||||
@@ -154,8 +162,34 @@ func (s OrganizationService) UpdateContext(
|
||||
return fmt.Errorf("cannot load organization context: %w", err)
|
||||
}
|
||||
|
||||
if req.Summary != nil {
|
||||
organizationContext.Summary = *req.Summary
|
||||
updated := false
|
||||
|
||||
if req.Product != nil {
|
||||
organizationContext.Product = *req.Product
|
||||
updated = true
|
||||
}
|
||||
|
||||
if req.Architecture != nil {
|
||||
organizationContext.Architecture = *req.Architecture
|
||||
updated = true
|
||||
}
|
||||
|
||||
if req.Team != nil {
|
||||
organizationContext.Team = *req.Team
|
||||
updated = true
|
||||
}
|
||||
|
||||
if req.Processes != nil {
|
||||
organizationContext.Processes = *req.Processes
|
||||
updated = true
|
||||
}
|
||||
|
||||
if req.Customers != nil {
|
||||
organizationContext.Customers = *req.Customers
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
organizationContext.UpdatedAt = time.Now()
|
||||
|
||||
if err := organizationContext.Update(ctx, tx, s.svc.scope); err != nil {
|
||||
|
||||
@@ -3846,7 +3846,11 @@ type Mutation {
|
||||
# Input Types
|
||||
input UpdateOrganizationContextInput {
|
||||
organizationId: ID!
|
||||
summary: String @goField(omittable: true)
|
||||
product: String @goField(omittable: true)
|
||||
architecture: String @goField(omittable: true)
|
||||
team: String @goField(omittable: true)
|
||||
processes: String @goField(omittable: true)
|
||||
customers: String @goField(omittable: true)
|
||||
}
|
||||
|
||||
input UpdateTrustCenterInput {
|
||||
@@ -4753,7 +4757,11 @@ type UpdateOrganizationContextPayload {
|
||||
|
||||
type OrganizationContext {
|
||||
organizationId: ID!
|
||||
summary: String
|
||||
product: String
|
||||
architecture: String
|
||||
team: String
|
||||
processes: String
|
||||
customers: String
|
||||
}
|
||||
|
||||
type UpdateTrustCenterPayload {
|
||||
|
||||
@@ -21,6 +21,10 @@ import (
|
||||
func NewOrganizationContext(oc *coredata.OrganizationContext) *OrganizationContext {
|
||||
return &OrganizationContext{
|
||||
OrganizationID: oc.OrganizationID,
|
||||
Summary: oc.Summary,
|
||||
Product: oc.Product,
|
||||
Architecture: oc.Architecture,
|
||||
Team: oc.Team,
|
||||
Processes: oc.Processes,
|
||||
Customers: oc.Customers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2048,7 +2048,11 @@ func (r *mutationResolver) UpdateOrganizationContext(ctx context.Context, input
|
||||
|
||||
req := probo.UpdateOrganizationContextRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Summary: gqlutils.UnwrapOmittable(input.Summary),
|
||||
Product: gqlutils.UnwrapOmittable(input.Product),
|
||||
Architecture: gqlutils.UnwrapOmittable(input.Architecture),
|
||||
Team: gqlutils.UnwrapOmittable(input.Team),
|
||||
Processes: gqlutils.UnwrapOmittable(input.Processes),
|
||||
Customers: gqlutils.UnwrapOmittable(input.Customers),
|
||||
}
|
||||
|
||||
organizationContext, err := prb.Organizations.UpdateContext(ctx, req)
|
||||
@@ -6387,7 +6391,7 @@ func (r *organizationResolver) Context(ctx context.Context, obj *types.Organizat
|
||||
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
|
||||
orgContext, err := prb.Organizations.GetContextSummary(ctx, obj.ID)
|
||||
orgContext, err := prb.Organizations.GetContext(ctx, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load organization context", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
|
||||
@@ -3241,3 +3241,43 @@ func (r *Resolver) UnarchiveDocumentTool(ctx context.Context, req *mcp.CallToolR
|
||||
Document: types.NewDocument(document, profileIDs(approverPage)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) GetOrganizationContextTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetOrganizationContextInput) (*mcp.CallToolResult, types.GetOrganizationContextOutput, error) {
|
||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionOrganizationContextGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
orgContext, err := prb.Organizations.GetContext(ctx, input.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, types.GetOrganizationContextOutput{}, fmt.Errorf("cannot get organization context: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.GetOrganizationContextOutput{
|
||||
OrganizationContext: types.NewOrganizationContext(orgContext),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateOrganizationContextInput) (*mcp.CallToolResult, types.UpdateOrganizationContextOutput, error) {
|
||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionOrganizationContextUpdate)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
orgContext, err := prb.Organizations.UpdateContext(
|
||||
ctx,
|
||||
probo.UpdateOrganizationContextRequest{
|
||||
OrganizationID: input.OrganizationID,
|
||||
Product: &input.Product,
|
||||
Architecture: &input.Architecture,
|
||||
Team: &input.Team,
|
||||
Processes: &input.Processes,
|
||||
Customers: &input.Customers,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, types.UpdateOrganizationContextOutput{}, fmt.Errorf("cannot update organization context: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.UpdateOrganizationContextOutput{
|
||||
OrganizationContext: types.NewOrganizationContext(orgContext),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -6323,6 +6323,79 @@ components:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Deleted applicability statement ID
|
||||
|
||||
OrganizationContext:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
product:
|
||||
type: string
|
||||
description: Product description
|
||||
architecture:
|
||||
type: string
|
||||
description: Architecture description
|
||||
team:
|
||||
type: string
|
||||
description: Team description
|
||||
processes:
|
||||
type: string
|
||||
description: Processes description
|
||||
customers:
|
||||
type: string
|
||||
description: Customers description
|
||||
|
||||
GetOrganizationContextInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
|
||||
GetOrganizationContextOutput:
|
||||
type: object
|
||||
required:
|
||||
- organization_context
|
||||
properties:
|
||||
organization_context:
|
||||
$ref: "#/components/schemas/OrganizationContext"
|
||||
|
||||
UpdateOrganizationContextInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
product:
|
||||
type: string
|
||||
description: Product description
|
||||
architecture:
|
||||
type: string
|
||||
description: Architecture description
|
||||
team:
|
||||
type: string
|
||||
description: Team description
|
||||
processes:
|
||||
type: string
|
||||
description: Processes description
|
||||
customers:
|
||||
type: string
|
||||
description: Customers description
|
||||
|
||||
UpdateOrganizationContextOutput:
|
||||
type: object
|
||||
required:
|
||||
- organization_context
|
||||
properties:
|
||||
organization_context:
|
||||
$ref: "#/components/schemas/OrganizationContext"
|
||||
|
||||
tools:
|
||||
- name: listOrganizations
|
||||
description: List all organizations the user has access to
|
||||
@@ -7441,3 +7514,20 @@ tools:
|
||||
$ref: "#/components/schemas/DeleteApplicabilityStatementInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/DeleteApplicabilityStatementOutput"
|
||||
- name: getOrganizationContext
|
||||
description: Get the organization context containing structured sections about the company
|
||||
hints:
|
||||
readonly: true
|
||||
idempotent: true
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/GetOrganizationContextInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/GetOrganizationContextOutput"
|
||||
- name: updateOrganizationContext
|
||||
description: Update the organization context sections (product, architecture, team, processes, customers)
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/UpdateOrganizationContextInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/UpdateOrganizationContextOutput"
|
||||
|
||||
30
pkg/server/api/mcp/v1/types/organization_context.go
Normal file
30
pkg/server/api/mcp/v1/types/organization_context.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func NewOrganizationContext(oc *coredata.OrganizationContext) *OrganizationContext {
|
||||
return &OrganizationContext{
|
||||
OrganizationID: oc.OrganizationID,
|
||||
Product: oc.Product,
|
||||
Architecture: oc.Architecture,
|
||||
Team: oc.Team,
|
||||
Processes: oc.Processes,
|
||||
Customers: oc.Customers,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user