diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 3dc482458..fc6af76ce 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -30,36 +30,60 @@ import { DropdownItem, IconChevronGrabberVertical, IconPlusLarge, + IconChevronDown, Avatar, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; +import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; -import { Suspense } from "react"; +import type { MainLayout_OrganizationSelector_viewer$key } from "./__generated__/MainLayout_OrganizationSelector_viewer.graphql"; +import { Suspense, useState } from "react"; import { useToast } from "@probo/ui"; import { ErrorBoundary } from "react-error-boundary"; import { PageError } from "/components/PageError"; import { buildEndpoint } from "/providers/RelayProviders"; const MainLayoutQuery = graphql` - query MainLayoutQuery { + query MainLayoutQuery($organizationId: ID!) { viewer { id user { fullName email } - organizations(first: 25) @connection(key: "MainLayout_organizations") { - __id - edges { - node { - id - name - logoUrl - } + ...MainLayout_OrganizationSelector_viewer + } + organization: node(id: $organizationId) { + ... on Organization { + id + name + logoUrl + } + } + } +`; + +const OrganizationSelectorFragment = graphql` + fragment MainLayout_OrganizationSelector_viewer on Viewer + @refetchable(queryName: "MainLayoutOrganizationSelectorPaginationQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 25 } + after: { type: "CursorKey" } + ) { + organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) + @connection(key: "MainLayout_OrganizationSelector_organizations") { + edges { + node { + id + name + logoUrl } } + pageInfo { + hasNextPage + endCursor + } } } `; @@ -83,11 +107,11 @@ export function MainLayout() { <>
}> - +
}> - + } @@ -188,10 +212,10 @@ export function MainLayout() { ); } -function UserDropdown() { +function UserDropdown({ organizationId }: { organizationId: string }) { const { __ } = useTranslate(); const { toast } = useToast(); - const user = useLazyLoadQuery(MainLayoutQuery, {}).viewer + const user = useLazyLoadQuery(MainLayoutQuery, { organizationId }).viewer .user; const handleLogout: React.MouseEventHandler = async ( @@ -242,16 +266,40 @@ function UserDropdown() { ); } -function OrganizationSelector({ organizationId }: { organizationId: string }) { - const organizations = useLazyLoadQuery( - MainLayoutQuery, - {} - ).viewer.organizations.edges.map((edge) => edge.node); - const currentOrganization = organizations.find( - (organization) => organization.id === organizationId - ); +function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) { + const data = useLazyLoadQuery(MainLayoutQuery, { organizationId }); + return ; +} + +function OrganizationSelector({ + viewer, + currentOrganization +}: { + viewer: MainLayout_OrganizationSelector_viewer$key; + currentOrganization: MainLayoutQueryType["response"]["organization"]; +}) { + const [isLoadingMore, setIsLoadingMore] = useState(false); const { __ } = useTranslate(); + const { data, loadNext, hasNext } = usePaginationFragment( + OrganizationSelectorFragment, + viewer + ); + + const organizations = data.organizations.edges.map((edge) => edge.node); + + const handleLoadMore = (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + if (hasNext && !isLoadingMore) { + setIsLoadingMore(true); + loadNext(25, { + onComplete: () => setIsLoadingMore(false), + }); + } + }; + return ( - {currentOrganization?.name} + {currentOrganization?.name || ""} } > - {organizations.map((organization) => ( - - - - {organization.name} - - - ))} +
+ {organizations.map((organization) => ( + + + + {organization.name} + + + ))} + {hasNext && ( +
+ +
+ )} +
diff --git a/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts new file mode 100644 index 000000000..bab583991 --- /dev/null +++ b/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts @@ -0,0 +1,230 @@ +/** + * @generated SignedSource<<92c96c7b8a58554e9fb99b86489e13d4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MainLayoutOrganizationSelectorPaginationQuery$variables = { + after?: any | null | undefined; + first?: number | null | undefined; +}; +export type MainLayoutOrganizationSelectorPaginationQuery$data = { + readonly viewer: { + readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; + }; +}; +export type MainLayoutOrganizationSelectorPaginationQuery = { + response: MainLayoutOrganizationSelectorPaginationQuery$data; + variables: MainLayoutOrganizationSelectorPaginationQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 25, + "kind": "LocalArgument", + "name": "first" + } +], +v1 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v2 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v3 = [ + (v1/*: any*/), + (v2/*: any*/), + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "NAME" + } + } +], +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MainLayoutOrganizationSelectorPaginationQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "args": [ + (v1/*: any*/), + (v2/*: any*/) + ], + "kind": "FragmentSpread", + "name": "MainLayout_OrganizationSelector_viewer" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MainLayoutOrganizationSelectorPaginationQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v3/*: any*/), + "concreteType": "OrganizationConnection", + "kind": "LinkedField", + "name": "organizations", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "OrganizationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v3/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "MainLayout_OrganizationSelector_organizations", + "kind": "LinkedHandle", + "name": "organizations" + }, + (v4/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "57fa65c9d5f23210b606fbf0b2005fb8", + "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" + } +}; +})(); + +(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af"; + +export default node; diff --git a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts index 977854355..518e4e085 100644 --- a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts +++ b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<0719379cc0124f36eb1810ad24b2ae5a>> * @lightSyntaxTransform * @nogrep */ @@ -9,24 +9,23 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type MainLayoutQuery$variables = Record; +import { FragmentRefs } from "relay-runtime"; +export type MainLayoutQuery$variables = { + organizationId: string; +}; export type MainLayoutQuery$data = { + readonly organization: { + readonly id?: string; + readonly logoUrl?: string | null | undefined; + readonly name?: string; + }; readonly viewer: { readonly id: string; - readonly organizations: { - readonly __id: string; - readonly edges: ReadonlyArray<{ - readonly node: { - readonly id: string; - readonly logoUrl: string | null | undefined; - readonly name: string; - }; - }>; - }; readonly user: { readonly email: string; readonly fullName: string; }; + readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; }; }; export type MainLayoutQuery = { @@ -35,127 +34,80 @@ export type MainLayoutQuery = { }; const node: ConcreteRequest = (function(){ -var v0 = { +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v1 = { +v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v2 = { +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "email", "storageKey": null }, -v3 = [ +v4 = [ { - "alias": null, - "args": null, - "concreteType": "OrganizationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null - }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] + "kind": "Variable", + "name": "id", + "variableName": "organizationId" } ], -v4 = [ +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null +}, +v7 = [ { "kind": "Literal", "name": "first", "value": 25 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "NAME" + } } -]; +], +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}; return { "fragment": { - "argumentDefinitions": [], + "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "MainLayoutQuery", @@ -168,7 +120,7 @@ return { "name": "viewer", "plural": false, "selections": [ - (v0/*: any*/), + (v1/*: any*/), { "alias": null, "args": null, @@ -177,20 +129,36 @@ return { "name": "user", "plural": false, "selections": [ - (v1/*: any*/), - (v2/*: any*/) + (v2/*: any*/), + (v3/*: any*/) ], "storageKey": null }, { - "alias": "organizations", "args": null, - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "__MainLayout_organizations_connection", - "plural": false, - "selections": (v3/*: any*/), - "storageKey": null + "kind": "FragmentSpread", + "name": "MainLayout_OrganizationSelector_viewer" + } + ], + "storageKey": null + }, + { + "alias": "organization", + "args": (v4/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v1/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "type": "Organization", + "abstractKey": null } ], "storageKey": null @@ -201,7 +169,7 @@ return { }, "kind": "Request", "operation": { - "argumentDefinitions": [], + "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "MainLayoutQuery", "selections": [ @@ -213,7 +181,7 @@ return { "name": "viewer", "plural": false, "selections": [ - (v0/*: any*/), + (v1/*: any*/), { "alias": null, "args": null, @@ -222,59 +190,130 @@ return { "name": "user", "plural": false, "selections": [ - (v1/*: any*/), (v2/*: any*/), - (v0/*: any*/) + (v3/*: any*/), + (v1/*: any*/) ], "storageKey": null }, { "alias": null, - "args": (v4/*: any*/), + "args": (v7/*: any*/), "concreteType": "OrganizationConnection", "kind": "LinkedField", "name": "organizations", "plural": false, - "selections": (v3/*: any*/), - "storageKey": "organizations(first:25)" + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "OrganizationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v8/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "organizations(first:25,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" }, { "alias": null, - "args": (v4/*: any*/), - "filters": null, + "args": (v7/*: any*/), + "filters": [ + "orderBy" + ], "handle": "connection", - "key": "MainLayout_organizations", + "key": "MainLayout_OrganizationSelector_organizations", "kind": "LinkedHandle", "name": "organizations" } ], "storageKey": null + }, + { + "alias": "organization", + "args": (v4/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v8/*: any*/), + (v1/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v5/*: any*/), + (v6/*: any*/) + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null } ] }, "params": { - "cacheID": "9ab80df970b8fa7c4eabff75a6730ec5", + "cacheID": "6287440397ef427d84f8734b0953784c", "id": null, - "metadata": { - "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "viewer", - "organizations" - ] - } - ] - }, + "metadata": {}, "name": "MainLayoutQuery", "operationKind": "query", - "text": "query MainLayoutQuery {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\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}\n" } }; })(); -(node as any).hash = "fc37d1b70f1a239abb4c959d47ee28fd"; +(node as any).hash = "aaaca58a896839aa85ce7c2fcf75de39"; export default node; diff --git a/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts b/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts new file mode 100644 index 000000000..7d9ae1760 --- /dev/null +++ b/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts @@ -0,0 +1,190 @@ +/** + * @generated SignedSource<<2eda1054dbabd2bb5e45d8db5c7a01fc>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MainLayout_OrganizationSelector_viewer$data = { + readonly organizations: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly logoUrl: string | null | undefined; + readonly name: string; + }; + }>; + readonly pageInfo: { + readonly endCursor: any | null | undefined; + readonly hasNextPage: boolean; + }; + }; + readonly " $fragmentType": "MainLayout_OrganizationSelector_viewer"; +}; +export type MainLayout_OrganizationSelector_viewer$key = { + readonly " $data"?: MainLayout_OrganizationSelector_viewer$data; + readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; +}; + +import MainLayoutOrganizationSelectorPaginationQuery_graphql from './MainLayoutOrganizationSelectorPaginationQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "organizations" +]; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 25, + "kind": "LocalArgument", + "name": "first" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": "first", + "cursor": "after", + "direction": "forward", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": null, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "viewer" + ], + "operation": MainLayoutOrganizationSelectorPaginationQuery_graphql + } + }, + "name": "MainLayout_OrganizationSelector_viewer", + "selections": [ + { + "alias": "organizations", + "args": [ + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "NAME" + } + } + ], + "concreteType": "OrganizationConnection", + "kind": "LinkedField", + "name": "__MainLayout_OrganizationSelector_organizations_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "OrganizationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" + } + ], + "type": "Viewer", + "abstractKey": null +}; +})(); + +(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af"; + +export default node; diff --git a/apps/console/src/pages/OrganizationsPage.tsx b/apps/console/src/pages/OrganizationsPage.tsx index e4da1f8de..f19ffe68f 100644 --- a/apps/console/src/pages/OrganizationsPage.tsx +++ b/apps/console/src/pages/OrganizationsPage.tsx @@ -9,18 +9,13 @@ import { Button, Card, IconPlusLarge, - ActionDropdown, - DropdownItem, - IconTrashCan, } from "@probo/ui"; import { usePageTitle } from "@probo/hooks"; -import { useDeleteOrganizationMutation } from "../hooks/graph/OrganizationGraph"; -import { DeleteOrganizationDialog } from "../components/organizations/DeleteOrganizationDialog"; const OrganizationsPageQuery = graphql` query OrganizationsPageQuery { viewer { - organizations(first: 25) @connection(key: "OrganizationsPage_organizations") { + organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) @connection(key: "OrganizationsPage_organizations") { __id edges { node { @@ -67,7 +62,6 @@ export default function OrganizationsPage() { ))} @@ -98,23 +92,10 @@ type OrganizationCardProps = { name: string; logoUrl: string | null | undefined; }; - connectionId: string; }; -function OrganizationCard({ organization, connectionId }: OrganizationCardProps) { +function OrganizationCard({ organization }: OrganizationCardProps) { const { __ } = useTranslate(); - const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation(); - - const handleDelete = () => { - return deleteOrganization({ - variables: { - input: { - organizationId: organization.id, - }, - connections: [connectionId], - }, - }); - }; return ( @@ -136,21 +117,6 @@ function OrganizationCard({ organization, connectionId }: OrganizationCardProps) {__("Select")} - - - - {__("Delete organization")} - - - diff --git a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts index fec9aa8f6..69aaee473 100644 --- a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<70d28036f17c3b03dcfecce6abd5b476>> + * @generated SignedSource<<84d0e43eced78862d1c82603e9c332e8>> * @lightSyntaxTransform * @nogrep */ @@ -31,13 +31,21 @@ export type OrganizationsPageQuery = { const node: ConcreteRequest = (function(){ var v0 = { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "NAME" + } +}, +v1 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v1 = [ +v2 = [ { "alias": null, "args": null, @@ -54,7 +62,7 @@ v1 = [ "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v1/*: any*/), { "alias": null, "args": null, @@ -127,12 +135,13 @@ v1 = [ ] } ], -v2 = [ +v3 = [ { "kind": "Literal", "name": "first", - "value": 25 - } + "value": 1000 + }, + (v0/*: any*/) ]; return { "fragment": { @@ -151,13 +160,15 @@ return { "selections": [ { "alias": "organizations", - "args": null, + "args": [ + (v0/*: any*/) + ], "concreteType": "OrganizationConnection", "kind": "LinkedField", "name": "__OrganizationsPage_organizations_connection", "plural": false, - "selections": (v1/*: any*/), - "storageKey": null + "selections": (v2/*: any*/), + "storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" } ], "storageKey": null @@ -182,31 +193,33 @@ return { "selections": [ { "alias": null, - "args": (v2/*: any*/), + "args": (v3/*: any*/), "concreteType": "OrganizationConnection", "kind": "LinkedField", "name": "organizations", "plural": false, - "selections": (v1/*: any*/), - "storageKey": "organizations(first:25)" + "selections": (v2/*: any*/), + "storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" }, { "alias": null, - "args": (v2/*: any*/), - "filters": null, + "args": (v3/*: any*/), + "filters": [ + "orderBy" + ], "handle": "connection", "key": "OrganizationsPage_organizations", "kind": "LinkedHandle", "name": "organizations" }, - (v0/*: any*/) + (v1/*: any*/) ], "storageKey": null } ] }, "params": { - "cacheID": "ddaafef96749a932a0cee5286a3c2bbd", + "cacheID": "1735764e6816660969c5f96922320ac5", "id": null, "metadata": { "connection": [ @@ -223,11 +236,11 @@ return { }, "name": "OrganizationsPageQuery", "operationKind": "query", - "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n" + "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "122ab4331082ffe892fa7fe48692d961"; +(node as any).hash = "6fde39384e4678f88f17c34bbd30e684"; export default node; diff --git a/apps/console/src/pages/organizations/NewOrganizationPage.tsx b/apps/console/src/pages/organizations/NewOrganizationPage.tsx index 08e377055..80adeb9ac 100644 --- a/apps/console/src/pages/organizations/NewOrganizationPage.tsx +++ b/apps/console/src/pages/organizations/NewOrganizationPage.tsx @@ -57,7 +57,11 @@ export default function NewOrganizationPage() { connections: [ ConnectionHandler.getConnectionID( data.viewer.id, - "NewOrganizationPageQuery" + "OrganizationsPage_organizations" + ), + ConnectionHandler.getConnectionID( + data.viewer.id, + "MainLayout_OrganizationSelector_organizations" ), ], }, diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index 825e913a7..eb7ac7dce 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -41,8 +41,12 @@ type ( func (o Organization) CursorKey(orderBy OrganizationOrderField) page.CursorKey { switch orderBy { + case OrganizationOrderFieldName: + return page.NewCursorKey(o.ID, o.Name) case OrganizationOrderFieldCreatedAt: return page.NewCursorKey(o.ID, o.CreatedAt) + case OrganizationOrderFieldUpdatedAt: + return page.NewCursorKey(o.ID, o.UpdatedAt) } panic(fmt.Sprintf("unsupported order by: %s", orderBy)) @@ -90,6 +94,57 @@ LIMIT 1; return nil } +// Tenant id scope is not applied in this functions because we want to access all user's organizations. +func (o *Organizations) ListForUserID( + ctx context.Context, + conn pg.Conn, + userID gid.GID, + cursor *page.Cursor[OrganizationOrderField], +) error { + q := ` +WITH user_org AS ( + SELECT + organization_id + FROM + users_organizations + WHERE + user_id = @user_id +) +SELECT + tenant_id, + id, + name, + logo_object_key, + created_at, + updated_at +FROM + organizations +INNER JOIN + user_org ON organizations.id = user_org.organization_id +WHERE + %s +` + + q = fmt.Sprintf(q, cursor.SQLFragment()) + + args := pgx.StrictNamedArgs{"user_id": userID} + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query organizations: %w", err) + } + + organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) + if err != nil { + return fmt.Errorf("cannot collect organizations: %w", err) + } + + *o = organizations + + return nil +} + func (o *Organization) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index d8be74f22..acc26424a 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -4870,25 +4870,26 @@ func (r *vendorServiceResolver) Vendor(ctx context.Context, obj *types.VendorSer func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.OrganizationOrder, filter *types.OrganizationFilter) (*types.OrganizationConnection, error) { user := UserFromContext(ctx) - // For now, we're not using cursor pagination since we're loading all organizations - organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID) + pageOrderBy := page.OrderBy[coredata.OrganizationOrderField]{ + Field: coredata.OrganizationOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.OrganizationOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + organizations, err := r.usrmgrSvc.ListOrganizationsForUserIDPaginated(ctx, user.ID, cursor) if err != nil { panic(fmt.Errorf("failed to list organizations for user: %w", err)) } - var edges []*types.OrganizationEdge - for _, organization := range organizations { - edges = append(edges, types.NewOrganizationEdge(organization, coredata.OrganizationOrderFieldCreatedAt)) - } + page := page.NewPage(organizations, cursor) - // The simple implementation doesn't handle pagination yet - return &types.OrganizationConnection{ - Edges: edges, - PageInfo: &types.PageInfo{ - HasNextPage: false, - HasPreviousPage: false, - }, - }, nil + return types.NewOrganizationConnection(page), nil } // Asset returns schema.AssetResolver implementation. diff --git a/pkg/usrmgr/usrmgr.go b/pkg/usrmgr/usrmgr.go index bc43f6355..9c890241c 100644 --- a/pkg/usrmgr/usrmgr.go +++ b/pkg/usrmgr/usrmgr.go @@ -515,6 +515,33 @@ func (s Service) ListOrganizationsForUserID( return organizations, nil } +// Tenant id scope is not applied in this functions because we want to access all user's organizations. +func (s Service) ListOrganizationsForUserIDPaginated( + ctx context.Context, + userID gid.GID, + cursor *page.Cursor[coredata.OrganizationOrderField], +) (coredata.Organizations, error) { + organizations := coredata.Organizations{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := organizations.ListForUserID(ctx, conn, userID, cursor) + if err != nil { + return fmt.Errorf("cannot list user organizations: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return organizations, nil +} + func (s Service) ListTenantsForUserID( ctx context.Context, userID gid.GID,