Remove meeting feature
Drop meetings and meeting_attendees tables, remove all meeting-related code across GraphQL, MCP, CLI, N8N, webhooks, frontend, and e2e tests. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -225,7 +225,7 @@ export function PersonForm(props: {
|
||||
{watchedRole === "VIEWER" && <p>{__("Read-only access")}</p>}
|
||||
{watchedRole === "AUDITOR" && (
|
||||
<p>
|
||||
{__("Read-only access without settings, tasks and meetings")}
|
||||
{__("Read-only access without settings and tasks")}
|
||||
</p>
|
||||
)}
|
||||
{watchedRole === "EMPLOYEE" && (
|
||||
|
||||
@@ -14,15 +14,11 @@
|
||||
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { PageHeader, TabLink, Tabs } from "@probo/ui";
|
||||
import { PageHeader } from "@probo/ui";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
export default function ContextLayoutLoader() {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const prefix = `/organizations/${organizationId}/context`;
|
||||
|
||||
usePageTitle(__("Context"));
|
||||
|
||||
@@ -31,13 +27,9 @@ export default function ContextLayoutLoader() {
|
||||
<PageHeader
|
||||
title={__("Context")}
|
||||
description={__(
|
||||
"Structured company information and meetings for AI assistants and compliance workflows.",
|
||||
"Structured company information for AI assistants and compliance workflows.",
|
||||
)}
|
||||
/>
|
||||
<Tabs>
|
||||
<TabLink to={prefix} end>{__("Context")}</TabLink>
|
||||
<TabLink to={`${prefix}/meetings`}>{__("Meetings")}</TabLink>
|
||||
</Tabs>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import { formatDate, sprintf } from "@probo/helpers";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Avatar,
|
||||
Breadcrumb,
|
||||
DropdownItem,
|
||||
IconPencil,
|
||||
IconTrashCan,
|
||||
PageHeader,
|
||||
useConfirm,
|
||||
} from "@probo/ui";
|
||||
import { useRef } from "react";
|
||||
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 { MeetingDetailPageQuery } from "#/__generated__/core/MeetingDetailPageQuery.graphql";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import {
|
||||
UpdateMeetingMinutesDialog,
|
||||
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
|
||||
name
|
||||
date
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
minutes
|
||||
canUpdate: permission(action: "core:meeting:update")
|
||||
canDelete: permission(action: "core:meeting:delete")
|
||||
attendees {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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<MeetingDetailPageQuery>;
|
||||
};
|
||||
|
||||
export default function MeetingDetailPage(props: Props) {
|
||||
const node = usePreloadedQuery(meetingDetailPageQuery, props.queryRef).node;
|
||||
const meeting = useFragment<MeetingDetailPageMeetingFragment$key>(
|
||||
meetingFragment,
|
||||
node,
|
||||
);
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [deleteMeeting, isDeleting] = useDeleteMeetingMutation();
|
||||
const confirm = useConfirm();
|
||||
const updateMinutesDialogRef = useRef<UpdateMeetingMinutesDialogRef>(null);
|
||||
|
||||
usePageTitle(meeting.name);
|
||||
|
||||
const hasAnyAction = meeting.canUpdate || meeting.canDelete;
|
||||
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
() =>
|
||||
deleteMeeting({
|
||||
variables: {
|
||||
input: { meetingId: meeting.id },
|
||||
},
|
||||
onSuccess: () => {
|
||||
void navigate(`/organizations/${organizationId}/context/meetings`);
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
"This will permanently delete the meeting \"%s\". This action cannot be undone.",
|
||||
),
|
||||
meeting.name,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateMeetingMinutesDialog
|
||||
ref={updateMinutesDialogRef}
|
||||
meeting={meeting}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: __("Meetings"),
|
||||
to: `/organizations/${organizationId}/context/meetings`,
|
||||
},
|
||||
{
|
||||
label: meeting.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{hasAnyAction && (
|
||||
<ActionDropdown variant="secondary">
|
||||
{meeting.canUpdate && (
|
||||
<DropdownItem
|
||||
onClick={() => updateMinutesDialogRef.current?.open()}
|
||||
icon={IconPencil}
|
||||
>
|
||||
{__("Edit minutes")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{meeting.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{__("Delete meeting")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
)}
|
||||
</div>
|
||||
<PageHeader
|
||||
title={meeting.name}
|
||||
description={formatDate(meeting.date)}
|
||||
/>
|
||||
{meeting.attendees && meeting.attendees.length > 0 && (
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
{meeting.attendees.map(attendee => (
|
||||
<div key={attendee.id} className="flex gap-2 items-center">
|
||||
<Avatar name={attendee.fullName ?? ""} />
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/people/${attendee.id}`}
|
||||
className="text-sm hover:underline"
|
||||
>
|
||||
{attendee.fullName}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Outlet context={{ meeting }} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// 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.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
import type { MeetingDetailPageQuery } from "#/__generated__/core/MeetingDetailPageQuery.graphql";
|
||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
|
||||
|
||||
import MeetingDetailPage, { meetingDetailPageQuery } from "./MeetingDetailPage";
|
||||
|
||||
function MeetingDetailPageQueryLoader() {
|
||||
const { meetingId } = useParams<{ meetingId: string }>();
|
||||
const [queryRef, loadQuery] = useQueryLoader<MeetingDetailPageQuery>(meetingDetailPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (meetingId) {
|
||||
loadQuery({ meetingId });
|
||||
}
|
||||
}, [meetingId, loadQuery]);
|
||||
|
||||
if (!queryRef) return <PageSkeleton />;
|
||||
|
||||
return <MeetingDetailPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function MeetingDetailPageLoader() {
|
||||
return (
|
||||
<CoreRelayProvider>
|
||||
<MeetingDetailPageQueryLoader />
|
||||
</CoreRelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import { formatDate, sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
DropdownItem,
|
||||
IconPlusLarge,
|
||||
IconTrashCan,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirm,
|
||||
} from "@probo/ui";
|
||||
import {
|
||||
type PreloadedQuery,
|
||||
useFragment,
|
||||
usePaginationFragment,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { Link } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
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 { 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")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 50 }
|
||||
order: {
|
||||
type: "MeetingOrder"
|
||||
defaultValue: { field: DATE, direction: DESC }
|
||||
}
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
id
|
||||
canCreateMeeting: permission(action: "core:meeting:create")
|
||||
meetings(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "MeetingsListQuery_meetings") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...MeetingsPageRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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<MeetingsPageQuery>;
|
||||
};
|
||||
|
||||
export default function MeetingsPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organization = usePreloadedQuery(
|
||||
meetingsPageQuery,
|
||||
props.queryRef,
|
||||
).organization;
|
||||
|
||||
// eslint-disable-next-line relay/generated-typescript-types
|
||||
const pagination = usePaginationFragment(
|
||||
meetingsFragment,
|
||||
organization as MeetingsPageListFragment$key,
|
||||
);
|
||||
|
||||
const meetingNodes = pagination.data.meetings.edges
|
||||
.map(edge => edge.node)
|
||||
.filter(Boolean);
|
||||
const connectionId = pagination.data.meetings.__id;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{pagination.data.canCreateMeeting && (
|
||||
<div className="flex justify-end">
|
||||
<CreateMeetingDialog connectionId={connectionId}>
|
||||
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
|
||||
</CreateMeetingDialog>
|
||||
</div>
|
||||
)}
|
||||
{meetingNodes.length > 0
|
||||
? (
|
||||
<SortableTable {...pagination}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="DATE" className="w-40">
|
||||
{__("Date")}
|
||||
</SortableTh>
|
||||
<SortableTh field="NAME" className="min-w-0">
|
||||
{__("Meeting name")}
|
||||
</SortableTh>
|
||||
<Th className="w-60">{__("Attendees")}</Th>
|
||||
<Th className="w-18"></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{meetingNodes.map(meeting => (
|
||||
<MeetingRow
|
||||
key={meeting.id}
|
||||
meeting={meeting}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)
|
||||
: (
|
||||
<Card padded>
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{__("No meetings yet")}
|
||||
</h3>
|
||||
<p className="text-txt-tertiary mb-4">
|
||||
{__("Create your first meeting to get started.")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowFragment = graphql`
|
||||
fragment MeetingsPageRowFragment on Meeting {
|
||||
id
|
||||
name
|
||||
date
|
||||
attendees {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
canDelete: permission(action: "core:meeting:delete")
|
||||
}
|
||||
`;
|
||||
|
||||
function MeetingRow({
|
||||
meeting: meetingKey,
|
||||
organizationId,
|
||||
}: {
|
||||
meeting: MeetingsPageRowFragment$key;
|
||||
organizationId: string;
|
||||
}) {
|
||||
const meeting = useFragment<MeetingsPageRowFragment$key>(
|
||||
rowFragment,
|
||||
meetingKey,
|
||||
);
|
||||
const { __ } = useTranslate();
|
||||
const [deleteMeeting] = useDeleteMeetingMutation();
|
||||
const confirm = useConfirm();
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
() =>
|
||||
deleteMeeting({
|
||||
variables: {
|
||||
input: { meetingId: meeting.id },
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
"This will permanently delete the meeting \"%s\". This action cannot be undone.",
|
||||
),
|
||||
meeting.name,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
</Td>
|
||||
<Td className="w-60">
|
||||
{meeting.attendees && meeting.attendees.length > 0
|
||||
? (
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
{meeting.attendees.map(attendee => (
|
||||
<div key={attendee.id} className="flex gap-2 items-center">
|
||||
<Avatar name={attendee.fullName ?? ""} />
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/people/${attendee.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="text-sm hover:underline"
|
||||
>
|
||||
{attendee.fullName}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<span className="text-txt-tertiary text-sm">
|
||||
{__("No attendees")}
|
||||
</span>
|
||||
)}
|
||||
</Td>
|
||||
{meeting.canDelete && (
|
||||
<Td noLink width={50} className="text-end w-18">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
|
||||
import type { MeetingsPageQuery } from "#/__generated__/core/MeetingsPageQuery.graphql";
|
||||
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
|
||||
|
||||
import MeetingsPage, { meetingsPageQuery } from "./MeetingsPage";
|
||||
|
||||
function MeetingsPageQueryLoader() {
|
||||
const organizationId = useOrganizationId();
|
||||
const [queryRef, loadQuery] = useQueryLoader<MeetingsPageQuery>(meetingsPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId });
|
||||
}, [organizationId, loadQuery]);
|
||||
|
||||
if (!queryRef) return <LinkCardSkeleton />;
|
||||
|
||||
return <MeetingsPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function MeetingsPageLoader() {
|
||||
return (
|
||||
<CoreRelayProvider>
|
||||
<MeetingsPageQueryLoader />
|
||||
</CoreRelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import { formatDatetime } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Field,
|
||||
Input,
|
||||
Spinner,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CreateMeetingDialogCreateMutation } from "#/__generated__/core/CreateMeetingDialogCreateMutation.graphql";
|
||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
const createMeetingMutation = graphql`
|
||||
mutation CreateMeetingDialogCreateMutation(
|
||||
$input: CreateMeetingInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createMeeting(input: $input) {
|
||||
meetingEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
date
|
||||
minutes
|
||||
attendees {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
canDelete: permission(action: "core:meeting:delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: React.ReactElement;
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
const meetingSchema = z.object({
|
||||
name: z.string().min(1, "Meeting name is required"),
|
||||
date: z.string().min(1, "Date is required"),
|
||||
attendeeIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export function CreateMeetingDialog({ children, connectionId }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useDialogRef();
|
||||
const organizationId = useOrganizationId();
|
||||
const [createMeeting, isCreating]
|
||||
= useMutation<CreateMeetingDialogCreateMutation>(createMeetingMutation);
|
||||
const { handleSubmit, register, control } = useFormWithSchema(
|
||||
meetingSchema,
|
||||
{},
|
||||
);
|
||||
|
||||
const onSubmit = (data: z.infer<typeof meetingSchema>) => {
|
||||
createMeeting({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
name: data.name,
|
||||
date: formatDatetime(data.date)!,
|
||||
attendeeIds: data.attendeeIds || null,
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted: () => {
|
||||
dialogRef.current?.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog ref={dialogRef} trigger={children} title={__("Create meeting")}>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<Field label={__("Meeting name")} required>
|
||||
<Input
|
||||
{...register("name")}
|
||||
placeholder={__("Enter meeting name")}
|
||||
autoFocus
|
||||
/>
|
||||
</Field>
|
||||
<Field label={__("Date")} required>
|
||||
<Input
|
||||
{...register("date")}
|
||||
type="date"
|
||||
placeholder={__("Select date")}
|
||||
/>
|
||||
</Field>
|
||||
<PeopleMultiSelectField
|
||||
name="attendeeIds"
|
||||
control={control}
|
||||
organizationId={organizationId}
|
||||
label={__("Attendees")}
|
||||
placeholder={__("Add attendees...")}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button disabled={isCreating} type="submit">
|
||||
{isCreating && <Spinner />}
|
||||
{__("Create meeting")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Spinner,
|
||||
Textarea,
|
||||
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 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;
|
||||
};
|
||||
|
||||
export type UpdateMeetingMinutesDialogRef = {
|
||||
open: () => void;
|
||||
};
|
||||
|
||||
const minutesSchema = z.object({
|
||||
minutes: z.string(),
|
||||
});
|
||||
|
||||
export const UpdateMeetingMinutesDialog = forwardRef<
|
||||
UpdateMeetingMinutesDialogRef,
|
||||
Props
|
||||
>(function UpdateMeetingMinutesDialog({ meeting }, ref) {
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useDialogRef();
|
||||
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 || "",
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
reset({
|
||||
minutes: meeting.minutes || "",
|
||||
});
|
||||
dialogRef.current?.open();
|
||||
},
|
||||
}));
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof minutesSchema>) => {
|
||||
await updateMeeting({
|
||||
variables: {
|
||||
input: {
|
||||
meetingId: meeting.id,
|
||||
minutes: data.minutes,
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
dialogRef.current?.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
title={<Breadcrumb items={[__("Meetings"), __("Edit minutes")]} />}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent>
|
||||
<Textarea
|
||||
id="minutes"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
placeholder={__("Add meeting minutes")}
|
||||
aria-label={__("Minutes")}
|
||||
className="p-6"
|
||||
{...register("minutes")}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button disabled={isUpdating} type="submit">
|
||||
{isUpdating && <Spinner />}
|
||||
{__("Update minutes")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2025-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.
|
||||
|
||||
import { Markdown } from "@probo/ui";
|
||||
import { useOutletContext } from "react-router";
|
||||
|
||||
import type { MeetingDetailPageMeetingFragment$data } from "#/__generated__/core/MeetingDetailPageMeetingFragment.graphql";
|
||||
|
||||
export default function MeetingMinutesTab() {
|
||||
const { meeting } = useOutletContext<{
|
||||
meeting: MeetingDetailPageMeetingFragment$data;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{meeting.minutes
|
||||
? (
|
||||
<Markdown content={meeting.minutes} />
|
||||
)
|
||||
: (
|
||||
<div className="text-txt-tertiary text-sm">
|
||||
{"No minutes recorded yet. Click \"Edit minutes\" to add meeting minutes."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,9 +155,6 @@ const deleteWebhookSubscriptionMutation = graphql`
|
||||
`;
|
||||
|
||||
const EVENT_TYPES = [
|
||||
{ value: "MEETING_CREATED", label: "meeting:created" },
|
||||
{ value: "MEETING_UPDATED", label: "meeting:updated" },
|
||||
{ value: "MEETING_DELETED", label: "meeting:deleted" },
|
||||
{ value: "VENDOR_CREATED", label: "vendor:created" },
|
||||
{ value: "VENDOR_UPDATED", label: "vendor:updated" },
|
||||
{ value: "VENDOR_DELETED", label: "vendor:deleted" },
|
||||
|
||||
@@ -33,29 +33,6 @@ export const contextRoutes = [
|
||||
() => import("#/pages/organizations/context/ContextPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "meetings",
|
||||
Fallback: LinkCardSkeleton,
|
||||
Component: lazy(
|
||||
() => import("#/pages/organizations/meetings/MeetingsPageLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "meetings/:meetingId",
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(
|
||||
() => import("#/pages/organizations/meetings/MeetingDetailPageLoader"),
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
Fallback: LinkCardSkeleton,
|
||||
Component: lazy(
|
||||
() => import("#/pages/organizations/meetings/tabs/MeetingMinutesTab"),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
Reference in New Issue
Block a user