Skip to content

Commit 4fbc52e

Browse files
fix(credential-groups): simplify people actions
1 parent 86fa5e0 commit 4fbc52e

5 files changed

Lines changed: 84 additions & 121 deletions

File tree

apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/enrollments/[enrollmentId]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const DELETE = defineInternalJsonRoute({
1616
reason: 'Preserve existing internal Credential Group revocation behavior',
1717
}),
1818
errorPolicy: createCredentialGroupInternalErrorPolicy(
19-
'Failed to revoke credential group enrollment'
19+
'Failed to delete person from credential group'
2020
),
2121
mapInput: ({ params }) => ({
2222
assertedWorkspaceId: params.id,

apps/sim/ee/credential-groups/components/credential-group-detail.test.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

apps/sim/ee/credential-groups/components/credential-group-detail.tsx

Lines changed: 43 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Chip, ChipConfirmModal, ChipModalTabs, ChipTag, toast } from '@sim/emcn'
4+
import { Chip, ChipConfirmModal, ChipModalTabs, toast } from '@sim/emcn'
55
import { ArrowLeft, Plus, User } from '@sim/emcn/icons'
66
import { getErrorMessage } from '@sim/utils/errors'
77
import { useQueryState } from 'nuqs'
88
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
99
import type {
1010
CredentialGroupEnrollment,
1111
CredentialGroupEnrollmentConnection,
12-
CredentialGroupEnrollmentDetail,
1312
} from '@/lib/api/contracts/credential-groups'
1413
import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
1514
import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
@@ -53,35 +52,6 @@ const CREDENTIAL_GROUP_TABS = [
5352
{ value: 'people', label: 'People' },
5453
] as const
5554

56-
export function getEnrollmentStatus(
57-
enrollment: CredentialGroupEnrollmentDetail,
58-
activeProviders: CredentialGroupProvider[]
59-
) {
60-
if (enrollment.status === 'revoked') return { label: 'Revoked', invalid: false }
61-
if (enrollment.status === 'delivery_failed') return { label: 'Delivery failed', invalid: true }
62-
if (enrollment.status !== 'completed' && enrollment.expired) {
63-
return { label: 'Expired', invalid: true }
64-
}
65-
const needsReauthorization = enrollment.connections.some(
66-
(connection) => connection.status === 'needs_reauth'
67-
)
68-
if (needsReauthorization) return { label: 'Reconnect needed', invalid: false }
69-
const connectedProviders = new Set(
70-
enrollment.connections
71-
.filter((connection) => connection.status === 'active')
72-
.map((connection) => connection.provider)
73-
)
74-
const allProvidersConnected =
75-
activeProviders.length > 0 &&
76-
activeProviders.every((provider) => connectedProviders.has(provider))
77-
if (enrollment.status === 'completed' && allProvidersConnected) {
78-
return { label: 'Connected', invalid: false }
79-
}
80-
if (enrollment.status === 'completed') return { label: 'In progress', invalid: false }
81-
if (enrollment.status === 'in_progress') return { label: 'In progress', invalid: false }
82-
return { label: 'Invited', invalid: false }
83-
}
84-
8555
interface EnrollmentConnectionsProps {
8656
connections: CredentialGroupEnrollmentConnection[]
8757
}
@@ -124,7 +94,7 @@ export function CredentialGroupDetail({
12494
providerId: SLACK_CUSTOM_BOT_PROVIDER_ID,
12595
})
12696
const resend = useResendCredentialGroupEnrollment()
127-
const revoke = useRevokeCredentialGroupEnrollment()
97+
const deleteEnrollment = useRevokeCredentialGroupEnrollment()
12898
const updateGroup = useUpdateCredentialGroup()
12999
const deleteGroup = useDeleteCredentialGroup()
130100
const [activeTab, setActiveTab] = useQueryState(credentialGroupTabParam.key, {
@@ -133,18 +103,14 @@ export function CredentialGroupDetail({
133103
})
134104
const [showInvite, setShowInvite] = useState(false)
135105
const [showDelete, setShowDelete] = useState(false)
136-
const [revokingEnrollmentId, setRevokingEnrollmentId] = useState<string | null>(null)
106+
const [deletingEnrollmentId, setDeletingEnrollmentId] = useState<string | null>(null)
137107
const [draftName, setDraftName] = useState<string | null>(null)
138108
const [draftDescription, setDraftDescription] = useState<string | null>(null)
139109
const credentialGroup = detail.data?.pages[0]?.credentialGroup
140110
const enrollments = detail.data?.pages.flatMap((page) => page.enrollments) ?? []
141-
const revokingEnrollment = revokingEnrollmentId
142-
? (enrollments.find((enrollment) => enrollment.id === revokingEnrollmentId) ?? null)
111+
const deletingEnrollment = deletingEnrollmentId
112+
? (enrollments.find((enrollment) => enrollment.id === deletingEnrollmentId) ?? null)
143113
: null
144-
const activeProviders =
145-
credentialGroup?.options
146-
.filter((option) => option.status === 'active')
147-
.map((option) => option.provider) ?? []
148114
const configurationReady =
149115
Boolean(credentialGroup?.options.length) &&
150116
credentialGroup?.options.every(
@@ -226,18 +192,18 @@ export function CredentialGroupDetail({
226192
}
227193
}
228194

229-
const handleRevoke = async () => {
230-
if (!revokingEnrollment) return
195+
const handleDeleteEnrollment = async () => {
196+
if (!deletingEnrollment) return
231197
try {
232-
await revoke.mutateAsync({
198+
await deleteEnrollment.mutateAsync({
233199
workspaceId,
234200
groupId,
235-
enrollmentId: revokingEnrollment.id,
201+
enrollmentId: deletingEnrollment.id,
236202
})
237-
toast.success(`Access revoked for ${revokingEnrollment.email}`)
238-
setRevokingEnrollmentId(null)
203+
toast.success(`${deletingEnrollment.email} deleted`)
204+
setDeletingEnrollmentId(null)
239205
} catch (error) {
240-
toast.error(getErrorMessage(error, 'Failed to revoke access'))
206+
toast.error(getErrorMessage(error, 'Failed to delete person'))
241207
}
242208
}
243209

@@ -307,7 +273,6 @@ export function CredentialGroupDetail({
307273
) : (
308274
<div className={RESOURCE_LIST_STACK}>
309275
{enrollments.map((enrollment) => {
310-
const status = getEnrollmentStatus(enrollment, activeProviders)
311276
return (
312277
<SettingsResourceRow
313278
key={enrollment.id}
@@ -317,32 +282,22 @@ export function CredentialGroupDetail({
317282
description={
318283
<EnrollmentConnections connections={enrollment.connections} />
319284
}
320-
badge={
321-
<ChipTag
322-
variant={status.invalid ? 'invite' : 'gray'}
323-
invalid={status.invalid}
324-
>
325-
{status.label}
326-
</ChipTag>
327-
}
328285
trailing={
329-
enrollment.status === 'revoked' ? undefined : (
330-
<RowActionsMenu
331-
label={`${enrollment.email} actions`}
332-
actions={[
333-
{
334-
label: 'Resend',
335-
onSelect: () => void handleResend(enrollment),
336-
disabled: resend.isPending,
337-
},
338-
{
339-
label: 'Revoke',
340-
destructive: true,
341-
onSelect: () => setRevokingEnrollmentId(enrollment.id),
342-
},
343-
]}
344-
/>
345-
)
286+
<RowActionsMenu
287+
label={`${enrollment.email} actions`}
288+
actions={[
289+
{
290+
label: 'Resend',
291+
onSelect: () => void handleResend(enrollment),
292+
disabled: resend.isPending,
293+
},
294+
{
295+
label: 'Delete',
296+
destructive: true,
297+
onSelect: () => setDeletingEnrollmentId(enrollment.id),
298+
},
299+
]}
300+
/>
346301
}
347302
/>
348303
)
@@ -363,16 +318,24 @@ export function CredentialGroupDetail({
363318
/>
364319
)}
365320
<ChipConfirmModal
366-
open={Boolean(revokingEnrollment)}
367-
onOpenChange={(open) => !open && !revoke.isPending && setRevokingEnrollmentId(null)}
368-
srTitle='Revoke access'
369-
title='Revoke access?'
370-
text={`Revoke access for ${revokingEnrollment?.email ?? 'this user'}? Their private link will stop working and all accounts they connected to this Credential Group will be removed.`}
321+
open={Boolean(deletingEnrollment)}
322+
onOpenChange={(open) =>
323+
!open && !deleteEnrollment.isPending && setDeletingEnrollmentId(null)
324+
}
325+
srTitle='Delete person'
326+
title='Delete person'
327+
text={[
328+
`Delete ${deletingEnrollment?.email ?? 'this person'}?`,
329+
{
330+
text: ' Their private link will stop working and all accounts they connected to this Credential Group will be removed.',
331+
error: true,
332+
},
333+
]}
371334
dismissLabel='Cancel'
372335
confirm={{
373-
label: revoke.isPending ? 'Revoking...' : 'Revoke',
374-
onClick: handleRevoke,
375-
disabled: revoke.isPending,
336+
label: deleteEnrollment.isPending ? 'Deleting...' : 'Delete',
337+
onClick: handleDeleteEnrollment,
338+
disabled: deleteEnrollment.isPending,
376339
}}
377340
/>
378341
<ChipConfirmModal

apps/sim/lib/credential-groups/application/manage-groups.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const mocks = vi.hoisted(() => ({
88
create: vi.fn(),
9+
get: vi.fn(),
910
list: vi.fn(),
11+
listEnrollments: vi.fn(),
1012
requireAvailable: vi.fn(),
1113
resolveGroup: vi.fn(),
1214
resolvePermission: vi.fn(),
@@ -22,11 +24,23 @@ vi.mock('@/lib/credential-groups/application/context', () => ({
2224
vi.mock('@/lib/credential-groups/service', () => ({
2325
createCredentialGroup: mocks.create,
2426
deleteCredentialGroup: vi.fn(),
25-
getCredentialGroup: vi.fn(),
27+
getCredentialGroup: mocks.get,
2628
listCredentialGroups: mocks.list,
2729
updateCredentialGroup: vi.fn(),
2830
}))
2931

32+
vi.mock('@/lib/credential-groups/enrollments', () => ({
33+
CredentialGroupEnrollmentError: class CredentialGroupEnrollmentError extends Error {
34+
constructor(
35+
message: string,
36+
readonly status: number
37+
) {
38+
super(message)
39+
}
40+
},
41+
listCredentialGroupEnrollments: mocks.listEnrollments,
42+
}))
43+
3044
vi.mock('@sim/platform-authz/workspace', () => ({
3145
permissionSatisfies: (permission: string | null, required: string) =>
3246
permission === 'admin' || permission === required,
@@ -35,6 +49,7 @@ vi.mock('@sim/platform-authz/workspace', () => ({
3549

3650
import {
3751
createCredentialGroupSettings,
52+
getCredentialGroupSettings,
3853
listCredentialGroupSettings,
3954
} from '@/lib/credential-groups/application/manage-groups'
4055

@@ -62,10 +77,17 @@ describe('Credential Group Settings application operations', () => {
6277
beforeEach(() => {
6378
vi.clearAllMocks()
6479
mocks.resolveWorkspace.mockResolvedValue(workspaceContext)
80+
mocks.resolveGroup.mockResolvedValue({
81+
...workspaceContext,
82+
credentialGroupId: 'group-1',
83+
name: 'Support',
84+
})
6585
mocks.resolvePermission.mockResolvedValue('admin')
6686
mocks.requireAvailable.mockResolvedValue(undefined)
6787
mocks.list.mockResolvedValue([])
6888
mocks.create.mockResolvedValue({ id: 'group-1', name: 'Support' })
89+
mocks.get.mockResolvedValue({ id: 'group-1', name: 'Support' })
90+
mocks.listEnrollments.mockResolvedValue({ enrollments: [], nextCursor: null })
6991
})
7092

7193
it('rejects an enrollment bearer before loading workspace settings', async () => {
@@ -115,4 +137,19 @@ describe('Credential Group Settings application operations', () => {
115137
options: [],
116138
})
117139
})
140+
141+
it('excludes deleted people from Credential Group settings', async () => {
142+
await getCredentialGroupSettings.execute({
143+
principal: sessionPrincipal,
144+
input: {
145+
assertedWorkspaceId: 'workspace-1',
146+
credentialGroupId: 'group-1',
147+
limit: 50,
148+
},
149+
})
150+
151+
expect(mocks.listEnrollments).toHaveBeenCalledWith('workspace-1', 'group-1', 50, undefined, {
152+
statuses: ['invited', 'in_progress', 'completed', 'delivery_failed'],
153+
})
154+
})
118155
})

apps/sim/lib/credential-groups/application/manage-groups.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ export const getCredentialGroupSettings = defineAuthorizedWorkspaceUseCase({
108108
context.workspaceId,
109109
context.credentialGroupId,
110110
input.limit,
111-
input.cursor
111+
input.cursor,
112+
{ statuses: ['invited', 'in_progress', 'completed', 'delivery_failed'] }
112113
)
113114
return { credentialGroup, ...enrollmentPage }
114115
} catch (error) {

0 commit comments

Comments
 (0)