@@ -5,6 +5,7 @@ import { Chip, ChipConfirmModal, ChipModalTabs, ChipTag, toast } from '@sim/emcn
55import { ArrowLeft , KeySquare , Plus } from '@sim/emcn/icons'
66import { getErrorMessage } from '@sim/utils/errors'
77import { useQueryState } from 'nuqs'
8+ import { saveDiscardActions } from '@/components/settings/save-discard-actions'
89import type {
910 CredentialGroupEnrollment ,
1011 CredentialGroupEnrollmentConnection ,
@@ -13,6 +14,7 @@ import type {
1314import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
1415import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
1516import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
17+ import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
1618import {
1719 credentialGroupTabParam ,
1820 credentialGroupTabUrlKeys ,
@@ -26,12 +28,15 @@ import {
2628 SettingsResourceRow ,
2729} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
2830import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
31+ import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard'
2932import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details'
3033import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal'
3134import {
3235 useCredentialGroupDetail ,
36+ useDeleteCredentialGroup ,
3337 useResendCredentialGroupEnrollment ,
3438 useRevokeCredentialGroupEnrollment ,
39+ useUpdateCredentialGroup ,
3540} from '@/hooks/queries/credential-groups'
3641import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
3742
@@ -120,12 +125,17 @@ export function CredentialGroupDetail({
120125 } )
121126 const resend = useResendCredentialGroupEnrollment ( )
122127 const revoke = useRevokeCredentialGroupEnrollment ( )
128+ const updateGroup = useUpdateCredentialGroup ( )
129+ const deleteGroup = useDeleteCredentialGroup ( )
123130 const [ activeTab , setActiveTab ] = useQueryState ( credentialGroupTabParam . key , {
124131 ...credentialGroupTabParam . parser ,
125132 ...credentialGroupTabUrlKeys ,
126133 } )
127134 const [ showInvite , setShowInvite ] = useState ( false )
135+ const [ showDelete , setShowDelete ] = useState ( false )
128136 const [ revokingEnrollmentId , setRevokingEnrollmentId ] = useState < string | null > ( null )
137+ const [ draftName , setDraftName ] = useState < string | null > ( null )
138+ const [ draftDescription , setDraftDescription ] = useState < string | null > ( null )
129139 const credentialGroup = detail . data ?. pages [ 0 ] ?. credentialGroup
130140 const enrollments = detail . data ?. pages . flatMap ( ( page ) => page . enrollments ) ?? [ ]
131141 const revokingEnrollment = revokingEnrollmentId
@@ -144,14 +154,65 @@ export function CredentialGroupDetail({
144154 slackBots . data ?. some ( ( bot ) => bot . id === option . slackBotCredentialId ) )
145155 )
146156
157+ const name = draftName ?? credentialGroup ?. name ?? ''
158+ const description = draftDescription ?? credentialGroup ?. description ?? ''
159+ const normalizedDescription = description . trim ( ) || null
160+ const detailsDirty = Boolean (
161+ credentialGroup &&
162+ ( name . trim ( ) !== credentialGroup . name ||
163+ normalizedDescription !== credentialGroup . description )
164+ )
165+ const guard = useSettingsUnsavedGuard ( { isDirty : detailsDirty } )
166+
167+ const discardDetails = ( ) => {
168+ setDraftName ( null )
169+ setDraftDescription ( null )
170+ }
171+
172+ const handleSaveDetails = async ( ) => {
173+ if ( ! credentialGroup || ! name . trim ( ) ) return
174+ try {
175+ await updateGroup . mutateAsync ( {
176+ workspaceId,
177+ groupId : credentialGroup . id ,
178+ body : { name : name . trim ( ) , description : normalizedDescription } ,
179+ } )
180+ discardDetails ( )
181+ toast . success ( 'Details saved' )
182+ } catch ( error ) {
183+ toast . error ( getErrorMessage ( error , 'Could not save details' ) )
184+ }
185+ }
186+
187+ /**
188+ * Each tab owns its own primary action: Details commits the edited name and
189+ * description, People invites more users. Delete is available from both.
190+ */
147191 const actions : SettingsAction [ ] = credentialGroup
148192 ? [
193+ ...( activeTab === 'details'
194+ ? saveDiscardActions ( {
195+ dirty : detailsDirty ,
196+ saving : updateGroup . isPending ,
197+ onSave : ( ) => void handleSaveDetails ( ) ,
198+ onDiscard : discardDetails ,
199+ saveDisabled : ! name . trim ( ) ,
200+ saveTooltip : name . trim ( ) ? undefined : 'Name is required' ,
201+ } )
202+ : [
203+ {
204+ text : 'Invite users' ,
205+ icon : Plus ,
206+ variant : 'primary' as const ,
207+ onSelect : ( ) => setShowInvite ( true ) ,
208+ disabled : credentialGroup . status !== 'active' || ! configurationReady ,
209+ } ,
210+ ] ) ,
149211 {
150- text : 'Invite users' ,
151- icon : Plus ,
152- variant : 'primary' ,
153- onSelect : ( ) => setShowInvite ( true ) ,
154- disabled : credentialGroup . status !== 'active' || ! configurationReady ,
212+ id : 'delete' ,
213+ text : deleteGroup . isPending ? 'Deleting...' : 'Delete' ,
214+ onSelect : ( ) => setShowDelete ( true ) ,
215+ disabled : deleteGroup . isPending ,
155216 } ,
156217 ]
157218 : [ ]
@@ -180,15 +241,25 @@ export function CredentialGroupDetail({
180241 }
181242 }
182243
183- const handleBack = ( ) => {
184- void setActiveTab ( null , { history : 'replace' } )
185- onBack ( )
244+ const handleDelete = async ( ) => {
245+ if ( ! credentialGroup ) return
246+ try {
247+ await deleteGroup . mutateAsync ( { workspaceId, groupId } )
248+ setShowDelete ( false )
249+ onBack ( )
250+ } catch ( error ) {
251+ toast . error ( getErrorMessage ( error , 'Could not delete credential group' ) )
252+ }
186253 }
187254
188255 return (
189256 < >
190257 < SettingsPanel
191- back = { { text : 'Credential groups' , icon : ArrowLeft , onSelect : handleBack } }
258+ back = { {
259+ text : 'Credential groups' ,
260+ icon : ArrowLeft ,
261+ onSelect : ( ) => guard . guardBack ( onBack ) ,
262+ } }
192263 title = { credentialGroup ?. name ?? 'Credential group' }
193264 description = { credentialGroup ?. description ?? undefined }
194265 actions = { actions }
@@ -198,7 +269,7 @@ export function CredentialGroupDetail({
198269 { getErrorMessage ( detail . error , "Couldn't load credential group" ) }
199270 </ SettingsEmptyState >
200271 ) : detail . isPending || ! credentialGroup ? null : (
201- < div className = 'flex flex-col gap-7' >
272+ < >
202273 < ChipModalTabs
203274 tabs = { CREDENTIAL_GROUP_TABS }
204275 value = { activeTab }
@@ -207,7 +278,14 @@ export function CredentialGroupDetail({
207278 />
208279
209280 { activeTab === 'details' && (
210- < CredentialGroupDetails workspaceId = { workspaceId } credentialGroup = { credentialGroup } />
281+ < CredentialGroupDetails
282+ workspaceId = { workspaceId }
283+ credentialGroup = { credentialGroup }
284+ name = { name }
285+ onNameChange = { setDraftName }
286+ description = { description }
287+ onDescriptionChange = { setDraftDescription }
288+ />
211289 ) }
212290
213291 { activeTab === 'people' && (
@@ -233,7 +311,8 @@ export function CredentialGroupDetail({
233311 return (
234312 < SettingsResourceRow
235313 key = { enrollment . id }
236- icon = { < KeySquare /> }
314+ icon = { < KeySquare className = 'text-[var(--text-icon)]' /> }
315+ iconFilled
237316 title = { enrollment . email }
238317 description = {
239318 < EnrollmentConnections connections = { enrollment . connections } />
@@ -272,7 +351,7 @@ export function CredentialGroupDetail({
272351 ) }
273352 </ SettingsSection >
274353 ) }
275- </ div >
354+ </ >
276355 ) }
277356 </ SettingsPanel >
278357 { credentialGroup && (
@@ -296,6 +375,27 @@ export function CredentialGroupDetail({
296375 disabled : revoke . isPending ,
297376 } }
298377 />
378+ < ChipConfirmModal
379+ open = { showDelete }
380+ onOpenChange = { ( open ) => ! open && ! deleteGroup . isPending && setShowDelete ( false ) }
381+ srTitle = 'Delete credential group'
382+ title = 'Delete credential group'
383+ text = { [
384+ `Delete ${ credentialGroup ?. name ?? 'this credential group' } ?` ,
385+ { text : ' This cannot be undone.' , error : true } ,
386+ ] }
387+ dismissLabel = 'Cancel'
388+ confirm = { {
389+ label : deleteGroup . isPending ? 'Deleting...' : 'Delete' ,
390+ onClick : handleDelete ,
391+ disabled : deleteGroup . isPending ,
392+ } }
393+ />
394+ < UnsavedChangesModal
395+ open = { guard . showUnsavedModal }
396+ onOpenChange = { guard . setShowUnsavedModal }
397+ onDiscard = { guard . confirmDiscard }
398+ />
299399 </ >
300400 )
301401}
0 commit comments