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,