Skip to content

Commit 0ce8ded

Browse files
authored
improvement(credential-groups): align settings surface with the shared page patterns (#6727)
* improvement(credential-groups): align settings surface with the shared page patterns - drop the row "..." menu; a row opening a detail page carries the chevron only, and Delete moves to the detail header behind a confirm modal - replace the hand-rolled Save chip with saveDiscardActions, and wire useSettingsUnsavedGuard so detail edits survive tab switches - fix swapped staleTime constants: the list carried Infinity, which combined with the app-wide retryOnMount:false to cache one transient failure until a full page reload - evict the detail query on delete, and keep the bots prop referentially stable so a refetch cannot drop a queued Slack authorization message - reset the detail tab param on open and close so a stale link cannot open the next group on the previous group's tab - match peer rows (iconFilled + --text-icon), drop a bespoke max-w and a duplicated gap-7, align no-results copy and the Slack modal field gutter * improvement(credential-groups): hold first paint for a deep-linked group Matches the data-drains list: a deep link whose id is still resolving no longer flashes the list chrome before jumping to the detail. Keys the detail by group id so lifted draft state can never carry across groups. * fix(credential-groups): await the refetch before clearing the edit buffer The update mutation fired its invalidations without returning them, so mutateAsync resolved before the refetch landed. Callers that clear their draft on success then fell back onto the pre-save cache and flashed the old name and description until the refetch completed — or kept showing them if it failed.
1 parent a9688d0 commit 0ce8ded

6 files changed

Lines changed: 374 additions & 275 deletions

File tree

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

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Chip, ChipConfirmModal, ChipModalTabs, ChipTag, toast } from '@sim/emcn
55
import { ArrowLeft, KeySquare, Plus } from '@sim/emcn/icons'
66
import { getErrorMessage } from '@sim/utils/errors'
77
import { useQueryState } from 'nuqs'
8+
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
89
import type {
910
CredentialGroupEnrollment,
1011
CredentialGroupEnrollmentConnection,
@@ -13,6 +14,7 @@ import type {
1314
import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
1415
import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
1516
import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
17+
import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
1618
import {
1719
credentialGroupTabParam,
1820
credentialGroupTabUrlKeys,
@@ -26,12 +28,15 @@ import {
2628
SettingsResourceRow,
2729
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
2830
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
31+
import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard'
2932
import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details'
3033
import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal'
3134
import {
3235
useCredentialGroupDetail,
36+
useDeleteCredentialGroup,
3337
useResendCredentialGroupEnrollment,
3438
useRevokeCredentialGroupEnrollment,
39+
useUpdateCredentialGroup,
3540
} from '@/hooks/queries/credential-groups'
3641
import { 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

Comments
 (0)