Plug back new org CTA in org dropdown

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-20 00:43:54 +01:00
committed by Bryan Frimin
parent 38bf027dd6
commit 8b8e671953
6 changed files with 193 additions and 46 deletions

View File

@@ -4,8 +4,8 @@ import {
type PreloadedQuery, type PreloadedQuery,
} from "react-relay"; } from "react-relay";
import { organizationLayoutQuery } from "./OrganizationLayoutQuery"; import { organizationLayoutQuery } from "./OrganizationLayoutQuery";
import { Outlet } from "react-router"; import { Link, Outlet } from "react-router";
import { Layout, Skeleton } from "@probo/ui"; import { Badge, Button, IconPeopleAdd, Layout, Skeleton } from "@probo/ui";
import { Sidebar } from "./_components/Sidebar"; import { Sidebar } from "./_components/Sidebar";
import { OrganizationDropdown } from "./_components/OrganizationDropdown"; import { OrganizationDropdown } from "./_components/OrganizationDropdown";
import { consoleEnvironment } from "/environments"; import { consoleEnvironment } from "/environments";
@@ -13,6 +13,7 @@ import type { OrganizationLayoutQuery } from "./__generated__/OrganizationLayout
import { PermissionsProvider } from "/providers/PermissionsProvider"; import { PermissionsProvider } from "/providers/PermissionsProvider";
import { SessionDropdown } from "./_components/SessionDropdown"; import { SessionDropdown } from "./_components/SessionDropdown";
import { Suspense } from "react"; import { Suspense } from "react";
import { useTranslate } from "@probo/i18n";
interface OrganizationLayoutProps { interface OrganizationLayoutProps {
queryRef: PreloadedQuery<OrganizationLayoutQuery>; queryRef: PreloadedQuery<OrganizationLayoutQuery>;
@@ -21,7 +22,9 @@ interface OrganizationLayoutProps {
export default function OrganizationLayout(props: OrganizationLayoutProps) { export default function OrganizationLayout(props: OrganizationLayoutProps) {
const { queryRef } = props; const { queryRef } = props;
const data = usePreloadedQuery<OrganizationLayoutQuery>( const { __ } = useTranslate();
const { organization, viewer } = usePreloadedQuery<OrganizationLayoutQuery>(
organizationLayoutQuery, organizationLayoutQuery,
queryRef, queryRef,
); );
@@ -32,10 +35,25 @@ export default function OrganizationLayout(props: OrganizationLayoutProps) {
header={ header={
<> <>
<div className="mr-auto"> <div className="mr-auto">
<OrganizationDropdown fKey={data.organization} /> <OrganizationDropdown
organizationFKey={organization}
viewerFKey={viewer}
/>
{viewer.pendingInvitations.totalCount > 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"
>
{viewer.pendingInvitations.totalCount}
</Badge>
</Link>
)}
</div> </div>
<Suspense fallback={<Skeleton className="w-32 h-8" />}> <Suspense fallback={<Skeleton className="w-32 h-8" />}>
<SessionDropdown fKey={data.viewer} /> <SessionDropdown fKey={viewer} />
</Suspense> </Suspense>
</> </>
} }

View File

@@ -12,12 +12,13 @@ export const organizationLayoutQuery = graphql`
query OrganizationLayoutQuery($organizationId: ID!) { query OrganizationLayoutQuery($organizationId: ID!) {
organization: node(id: $organizationId) @required(action: THROW) { organization: node(id: $organizationId) @required(action: THROW) {
... on Organization { ... on Organization {
...OrganizationDropdownFragment ...OrganizationDropdown_organizationFragment
} }
} }
viewer @required(action: THROW) { viewer @required(action: THROW) {
pendingInvitations { ...OrganizationDropdown_viewerFragment
totalCount pendingInvitations @required(action: THROW) {
totalCount @required(action: THROW)
} }
...SessionDropdownFragment @arguments(organizationId: $organizationId) ...SessionDropdownFragment @arguments(organizationId: $organizationId)
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<1d82d885a77675a330799121edb5cb69>> * @generated SignedSource<<09fbc762db2d00eb944a0ef9afe3756d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -15,13 +15,13 @@ export type OrganizationLayoutQuery$variables = {
}; };
export type OrganizationLayoutQuery$data = { export type OrganizationLayoutQuery$data = {
readonly organization: { readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownFragment">; readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdown_organizationFragment">;
}; };
readonly viewer: { readonly viewer: {
readonly pendingInvitations: { readonly pendingInvitations: {
readonly totalCount: number | null | undefined; readonly totalCount: number;
} | null | undefined; };
readonly " $fragmentSpreads": FragmentRefs<"SessionDropdownFragment">; readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdown_viewerFragment" | "SessionDropdownFragment">;
}; };
}; };
export type OrganizationLayoutQuery = { export type OrganizationLayoutQuery = {
@@ -47,19 +47,8 @@ v1 = [
v2 = { v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "InvitationConnection", "kind": "ScalarField",
"kind": "LinkedField", "name": "totalCount",
"name": "pendingInvitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": null "storageKey": null
}, },
v3 = [ v3 = [
@@ -99,7 +88,7 @@ return {
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "OrganizationDropdownFragment" "name": "OrganizationDropdown_organizationFragment"
} }
], ],
"type": "Organization", "type": "Organization",
@@ -120,7 +109,31 @@ return {
"name": "viewer", "name": "viewer",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), {
"args": null,
"kind": "FragmentSpread",
"name": "OrganizationDropdown_viewerFragment"
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "pendingInvitations",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": (v2/*: any*/),
"action": "THROW"
}
],
"storageKey": null
},
"action": "THROW"
},
{ {
"args": (v3/*: any*/), "args": (v3/*: any*/),
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -182,7 +195,18 @@ return {
"name": "viewer", "name": "viewer",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), {
"alias": null,
"args": null,
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "pendingInvitations",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -223,16 +247,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "0ebb386f89d19888f18a05c8dc573895", "cacheID": "ca7f41668eb74024237351d0e076819e",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "OrganizationLayoutQuery", "name": "OrganizationLayoutQuery",
"operationKind": "query", "operationKind": "query",
"text": "query OrganizationLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...OrganizationDropdownFragment\n }\n id\n }\n viewer {\n pendingInvitations {\n totalCount\n }\n ...SessionDropdownFragment_4xMPKw\n id\n }\n}\n\nfragment OrganizationDropdownFragment on Organization {\n name\n}\n\nfragment SessionDropdownFragment_4xMPKw on Identity {\n email\n profileFor(organizationId: $organizationId) {\n firstName\n lastName\n id\n }\n}\n" "text": "query OrganizationLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...OrganizationDropdown_organizationFragment\n }\n id\n }\n viewer {\n ...OrganizationDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n ...SessionDropdownFragment_4xMPKw\n id\n }\n}\n\nfragment OrganizationDropdown_organizationFragment on Organization {\n name\n}\n\nfragment OrganizationDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SessionDropdownFragment_4xMPKw on Identity {\n email\n profileFor(organizationId: $organizationId) {\n firstName\n lastName\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "d3d91888eabb36cc450ca585757b21c7"; (node as any).hash = "81b8eb0af21d649268e30c80d16226de";
export default node; export default node;

View File

@@ -1,12 +1,15 @@
import { useFragment, useQueryLoader } from "react-relay"; import { useFragment, useQueryLoader } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { OrganizationDropdownFragment$key } from "./__generated__/OrganizationDropdownFragment.graphql";
import { import {
Badge,
Button, Button,
Dropdown, Dropdown,
DropdownItem,
DropdownSeparator, DropdownSeparator,
IconChevronGrabberVertical, IconChevronGrabberVertical,
IconMagnifyingGlass, IconMagnifyingGlass,
IconPeopleAdd,
IconPlusLarge,
Input, Input,
} from "@probo/ui"; } from "@probo/ui";
import { Suspense, useCallback, useState } from "react"; import { Suspense, useCallback, useState } from "react";
@@ -16,22 +19,43 @@ import {
organizationDropdownMenuQuery, organizationDropdownMenuQuery,
} from "./OrganizationDropdownMenu"; } from "./OrganizationDropdownMenu";
import type { OrganizationDropdownMenuQuery } from "./__generated__/OrganizationDropdownMenuQuery.graphql"; import type { OrganizationDropdownMenuQuery } from "./__generated__/OrganizationDropdownMenuQuery.graphql";
import { Link } from "react-router";
import type { OrganizationDropdown_organizationFragment$key } from "./__generated__/OrganizationDropdown_organizationFragment.graphql";
import type { OrganizationDropdown_viewerFragment$key } from "./__generated__/OrganizationDropdown_viewerFragment.graphql";
const fragment = graphql` const organizationFragment = graphql`
fragment OrganizationDropdownFragment on Organization { fragment OrganizationDropdown_organizationFragment on Organization {
name name
} }
`; `;
const viewerFragment = graphql`
fragment OrganizationDropdown_viewerFragment on Identity {
pendingInvitations @required(action: THROW) {
totalCount @required(action: THROW)
}
}
`;
export function OrganizationDropdown(props: { export function OrganizationDropdown(props: {
fKey: OrganizationDropdownFragment$key; organizationFKey: OrganizationDropdown_organizationFragment$key;
viewerFKey: OrganizationDropdown_viewerFragment$key;
}) { }) {
const { fKey } = props; const { organizationFKey, viewerFKey } = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const currentOrganization = useFragment(fragment, fKey); const currentOrganization =
useFragment<OrganizationDropdown_organizationFragment$key>(
organizationFragment,
organizationFKey,
);
const {
pendingInvitations: { totalCount: pendingInvitationCount },
} = useFragment<OrganizationDropdown_viewerFragment$key>(
viewerFragment,
viewerFKey,
);
const [queryRef, loadQuery] = useQueryLoader<OrganizationDropdownMenuQuery>( const [queryRef, loadQuery] = useQueryLoader<OrganizationDropdownMenuQuery>(
organizationDropdownMenuQuery, organizationDropdownMenuQuery,
); );
@@ -83,6 +107,23 @@ export function OrganizationDropdown(props: {
)} )}
</div> </div>
<DropdownSeparator /> <DropdownSeparator />
{pendingInvitationCount > 0 && (
<DropdownItem asChild>
<Link to="/">
<IconPeopleAdd size={16} />
<span className="flex-1">{__("Invitations")}</span>
<Badge variant="info" size="sm">
{pendingInvitationCount}
</Badge>
</Link>
</DropdownItem>
)}
<DropdownItem asChild>
<Link to="/organizations/new">
<IconPlusLarge size={16} />
{__("Add organization")}
</Link>
</DropdownItem>
</Dropdown> </Dropdown>
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<06f02e43b64c316083067f5b3978a08a>> * @generated SignedSource<<1d2465517ea2dcfa99fb3dd8757fe980>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,20 +10,20 @@
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type OrganizationDropdownFragment$data = { export type OrganizationDropdown_organizationFragment$data = {
readonly name: string; readonly name: string;
readonly " $fragmentType": "OrganizationDropdownFragment"; readonly " $fragmentType": "OrganizationDropdown_organizationFragment";
}; };
export type OrganizationDropdownFragment$key = { export type OrganizationDropdown_organizationFragment$key = {
readonly " $data"?: OrganizationDropdownFragment$data; readonly " $data"?: OrganizationDropdown_organizationFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownFragment">; readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdown_organizationFragment">;
}; };
const node: ReaderFragment = { const node: ReaderFragment = {
"argumentDefinitions": [], "argumentDefinitions": [],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "OrganizationDropdownFragment", "name": "OrganizationDropdown_organizationFragment",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
@@ -37,6 +37,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "9f19581f86736e6345912284668a8f25"; (node as any).hash = "8e6d0c702f821670315b5bde55e2556a";
export default node; export default node;

View File

@@ -0,0 +1,63 @@
/**
* @generated SignedSource<<d4b9efa7b9092ceb3f995507108a68e3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type OrganizationDropdown_viewerFragment$data = {
readonly pendingInvitations: {
readonly totalCount: number;
};
readonly " $fragmentType": "OrganizationDropdown_viewerFragment";
};
export type OrganizationDropdown_viewerFragment$key = {
readonly " $data"?: OrganizationDropdown_viewerFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdown_viewerFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationDropdown_viewerFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "pendingInvitations",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Identity",
"abstractKey": null
};
(node as any).hash = "ce947f812c95b74e9f250b8d5821ebb0";
export default node;