Split user remove and archive actions
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
committed by
Bryan Frimin
parent
a71a7bb56f
commit
1e08a23ddc
@@ -14,7 +14,7 @@
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
|
||||
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconArchive, IconTrashCan, useConfirm } from "@probo/ui";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { useNavigate } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
@@ -34,6 +34,7 @@ export const personPageQuery = graphql`
|
||||
fullName
|
||||
emailAddress
|
||||
source
|
||||
state
|
||||
canDelete: permission(action: "iam:membership-profile:delete")
|
||||
...PersonFormFragment
|
||||
}
|
||||
@@ -51,6 +52,16 @@ const removeUserMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const archiveUserMutation = graphql`
|
||||
mutation PersonPage_archiveMutation(
|
||||
$input: ArchiveUserInput!
|
||||
) {
|
||||
archiveUser(input: $input) {
|
||||
archivedProfileId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> }) {
|
||||
const { queryRef } = props;
|
||||
|
||||
@@ -64,18 +75,25 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
||||
throw new Error("invalid type for node");
|
||||
}
|
||||
|
||||
const [removeUser, isRemoving] = useMutationWithToasts(
|
||||
removeUserMutation,
|
||||
const [archiveUser, isArchiving] = useMutationWithToasts(
|
||||
archiveUserMutation,
|
||||
{
|
||||
successMessage: __("Person archived successfully"),
|
||||
errorMessage: __("Failed to archive person"),
|
||||
},
|
||||
);
|
||||
const [removeUser, isRemoving] = useMutationWithToasts(
|
||||
removeUserMutation,
|
||||
{
|
||||
successMessage: __("Person removed successfully"),
|
||||
errorMessage: __("Failed to remove person"),
|
||||
},
|
||||
);
|
||||
|
||||
const handleRemove = () => {
|
||||
const handleArchive = () => {
|
||||
confirm(
|
||||
() => {
|
||||
return removeUser({
|
||||
return archiveUser({
|
||||
variables: {
|
||||
input: {
|
||||
profileId: person.id,
|
||||
@@ -96,7 +114,32 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
||||
);
|
||||
};
|
||||
|
||||
const canArchive = person.canDelete && person.source !== "SCIM";
|
||||
const handleRemove = () => {
|
||||
confirm(
|
||||
() => {
|
||||
return removeUser({
|
||||
variables: {
|
||||
input: {
|
||||
profileId: person.id,
|
||||
organizationId: organizationId,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
void navigate(`/organizations/${organizationId}/people`);
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to remove %s?"),
|
||||
person.fullName,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const canArchive = person.canDelete && person.source !== "SCIM" && person.state !== "INACTIVE";
|
||||
const canRemove = person.canDelete && person.source !== "SCIM";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -122,16 +165,27 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
||||
<div className="text-lg text-txt-secondary">{person.emailAddress}</div>
|
||||
</div>
|
||||
</div>
|
||||
{canArchive && (
|
||||
{(canArchive || canRemove) && (
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleRemove}
|
||||
disabled={isRemoving}
|
||||
>
|
||||
{__("Archive")}
|
||||
</DropdownItem>
|
||||
{canArchive && (
|
||||
<DropdownItem
|
||||
icon={IconArchive}
|
||||
onClick={handleArchive}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{__("Archive")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canRemove && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleRemove}
|
||||
disabled={isRemoving}
|
||||
>
|
||||
{__("Remove")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
DropdownItem,
|
||||
IconArchive,
|
||||
IconMail,
|
||||
IconTrashCan,
|
||||
Option,
|
||||
@@ -107,6 +108,14 @@ const removeUserMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const archiveUserMutation = graphql`
|
||||
mutation PeopleListItem_archiveMutation($input: ArchiveUserInput!) {
|
||||
archiveUser(input: $input) {
|
||||
archivedProfileId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function PeopleListItem(props: {
|
||||
connectionId: DataID;
|
||||
fKey: PeopleListItemFragment$key;
|
||||
@@ -128,6 +137,7 @@ export function PeopleListItem(props: {
|
||||
|
||||
const canSendActivationMail = isInactive && profile.source !== "SCIM" && profile.canInvite;
|
||||
const canArchive = profile.canDelete && profile.source !== "SCIM" && profile.state !== "INACTIVE";
|
||||
const canRemove = profile.canDelete && profile.source !== "SCIM";
|
||||
|
||||
const [inviteUser]
|
||||
= useMutationWithToasts<PeopleListItem_inviteMutation>(inviteUserMutation, {
|
||||
@@ -141,13 +151,21 @@ export function PeopleListItem(props: {
|
||||
errorMessage: __("Failed to update role"),
|
||||
},
|
||||
);
|
||||
const [removeUser, isRemoving] = useMutationWithToasts(
|
||||
removeUserMutation,
|
||||
const [archiveUser, isArchiving] = useMutationWithToasts(
|
||||
archiveUserMutation,
|
||||
{
|
||||
successMessage: __("Person archived successfully"),
|
||||
errorMessage: __("Failed to archive person"),
|
||||
},
|
||||
);
|
||||
const [removeUser, isRemoving] = useMutationWithToasts(
|
||||
removeUserMutation,
|
||||
{
|
||||
successMessage: __("Person removed successfully"),
|
||||
errorMessage: __("Failed to remove person"),
|
||||
},
|
||||
);
|
||||
const isMutating = isArchiving || isRemoving;
|
||||
|
||||
const handleInvite = () => {
|
||||
confirm(
|
||||
@@ -183,6 +201,29 @@ export function PeopleListItem(props: {
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleArchive = () => {
|
||||
confirm(
|
||||
() => {
|
||||
return archiveUser({
|
||||
variables: {
|
||||
input: {
|
||||
profileId: profile.id,
|
||||
organizationId: organizationId,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to archive %s?"),
|
||||
profile.fullName,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
const handleRemove = () => {
|
||||
confirm(
|
||||
() => {
|
||||
@@ -201,7 +242,7 @@ export function PeopleListItem(props: {
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to archive %s?"),
|
||||
__("Are you sure you want to remove %s?"),
|
||||
profile.fullName,
|
||||
),
|
||||
},
|
||||
@@ -211,7 +252,7 @@ export function PeopleListItem(props: {
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/people/${profile.id}`}>
|
||||
<Td className={clsx(
|
||||
isRemoving && "opacity-60 pointer-events-none",
|
||||
isMutating && "opacity-60 pointer-events-none",
|
||||
isInactive && "opacity-50",
|
||||
)}
|
||||
>
|
||||
@@ -221,7 +262,7 @@ export function PeopleListItem(props: {
|
||||
<Badge variant={profile.state === "INACTIVE" ? "neutral" : "success"}>{profile.state}</Badge>
|
||||
</Td>
|
||||
<Td className={clsx(
|
||||
isRemoving && "opacity-60 pointer-events-none",
|
||||
isMutating && "opacity-60 pointer-events-none",
|
||||
isInactive && "opacity-50",
|
||||
)}
|
||||
>
|
||||
@@ -235,7 +276,7 @@ export function PeopleListItem(props: {
|
||||
noLink
|
||||
className={clsx(
|
||||
"pr-4",
|
||||
isRemoving && "opacity-60 pointer-events-none",
|
||||
isMutating && "opacity-60 pointer-events-none",
|
||||
isInactive && "opacity-50",
|
||||
)}
|
||||
>
|
||||
@@ -263,14 +304,14 @@ export function PeopleListItem(props: {
|
||||
</Td>
|
||||
)}
|
||||
<Td className={clsx(
|
||||
isRemoving && "opacity-60 pointer-events-none",
|
||||
isMutating && "opacity-60 pointer-events-none",
|
||||
isInactive && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{new Date(profile.createdAt).toLocaleDateString()}
|
||||
</Td>
|
||||
<Td noLink width={160} className="text-end">
|
||||
{(canSendActivationMail || canArchive) && (
|
||||
{(canSendActivationMail || canArchive || canRemove) && (
|
||||
<ActionDropdown>
|
||||
{canSendActivationMail && (
|
||||
<DropdownItem
|
||||
@@ -281,12 +322,20 @@ export function PeopleListItem(props: {
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canArchive && (
|
||||
<DropdownItem
|
||||
onClick={handleArchive}
|
||||
icon={IconArchive}
|
||||
>
|
||||
{__("Archive person")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canRemove && (
|
||||
<DropdownItem
|
||||
onClick={handleRemove}
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Archive person")}
|
||||
{__("Remove person")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
|
||||
Reference in New Issue
Block a user