Refactor invitation system

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-10 01:11:09 +02:00
parent 0f96b8518f
commit 165eb267f2
39 changed files with 2730 additions and 1194 deletions

View File

@@ -32,6 +32,8 @@ import {
IconPlusLarge,
IconChevronDown,
Avatar,
IconPeopleAdd,
Badge,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
@@ -85,6 +87,9 @@ const OrganizationSelectorFragment = graphql`
endCursor
}
}
invitations(first: 1, filter: {onlyPending: true}) {
totalCount
}
}
`;
@@ -287,6 +292,7 @@ function OrganizationSelector({
);
const organizations = data.organizations.edges.map((edge) => edge.node);
const pendingInvitationsCount = data.invitations.totalCount;
const handleLoadMore = (e?: React.MouseEvent) => {
e?.preventDefault();
@@ -301,54 +307,79 @@ function OrganizationSelector({
};
return (
<Dropdown
toggle={
<Button
className="-ml-3"
variant="tertiary"
iconAfter={IconChevronGrabberVertical}
>
{currentOrganization?.name || ""}
</Button>
}
>
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
{organizations.map((organization) => (
<DropdownItem
asChild
key={organization.id}
<div className="flex items-center gap-1">
<Dropdown
toggle={
<Button
className="-ml-3"
variant="tertiary"
iconAfter={IconChevronGrabberVertical}
>
<Link to={`/organizations/${organization.id}`}>
<Avatar src={organization.logoUrl} name={organization.name} />
{organization.name}
{currentOrganization?.name || ""}
</Button>
}
>
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
{organizations.map((organization) => (
<DropdownItem
asChild
key={organization.id}
>
<Link to={`/organizations/${organization.id}`}>
<Avatar src={organization.logoUrl} name={organization.name} />
{organization.name}
</Link>
</DropdownItem>
))}
{hasNext && (
<div className="px-3 py-1 flex justify-center">
<Button
variant="tertiary"
onClick={handleLoadMore}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}}
className="mx-auto"
icon={IconChevronDown}
disabled={isLoadingMore}
>
{isLoadingMore ? __("Loading...") : __("Show More")}
</Button>
</div>
)}
</div>
<DropdownSeparator />
{pendingInvitationsCount > 0 && (
<DropdownItem asChild>
<Link to="/">
<IconPeopleAdd size={16} />
<span className="flex-1">{__("Invitations")}</span>
<Badge variant="info" size="sm">
{pendingInvitationsCount}
</Badge>
</Link>
</DropdownItem>
))}
{hasNext && (
<div className="px-3 py-1 flex justify-center">
<Button
variant="tertiary"
onClick={handleLoadMore}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}}
className="mx-auto"
icon={IconChevronDown}
disabled={isLoadingMore}
>
{isLoadingMore ? __("Loading...") : __("Show More")}
</Button>
</div>
)}
</div>
<DropdownSeparator />
<DropdownItem asChild icon={IconPlusLarge}>
<Link to="/organizations/new">
<IconPlusLarge size={16} />
{__("Add organization")}
<DropdownItem asChild>
<Link to="/organizations/new">
<IconPlusLarge size={16} />
{__("Add organization")}
</Link>
</DropdownItem>
</Dropdown>
{pendingInvitationsCount > 0 && (
<Link to="/" className="relative" title={__("Invitations")}>
<Button variant="tertiary" icon={IconPeopleAdd} />
<Badge
variant="info"
size="sm"
className="absolute -top-1 -right-1 min-w-[20px] h-5 flex items-center justify-center"
>
{pendingInvitationsCount}
</Badge>
</Link>
</DropdownItem>
</Dropdown>
)}
</div>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<92c96c7b8a58554e9fb99b86489e13d4>>
* @generated SignedSource<<7288534d3b5e852c463af820a9001298>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -208,6 +208,37 @@ return {
"kind": "LinkedHandle",
"name": "organizations"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"onlyPending": true
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"onlyPending\":true},first:1)"
},
(v4/*: any*/)
],
"storageKey": null
@@ -215,16 +246,16 @@ return {
]
},
"params": {
"cacheID": "57fa65c9d5f23210b606fbf0b2005fb8",
"cacheID": "77f3fbbca18dda24151bff9901c54626",
"id": null,
"metadata": {},
"name": "MainLayoutOrganizationSelectorPaginationQuery",
"operationKind": "query",
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {onlyPending: true}) {\n totalCount\n }\n}\n"
}
};
})();
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
(node as any).hash = "99a5cdcce2542bc7fba01d32bbd4648b";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0719379cc0124f36eb1810ad24b2ae5a>>
* @generated SignedSource<<cb335e1fd93543371a82adf23002f600>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -275,6 +275,37 @@ return {
"key": "MainLayout_OrganizationSelector_organizations",
"kind": "LinkedHandle",
"name": "organizations"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"status": "PENDING"
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
}
],
"storageKey": null
@@ -304,12 +335,12 @@ return {
]
},
"params": {
"cacheID": "6287440397ef427d84f8734b0953784c",
"cacheID": "117a78fef8e7a8fe200baf1ebf2d2487",
"id": null,
"metadata": {},
"name": "MainLayoutQuery",
"operationKind": "query",
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {status: PENDING}) {\n totalCount\n }\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<2eda1054dbabd2bb5e45d8db5c7a01fc>>
* @generated SignedSource<<bd521d62fb0f29ace7af511b527ef750>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,6 +11,9 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MainLayout_OrganizationSelector_viewer$data = {
readonly invitations: {
readonly totalCount: number;
};
readonly organizations: {
readonly edges: ReadonlyArray<{
readonly node: {
@@ -178,6 +181,37 @@ return {
}
],
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"status": "PENDING"
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
}
],
"type": "Viewer",
@@ -185,6 +219,6 @@ return {
};
})();
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
(node as any).hash = "d1491a8228d455e2491724c03c7c30c1";
export default node;