Fix console app graphQL queries

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-13 14:11:11 +04:00
parent 4c67217e35
commit ec3d8fad40
22 changed files with 229 additions and 209 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<beba83081139a0721dd1ca6e169c56f2>>
* @generated SignedSource<<dcdbded62e85b824993721946b5e5e2e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,7 +19,7 @@ export type MembershipsDropdownMenuQuery$data = {
readonly id: string;
readonly membership: {
readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdownMenuItemFragment">;
} | null | undefined;
};
readonly organization: {
readonly name: string;
readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdownMenuItem_organizationFragment">;
@@ -134,20 +134,24 @@ return {
"action": "THROW"
},
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "MembershipsDropdownMenuItemFragment"
}
],
"storageKey": null
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "MembershipsDropdownMenuItemFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
@@ -289,6 +293,6 @@ return {
};
})();
(node as any).hash = "14dbd79243ffb6104ff7315b78aa7990";
(node as any).hash = "11d7ec7c24e0e539fd42e428d71998e1";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5916f4df5b8113a300774131759cb218>>
* @generated SignedSource<<b6eef6dfea77fdf5e4e151eef11f4484>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -285,6 +285,19 @@ return {
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
}
],
"storageKey": null
@@ -328,19 +341,6 @@ return {
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
},
(v11/*: any*/)
],
"storageKey": null
@@ -430,12 +430,12 @@ return {
]
},
"params": {
"cacheID": "3ed525921615d7910d784222c189566a",
"cacheID": "25aa17e466fdec627ba3d24d5230301b",
"id": null,
"metadata": {},
"name": "PeopleListFragment_RefetchQuery",
"operationKind": "query",
"text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n"
"text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b56389d57c3d4cb31dd873feaeff2bb3>>
* @generated SignedSource<<6e9b0a4fbf6ce65f11f2367f2ec5b6d4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,7 +15,6 @@ export type MembershipState = "ACTIVE" | "INACTIVE";
export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
import { FragmentRefs } from "relay-runtime";
export type PeopleListItemFragment$data = {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: string;
readonly fullName: string;
@@ -25,6 +24,7 @@ export type PeopleListItemFragment$data = {
};
readonly kind: ProfileKind;
readonly membership: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly id: string;
readonly role: MembershipRole;
@@ -119,6 +119,19 @@ return {
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
}
],
"storageKey": null
@@ -166,19 +179,6 @@ return {
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
}
],
"type": "Profile",
@@ -186,6 +186,6 @@ return {
};
})();
(node as any).hash = "5f45ee7c58d50b0a6569c3bd22ffc83f";
(node as any).hash = "89702fbdf02294212269adb22efb65fe";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<53070e63046901877444d5de41cc8624>>
* @generated SignedSource<<e628337d33e88ca5cfe6e45a55eebe32>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -396,6 +396,19 @@ return {
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
}
],
"storageKey": null
@@ -427,19 +440,6 @@ return {
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership-profile:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"iam:membership-profile:delete\")"
},
(v2/*: any*/)
],
"storageKey": null
@@ -552,12 +552,12 @@ return {
]
},
"params": {
"cacheID": "d13993ce884669894f65925a714aeaa6",
"cacheID": "4303ec6c2fa72181d747bb9053446aaa",
"id": null,
"metadata": {},
"name": "PeoplePageQuery",
"operationKind": "query",
"text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n"
"text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<405fdde1b279cf7336ca395457041b93>>
* @generated SignedSource<<2e75f19fcdaf2b78a187c33458c9cfe0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -23,7 +23,7 @@ export type PersonPageQuery$data = {
};
readonly membership: {
readonly id: string;
} | null | undefined;
};
readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">;
} | {
// This will never be '%other', but we need some
@@ -126,7 +126,11 @@ return {
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v5/*: any*/),
{
"kind": "RequiredField",
"field": (v5/*: any*/),
"action": "THROW"
},
{
"kind": "RequiredField",
"field": {
@@ -265,6 +269,6 @@ return {
};
})();
(node as any).hash = "24863216f5484c1fae7ff6ad002a7f8c";
(node as any).hash = "5dce91fda9224f6f3928d4549485d3bd";
export default node;

View File

@@ -18,19 +18,22 @@ import { MembershipCard } from "./_components/MembershipCard";
export const membershipsPageQuery = graphql`
query MembershipsPageQuery {
viewer @required(action: THROW) {
memberships(
profiles(
first: 1000
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
)
@connection(key: "MembershipsPage_memberships")
@connection(key: "MembershipsPage_profiles")
@required(action: THROW) {
__id
edges @required(action: THROW) {
node {
id
membership @required(action: THROW) {
...MembershipCardFragment
}
organization @required(action: THROW) {
name
...MembershipCard_organizationFragment
}
}
}
@@ -64,7 +67,7 @@ export function MembershipsPage(props: {
const { queryRef } = props;
const {
viewer: {
memberships: { __id: membershipConnectionId, edges: initialMemberships },
profiles: { edges: initialProfiles },
pendingInvitations: {
__id: pendingInvitationsConnectionId,
edges: invitations,
@@ -72,14 +75,14 @@ export function MembershipsPage(props: {
},
} = usePreloadedQuery<MembershipsPageQuery>(membershipsPageQuery, queryRef);
const memberships = useMemo(() => {
const profiles = useMemo(() => {
if (!search.trim()) {
return initialMemberships;
return initialProfiles;
}
return initialMemberships.filter(({ node }) =>
return initialProfiles.filter(({ node }) =>
node.organization.name.toLowerCase().includes(search.toLowerCase()),
);
}, [initialMemberships, search]);
}, [initialProfiles, search]);
return (
<>
@@ -98,14 +101,13 @@ export function MembershipsPage(props: {
pendingInvitationsConnectionId={
pendingInvitationsConnectionId
}
membershipConnectionId={membershipConnectionId}
key={node.id}
fKey={node}
/>
))}
</div>
)}
{initialMemberships.length > 0 && (
{initialProfiles.length > 0 && (
<div className="space-y-3">
<h2 className="text-xl font-semibold">
{__("Your organizations")}
@@ -118,15 +120,19 @@ export function MembershipsPage(props: {
onValueChange={setSearch}
/>
</div>
{memberships.length === 0
{profiles.length === 0
? (
<div className="text-center text-txt-secondary py-4">
{__("No organizations found")}
</div>
)
: (
memberships.map(({ node }) => (
<MembershipCard key={node.id} fKey={node} />
profiles.map(({ node }) => (
<MembershipCard
key={node.id}
fKey={node.membership}
organizationFragmentRef={node.organization}
/>
))
)}
</div>

View File

@@ -22,21 +22,17 @@ const fragment = graphql`
const acceptMutation = graphql`
mutation InvitationCardMutation(
$input: AcceptInvitationInput!
$membershipConnections: [ID!]!
# FIXME: profiles
# $membershipConnections: [ID!]!
$pendingInvitationConnections: [ID!]!
) {
acceptInvitation(input: $input) {
invitation {
id @deleteEdge(connections: $pendingInvitationConnections)
}
membershipEdge @prependEdge(connections: $membershipConnections) {
node {
id
...MembershipCardFragment
organization {
name
}
}
membership {
id
...MembershipCardFragment
}
}
}
@@ -44,12 +40,11 @@ const acceptMutation = graphql`
interface InvitationCardProps {
pendingInvitationsConnectionId: DataID;
membershipConnectionId: DataID;
fKey: InvitationCardFragment$key;
}
export function InvitationCard(props: InvitationCardProps) {
const { pendingInvitationsConnectionId, membershipConnectionId, fKey }
const { pendingInvitationsConnectionId, fKey }
= props;
const { __ } = useTranslate();
@@ -67,7 +62,6 @@ export function InvitationCard(props: InvitationCardProps) {
invitationId: invitation.id,
},
pendingInvitationConnections: [pendingInvitationsConnectionId],
membershipConnections: [membershipConnectionId],
},
onCompleted: (_, e) => {
if (e) {

View File

@@ -13,6 +13,7 @@ import { useFragment } from "react-relay";
import { Link } from "react-router";
import { graphql } from "relay-runtime";
import type { MembershipCard_organizationFragment$key } from "#/__generated__/iam/MembershipCard_organizationFragment.graphql";
import type { MembershipCardFragment$key } from "#/__generated__/iam/MembershipCardFragment.graphql";
const fragment = graphql`
@@ -21,26 +22,34 @@ const fragment = graphql`
id
expiresAt
}
organization @required(action: THROW) {
id
name
logoUrl
}
}
`;
const organizationFragment = graphql`
fragment MembershipCard_organizationFragment on Organization {
id
name
logoUrl
}
`;
interface MembershipCardProps {
fKey: MembershipCardFragment$key;
organizationFragmentRef: MembershipCard_organizationFragment$key;
}
export function MembershipCard(props: MembershipCardProps) {
const { fKey } = props;
const { fKey, organizationFragmentRef } = props;
const { __ } = useTranslate();
const { lastSession, organization } = useFragment<MembershipCardFragment$key>(
const { lastSession } = useFragment<MembershipCardFragment$key>(
fragment,
fKey,
);
const organization = useFragment<MembershipCard_organizationFragment$key>(
organizationFragment,
organizationFragmentRef,
);
const isExpired
= lastSession && parseDate(lastSession.expiresAt) < new Date();
const isAssuming = !!lastSession && !isExpired;

View File

@@ -23,10 +23,10 @@ export const viewerMembershipLayoutQuery = graphql`
...MembershipsDropdown_organizationFragment
...ViewerMembershipDropdownFragment
...SidebarFragment @skip(if: $hideSidebar)
viewerMembership @required(action: THROW) {
role
profile @required(action: THROW) {
fullName
viewer @required(action: THROW) {
fullName
membership @required(action: THROW) {
role
}
}
}
@@ -91,11 +91,11 @@ export function ViewerMembershipLayout(props: {
<CurrentUser
value={{
email: viewer.email,
fullName: organization.viewerMembership.profile.fullName,
role: organization.viewerMembership.role,
fullName: organization.viewer.fullName,
role: organization.viewer.membership.role,
}}
>
<Outlet context={organization.viewerMembership.role} />
<Outlet context={organization.viewer.membership.role} />
</CurrentUser>
</CoreRelayProvider>
</Layout>

View File

@@ -8,7 +8,7 @@ import { MembershipsDropdownMenuItem } from "./MembershipsDropdownMenuItem";
export const membershipsDropdownMenuQuery = graphql`
query MembershipsDropdownMenuQuery {
viewer @required(action: THROW) {
memberships(
profiles(
first: 1000
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
) @required(action: THROW) {
@@ -17,8 +17,11 @@ export const membershipsDropdownMenuQuery = graphql`
id
organization @required(action: THROW) {
name
...MembershipsDropdownMenuItem_organizationFragment
}
membership @required(action: THROW) {
...MembershipsDropdownMenuItemFragment
}
...MembershipsDropdownMenuItemFragment
}
}
}
@@ -36,27 +39,27 @@ export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
const {
viewer: {
memberships: { edges: initialMemberships },
profiles: { edges: initialProfiles },
},
} = usePreloadedQuery<MembershipsDropdownMenuQuery>(
membershipsDropdownMenuQuery,
queryRef,
);
const memberships = useMemo(() => {
const profiles = useMemo(() => {
if (!search) {
return initialMemberships;
return initialProfiles;
}
return initialMemberships.filter(({ node: { organization } }) =>
return initialProfiles.filter(({ node: { organization } }) =>
organization.name.toLowerCase().includes(search.toLowerCase()),
);
}, [initialMemberships, search]);
}, [initialProfiles, search]);
return (
<>
{memberships.map(({ node }) => (
<MembershipsDropdownMenuItem fKey={node} key={node.id} />
{profiles.map(({ node }) => (
<MembershipsDropdownMenuItem fKey={node.membership} organizationFragmentRef={node.organization} key={node.id} />
))}
</>
);

View File

@@ -10,6 +10,7 @@ import { useFragment } from "react-relay";
import { Link } from "react-router";
import { graphql } from "relay-runtime";
import type { MembershipsDropdownMenuItem_organizationFragment$key } from "#/__generated__/iam/MembershipsDropdownMenuItem_organizationFragment.graphql";
import type { MembershipsDropdownMenuItemFragment$key } from "#/__generated__/iam/MembershipsDropdownMenuItemFragment.graphql";
const fragment = graphql`
@@ -19,21 +20,27 @@ const fragment = graphql`
id
expiresAt
}
organization @required(action: THROW) {
id
logoUrl
name
}
}
`;
const organizationFragment = graphql`
fragment MembershipsDropdownMenuItem_organizationFragment on Organization {
id
name
logoUrl
}
`;
export function MembershipsDropdownMenuItem(props: {
fKey: MembershipsDropdownMenuItemFragment$key;
organizationFragmentRef: MembershipsDropdownMenuItem_organizationFragment$key;
}) {
const { fKey } = props;
const { fKey, organizationFragmentRef } = props;
const { id, lastSession, organization }
const { id, lastSession }
= useFragment<MembershipsDropdownMenuItemFragment$key>(fragment, fKey);
const organization
= useFragment<MembershipsDropdownMenuItem_organizationFragment$key>(organizationFragment, organizationFragmentRef);
const isAssuming = !!lastSession;
const isExpired

View File

@@ -14,18 +14,17 @@ import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import type { ViewerMembershipDropdownFragment$key } from "#/__generated__/iam/ViewerMembershipDropdownFragment.graphql";
import type { ViewerMembershipDropdownSignOutMutation } from "#/__generated__/iam/ViewerMembershipDropdownSignOutMutation.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
export const fragment = graphql`
fragment ViewerMembershipDropdownFragment on Organization {
viewerMembership @required(action: THROW) {
viewer @required(action: THROW) {
fullName
identity @required(action: THROW) {
email
canListAPIKeys: permission(action: "iam:personal-api-key:list")
}
profile @required(action: THROW) {
fullName
}
}
}
`;
@@ -48,12 +47,12 @@ export function ViewerMembershipDropdown(props: {
const { toast } = useToast();
const {
viewerMembership: {
viewer: {
fullName,
identity: { canListAPIKeys, email },
profile: { fullName },
},
} = useFragment<ViewerMembershipDropdownFragment$key>(fragment, fKey);
const [signOut] = useMutation(signOutMutation);
const [signOut] = useMutation<ViewerMembershipDropdownSignOutMutation>(signOutMutation);
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.preventDefault();

View File

@@ -17,7 +17,7 @@ export const peoplePageQuery = graphql`
__typename
... on Organization {
canInviteUser: permission(action: "iam:invitation:create")
members(first: 20, orderBy: { direction: ASC, field: FULL_NAME })
profiles(first: 20, orderBy: { direction: ASC, field: FULL_NAME })
@required(action: THROW) {
totalCount
}
@@ -50,8 +50,8 @@ export function PeoplePage(props: {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">(
"memberships",
const [activeTab, setActiveTab] = useState<"people" | "invitations">(
"people",
);
const { organization } = usePreloadedQuery<PeoplePageQuery>(
@@ -85,12 +85,12 @@ export function PeoplePage(props: {
<Tabs>
<TabItem
active={activeTab === "memberships"}
onClick={() => setActiveTab("memberships")}
active={activeTab === "people"}
onClick={() => setActiveTab("people")}
>
{__("Members")}
{(organization.members.totalCount ?? 0) > 0 && (
<TabBadge>{organization.members.totalCount}</TabBadge>
{__("People")}
{(organization.profiles.totalCount ?? 0) > 0 && (
<TabBadge>{organization.profiles.totalCount}</TabBadge>
)}
</TabItem>
<TabItem
@@ -105,7 +105,7 @@ export function PeoplePage(props: {
</Tabs>
<div className="pb-6 pt-6">
{activeTab === "memberships" && <PeopleList fKey={organization} />}
{activeTab === "people" && <PeopleList fKey={organization} />}
{activeTab === "invitations" && (
<InvitationList

View File

@@ -15,9 +15,11 @@ export const personPageQuery = graphql`
query PersonPageQuery($personId: ID!) {
person: node(id: $personId) @required(action: THROW) {
__typename
... on MembershipProfile {
... on Profile {
fullName
membershipId
membership @required(action: THROW) {
id
}
identity @required(action: THROW) {
email
}
@@ -47,7 +49,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
const navigate = useNavigate();
const { person } = usePreloadedQuery<PersonPageQuery>(personPageQuery, queryRef);
if (person.__typename !== "MembershipProfile") {
if (person.__typename !== "Profile") {
throw new Error("invalid type for node");
}
@@ -65,7 +67,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
return removeMembership({
variables: {
input: {
membershipId: person.membershipId,
membershipId: person.membership.id,
organizationId: organizationId,
},
},

View File

@@ -15,20 +15,20 @@ const fragment = graphql`
@argumentDefinitions(
first: { type: "Int", defaultValue: 20 }
order: {
type: "MembershipOrder"
type: "ProfileOrder"
defaultValue: { direction: ASC, field: FULL_NAME }
}
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
members(
profiles(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "PeopleListFragment_members", filters: ["orderBy"]) @required(action: THROW) {
) @connection(key: "PeopleListFragment_profiles", filters: ["orderBy"]) @required(action: THROW) {
__id
totalCount
edges @required(action: THROW) {
@@ -51,7 +51,7 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
PeopleListFragment$key
>(fragment, fKey);
const refetchMemberships = () => {
const refetchPeople = () => {
peoplePagination.refetch({}, { fetchPolicy: "network-only" });
};
@@ -77,21 +77,21 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
</Tr>
</Thead>
<Tbody>
{peoplePagination.data.members.totalCount === 0
{peoplePagination.data.profiles.totalCount === 0
? (
<Tr>
<Td colSpan={7} className="text-center text-txt-secondary">
{__("No members")}
{__("No people")}
</Td>
</Tr>
)
: (
peoplePagination.data.members.edges.map(({ node: membership }) => (
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
<MemberListItem
connectionId={peoplePagination.data.members.__id}
key={membership.id}
fKey={membership}
onRefetch={refetchMemberships}
connectionId={peoplePagination.data.profiles.__id}
key={profile.id}
fKey={profile}
onRefetch={refetchPeople}
/>
))
)}

View File

@@ -22,23 +22,24 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
import { EditMemberDialog } from "../../settings/_components/EditMemberDialog";
const fragment = graphql`
fragment PeopleListItemFragment on Membership {
fragment PeopleListItemFragment on Profile {
id
role
source
state
profile @required(action: THROW) {
fullName
kind
position
membership @required(action: THROW) {
id
fullName
kind
position
role
source
state
canUpdate: permission(action: "iam:membership:update")
canDelete: permission(action: "iam:membership-profile:delete")
}
identity @required(action: THROW) {
email
}
createdAt
canUpdate: permission(action: "iam:membership:update")
canDelete: permission(action: "iam:membership:delete")
canUpdate: permission(action: "iam:membership-profile:update")
}
`;
@@ -65,9 +66,9 @@ export function MemberListItem(props: {
const confirm = useConfirm();
const [dialogOpen, setDialogOpen] = useState(false);
const membership = useFragment<PeopleListItemFragment$key>(fragment, fKey);
const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey);
const isInactive = membership.state === "INACTIVE";
const isInactive = profile.membership.state === "INACTIVE";
const [removeMembership, isRemoving] = useMutationWithToasts(
removeMemberMutation,
@@ -83,7 +84,7 @@ export function MemberListItem(props: {
return removeMembership({
variables: {
input: {
membershipId: membership.id,
membershipId: profile.membership.id,
organizationId: organizationId,
},
connections: [connectionId],
@@ -93,7 +94,7 @@ export function MemberListItem(props: {
{
message: sprintf(
__("Are you sure you want to remove %s?"),
membership.profile.fullName,
profile.fullName,
),
},
);
@@ -106,33 +107,33 @@ export function MemberListItem(props: {
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
to={`/organizations/${organizationId}/people/${membership.profile.id}`}
to={`/organizations/${organizationId}/people/${profile.id}`}
>
<Td>
<div className="flex items-center gap-2">
<span className="font-semibold">{membership.profile.fullName}</span>
<span className="font-semibold">{profile.fullName}</span>
{isInactive && <Badge variant="neutral">{__("Inactive")}</Badge>}
</div>
</Td>
<Td>
<div className="flex items-center gap-2">
{membership.identity.email}
<Badge variant="info">{membership.source}</Badge>
{profile.identity.email}
<Badge variant="info">{profile.membership.source}</Badge>
</div>
</Td>
<Td>{membership.profile.kind}</Td>
<Td>{profile.kind}</Td>
<Td>
<Badge>{membership.role}</Badge>
<Badge>{profile.membership.role}</Badge>
</Td>
<Td>{new Date(membership.createdAt).toLocaleDateString()}</Td>
<Td>{membership.profile.position}</Td>
<Td>{new Date(profile.createdAt).toLocaleDateString()}</Td>
<Td>{profile.position}</Td>
<Td noLink width={160} className="text-end">
{!isInactive && (
<div
className="flex gap-2 justify-end"
onClick={e => e.stopPropagation()}
>
{membership.canUpdate && (
{profile.membership.canUpdate && (
<Button
variant="secondary"
onClick={() => setDialogOpen(true)}
@@ -146,7 +147,7 @@ export function MemberListItem(props: {
<Spinner size={16} />
)
: (
membership.canDelete && (
profile.membership.canDelete && (
<Button
variant="danger"
onClick={handleRemove}
@@ -164,7 +165,7 @@ export function MemberListItem(props: {
{dialogOpen && (
<EditMemberDialog
onClose={() => setDialogOpen(false)}
membership={membership}
profile={profile}
/>
)}
</>

View File

@@ -13,7 +13,7 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const fragment = graphql`
fragment PersonFormFragment on MembershipProfile {
fragment PersonFormFragment on Profile {
id
fullName
kind

View File

@@ -30,10 +30,10 @@ const updateMembershipMutation = graphql`
`;
export function EditMemberDialog(props: {
membership: PeopleListItemFragment$data;
profile: PeopleListItemFragment$data;
onClose: () => void;
}) {
const { membership, onClose } = props;
const { profile, onClose } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
@@ -41,7 +41,7 @@ export function EditMemberDialog(props: {
const { role } = use(CurrentUser);
const availableRoles = getAssignableRoles(role);
const [selectedRole, setSelectedRole] = useState<string>(membership.role);
const [selectedRole, setSelectedRole] = useState<string>(profile.membership.role);
const [updateMembership, isUpdating] = useMutationWithToasts(
updateMembershipMutation,
@@ -56,7 +56,7 @@ export function EditMemberDialog(props: {
await updateMembership({
variables: {
input: {
membershipId: membership.id,
membershipId: profile.membership.id,
organizationId: organizationId,
role: selectedRole,
},
@@ -73,7 +73,7 @@ export function EditMemberDialog(props: {
<p className="text-txt-secondary text-sm mb-4">
{sprintf(
__("Update the role for %s"),
membership.profile.fullName,
profile.fullName,
)}
</p>
@@ -121,7 +121,7 @@ export function EditMemberDialog(props: {
<DialogFooter>
<Button
type="submit"
disabled={isUpdating || selectedRole === membership.role}
disabled={isUpdating || selectedRole === profile.membership.role}
>
{isUpdating && <Spinner />}
{__("Update Role")}

View File

@@ -8,18 +8,14 @@ import type { SCIMEventListItemFragment$key } from "#/__generated__/iam/SCIMEven
const SCIMEventListItemFragment = graphql`
fragment SCIMEventListItemFragment on SCIMEvent {
id
method
path
statusCode
errorMessage
ipAddress
createdAt
membership {
id
profile {
fullName
}
profile {
fullName
}
}
`;
@@ -93,7 +89,8 @@ export function SCIMEventListItem(props: {
<div className="flex gap-8 text-sm">
<div>
<span className="text-txt-secondary">User: </span>
<span>{event.membership?.profile?.fullName || "-"}</span>
{/* FIXME */}
{/* <span>{event.membership?.profile?.fullName || "-"}</span> */}
</div>
<div>
<span className="text-txt-secondary">IP Address: </span>

View File

@@ -736,13 +736,10 @@ WHERE
args := pgx.StrictNamedArgs{"identity_id": identityID}
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot query iam_membership_profiles: %w", err)
}
row := conn.QueryRow(ctx, q, args)
var count int
err = rows.Scan(&count)
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
@@ -774,13 +771,10 @@ WHERE
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot query iam_membership_profiles: %w", err)
}
row := conn.QueryRow(ctx, q, args)
var count int
err = rows.Scan(&count)
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}

View File

@@ -38,6 +38,8 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
return nil, err
}
filter := coredata.NewMembershipProfileFilter(nil)
if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{
Resolver: r,
@@ -45,8 +47,6 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
}, nil
}
filter := coredata.NewMembershipProfileFilter(nil)
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldFullName,
Direction: page.OrderDirectionAsc,
@@ -1247,6 +1247,8 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
return nil, err
}
filter := coredata.NewMembershipProfileFilter(nil)
if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{
Resolver: r,
@@ -1254,8 +1256,6 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
}, nil
}
filter := coredata.NewMembershipProfileFilter(nil)
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldFullName,
Direction: page.OrderDirectionAsc,
@@ -1514,7 +1514,7 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P
case *identityResolver:
count, err := r.iam.AccountService.CountProfiles(ctx, obj.ParentID, coredata.NewMembershipProfileFilter(nil))
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count sprofiles", log.Error(err))
r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &count, nil

View File

@@ -6576,7 +6576,7 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P
}
switch obj.Resolver.(type) {
case *stateOfApplicabilityResolver:
case *organizationResolver:
count, err := r.iam.OrganizationService.CountProfiles(ctx, obj.ParentID, obj.Filters)
if err != nil {
panic(fmt.Errorf("cannot count profiles: %w", err))