Fix invitation list connection

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-05 13:24:49 +01:00
committed by Bryan Frimin
parent da8a670a68
commit 110cac6847
7 changed files with 104 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<37b59716fa1aba5ed805ce03278ce01d>>
* @generated SignedSource<<d415b10cc562b5554452cc3ed15bc31e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -111,7 +111,13 @@ return {
"kind": "RequiredField",
"field": {
"alias": "invitations",
"args": null,
"args": [
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "__InvitationListFragment_invitations_connection",
@@ -233,6 +239,6 @@ return {
};
})();
(node as any).hash = "6a1b77bc8b0e5f345de9865ec78ef7e9";
(node as any).hash = "d6f8750bb89b233418c3a66669c4aa23";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e8e0e8e378c0a308255a8e2f3918b758>>
* @generated SignedSource<<23d03caf79ac153cc19aae8e87c38a27>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -355,7 +355,9 @@ return {
{
"alias": null,
"args": (v13/*: any*/),
"filters": [],
"filters": [
"orderBy"
],
"handle": "connection",
"key": "InvitationListFragment_invitations",
"kind": "LinkedHandle",
@@ -381,6 +383,6 @@ return {
};
})();
(node as any).hash = "6a1b77bc8b0e5f345de9865ec78ef7e9";
(node as any).hash = "d6f8750bb89b233418c3a66669c4aa23";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0f7c89bd92b5743593efd542812568a7>>
* @generated SignedSource<<bd98e1c1ff5f9393deaec2a4aec1649c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -197,7 +197,10 @@ v17 = {
"storageKey": null
}
]
};
},
v18 = [
"orderBy"
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
@@ -411,9 +414,7 @@ return {
{
"alias": null,
"args": (v6/*: any*/),
"filters": [
"orderBy"
],
"filters": (v18/*: any*/),
"handle": "connection",
"key": "MemberListFragment_members",
"kind": "LinkedHandle",
@@ -499,7 +500,7 @@ return {
{
"alias": null,
"args": (v6/*: any*/),
"filters": [],
"filters": (v18/*: any*/),
"handle": "connection",
"key": "InvitationListFragment_invitations",
"kind": "LinkedHandle",

View File

@@ -18,7 +18,7 @@ import {
import type { LoadMoreFn } from "react-relay";
import type { OperationType } from "relay-runtime";
type Order = {
export type Order = {
direction: "ASC" | "DESC";
field: string;
};
@@ -30,7 +30,7 @@ export const SortableContext = createContext({
direction: "DESC",
field: "CREATED_AT",
},
onOrderChange: (() => {}) as (order: Order) => void,
changeOrder: (() => {}) as (order: Order) => void,
});
const defaultOrder = {
@@ -54,14 +54,14 @@ export function SortableTable({
}) {
const { __ } = useTranslate();
const [order, setOrder] = useState(defaultOrder);
const onOrderChange = (o: Order) => {
const changeOrder = (o: Order) => {
startTransition(() => {
setOrder(o);
refetch({ order: o });
});
};
return (
<SortableContext value={{ order, onOrderChange }}>
<SortableContext value={{ order, changeOrder }}>
<div className="space-y-4">
<Table {...props} />
{hasNext && loadNext && (
@@ -83,22 +83,29 @@ export function SortableTable({
export function SortableTh({
children,
field,
onOrderChange,
...props
}: ComponentProps<typeof Th> & { field: string }) {
const { order, onOrderChange } = useContext(SortableContext);
}: ComponentProps<typeof Th> & {
field: string;
onOrderChange?: (order: { direction: "ASC" | "DESC"; field: string }) => void;
}) {
const { order, changeOrder } = useContext(SortableContext);
const isCurrentField = order.field === field;
const isDesc = order.direction === "DESC";
const changeOrder = () => {
onOrderChange({
direction: isDesc && isCurrentField ? "ASC" : "DESC",
const handleChangeOrder = () => {
const newOrder = {
direction:
isDesc && isCurrentField ? ("ASC" as const) : ("DESC" as const),
field,
});
};
changeOrder(newOrder);
onOrderChange?.(newOrder);
};
return (
<Th {...props}>
<button
className="flex items-center cursor-pointer hover:text-txt-primary"
onClick={changeOrder}
onClick={handleChangeOrder}
>
{children}
<IconChevronTriangleDownSmall

View File

@@ -1,5 +1,5 @@
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { ConnectionHandler, graphql } from "relay-runtime";
import { MemberList } from "./_components/MemberList";
import type { MembersPageQuery } from "/__generated__/iam/MembersPageQuery.graphql";
import { useTranslate } from "@probo/i18n";
@@ -7,6 +7,7 @@ import { Button, Card, TabBadge, TabItem, Tabs } from "@probo/ui";
import { useState } from "react";
import { InvitationList } from "./_components/InvitationList";
import { InviteUserDialog } from "./_components/InviteUserDialog";
import { useOrganizationId } from "/hooks/useOrganizationId";
export const membersPageQuery = graphql`
query MembersPageQuery($organizationId: ID!) {
@@ -37,6 +38,7 @@ export function MembersPage(props: {
}) {
const { queryRef } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">(
@@ -51,12 +53,23 @@ export function MembersPage(props: {
throw new Error("node is of invalid type");
}
const [invitationListConnectionId, setInvitationListConnectionId] = useState(
ConnectionHandler.getConnectionID(
organizationId,
"InvitationListFragment_invitations",
{ orderBy: { direction: "ASC", field: "CREATED_AT" } },
),
);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
{organization.canInviteUser && (
<InviteUserDialog viewerMembershipFKey={organization}>
<InviteUserDialog
connectionId={invitationListConnectionId}
viewerMembershipFKey={organization}
>
<Button variant="secondary">{__("Invite member")}</Button>
</InviteUserDialog>
)}
@@ -88,7 +101,10 @@ export function MembersPage(props: {
{activeTab === "memberships" && <MemberList fKey={organization} />}
{activeTab === "invitations" && (
<InvitationList fKey={organization} />
<InvitationList
fKey={organization}
onConnectionIdChange={setInvitationListConnectionId}
/>
)}
</div>
</Card>

View File

@@ -1,11 +1,16 @@
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { SortableTable, SortableTh } from "/components/SortableTable";
import {
SortableTable,
SortableTh,
type Order,
} from "/components/SortableTable";
import { useTranslate } from "@probo/i18n";
import { graphql, usePaginationFragment } from "react-relay";
import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay";
import { InvitationListItem } from "./InvitationListItem";
import type { InvitationListFragment$key } from "/__generated__/iam/InvitationListFragment.graphql";
import type { InvitationListFragment_RefetchQuery } from "/__generated__/iam/InvitationListFragment_RefetchQuery.graphql";
import type { ComponentProps } from "react";
import { type ComponentProps } from "react";
import { useOrganizationId } from "/hooks/useOrganizationId";
const fragment = graphql`
fragment InvitationListFragment on Organization
@@ -27,7 +32,10 @@ const fragment = graphql`
before: $before
orderBy: $order
)
@connection(key: "InvitationListFragment_invitations", filters: [])
@connection(
key: "InvitationListFragment_invitations"
filters: ["orderBy"]
)
@required(action: THROW) {
__id
totalCount
@@ -41,9 +49,13 @@ const fragment = graphql`
}
`;
export function InvitationList(props: { fKey: InvitationListFragment$key }) {
const { fKey } = props;
export function InvitationList(props: {
fKey: InvitationListFragment$key;
onConnectionIdChange: (connectionId: string) => void;
}) {
const { fKey, onConnectionIdChange } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const invitationsPagination = usePaginationFragment<
@@ -55,6 +67,16 @@ export function InvitationList(props: { fKey: InvitationListFragment$key }) {
invitationsPagination.refetch({}, { fetchPolicy: "network-only" });
};
const handleOrderChange = (order: Order) => {
onConnectionIdChange(
ConnectionHandler.getConnectionID(
organizationId,
"InvitationListFragment_invitations",
{ orderBy: order },
),
);
};
return (
<SortableTable
{...invitationsPagination}
@@ -67,12 +89,22 @@ export function InvitationList(props: { fKey: InvitationListFragment$key }) {
>
<Thead>
<Tr>
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
<SortableTh field="ROLE">{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT">{__("Invited")}</SortableTh>
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>
{__("Name")}
</SortableTh>
<SortableTh field="EMAIL" onOrderChange={handleOrderChange}>
{__("Email")}
</SortableTh>
<SortableTh field="ROLE" onOrderChange={handleOrderChange}>
{__("Role")}
</SortableTh>
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>
{__("Invited")}
</SortableTh>
<Th>{__("Status")}</Th>
<SortableTh field="ACCEPTED_AT">{__("Accepted at")}</SortableTh>
<SortableTh field="ACCEPTED_AT" onOrderChange={handleOrderChange}>
{__("Accepted at")}
</SortableTh>
<Th></Th>
</Tr>
</Thead>

View File

@@ -12,7 +12,7 @@ import {
} from "@probo/ui";
import { Controller } from "react-hook-form";
import { useFragment } from "react-relay";
import { ConnectionHandler, graphql } from "relay-runtime";
import { graphql } from "relay-runtime";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
@@ -59,11 +59,12 @@ const schema = z.object({
});
type InviteUserDialogProps = PropsWithChildren<{
connectionId: string;
viewerMembershipFKey: InviteUserDialog_currentRoleFragment$key;
}>;
export function InviteUserDialog(props: InviteUserDialogProps) {
const { children, viewerMembershipFKey } = props;
const { children, connectionId, viewerMembershipFKey } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
@@ -87,11 +88,6 @@ export function InviteUserDialog(props: InviteUserDialogProps) {
});
const onSubmit = handleSubmit((data) => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
"InvitationListFragment_invitations",
{},
);
inviteUser({
variables: {
input: {