Skip to content

Commit 4f00baf

Browse files
emir-karabegwaleedlatif1claude
authored
refactor(emcn): make ChipModal footer/header props-driven and migrate all consumers (#4905)
* refactor(emcn): make ChipModal footer/header props-driven and migrate all consumers * fix(emcn): restore Cancel disable guard for in-flight ChipModal actions Add `cancelDisabled` to ChipModalFooter and thread the pre-migration in-flight guards back into all 45 footers that had them, so destructive flows can no longer be dismissed mid-mutation. * feat(emcn): add ChipConfirmModal confirmation primitive Confirmations have a different button grammar than form modals: a named dismiss decision ('Keep editing') plus a usually-destructive confirm, with a single dismiss path — not the structural, never-relabeled Cancel that ChipModalFooter guarantees. Forcing confirmations through the form footer produced both the ambiguous-'Cancel' copy loss and the header-X/footer-Cancel state-reset drift. ChipConfirmModal models that grammar directly and owns the safety rails every hand-rolled confirm had to remember: header-X / dismiss button / Esc all route through onOpenChange (teardown can't desync), and 'pending' disables the dismiss while the action is in flight. Extract a shared ChipModalFooterShell so the footer chrome stays a single source of truth across both components. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(emcn): migrate all confirmation dialogs to ChipConfirmModal Migrate ~30 destructive-confirm and unsaved-changes dialogs across the app from hand-composed ChipModal + ChipModalFooter to the declarative ChipConfirmModal. Net effect: - Unsaved-changes dialogs read 'Keep editing' again (was an ambiguous 'Cancel' after the footer migration) via dismissLabel. - Header-X and dismiss now share one teardown path, fixing the api-keys / copilot / scheduled-tasks / base-tags / tables cases where dismissing via the X left the targeted row selected. - In-flight 'pending' disables the dismiss button uniformly, so destructive confirmations can't be dismissed mid-mutation. - Confirm-modal widths harmonized to 'sm' (several were an oversized 'md'). Form/editor modals and the three-way access-control unsaved dialog keep ChipModalFooter (structural Cancel is correct there). Also restores the JSON-cell font size in row-modal and the in-flight Cancel guard in invite-modal flagged in review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(emcn): normalize confirm pendingLabel ellipsis to '...' Two dialogs preserved a typographic '…' from their pre-migration copy; the codebase uses three-dot '...' everywhere else. Align for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(emcn): restore Enter-to-submit in form modals after footer migration The props-driven footer renders its primary action as a Chip (type='button'), so multi-field <form> modals lost implicit Enter submission when the old type='submit' control was removed. Add a hidden, disabled-mirroring submit button — the existing codebase idiom (a2a.tsx, mcp.tsx) — to create-base, row, and help modals so Enter submits exactly as before and still respects each form's in-flight/validation guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: waleed <walif6@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7434df9 commit 4f00baf

79 files changed

Lines changed: 2036 additions & 2264 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Eye, EyeOff } from 'lucide-react'
77
import Link from 'next/link'
88
import { useRouter, useSearchParams } from 'next/navigation'
99
import {
10-
Chip,
1110
ChipModal,
1211
ChipModalBody,
1312
ChipModalError,
@@ -560,24 +559,15 @@ export default function LoginPage({
560559
{resetStatus.type === 'error' ? resetStatus.message : null}
561560
</ChipModalError>
562561
</ChipModalBody>
563-
<ChipModalFooter>
564-
<Chip
565-
variant='filled'
566-
flush
567-
onClick={() => setForgotPasswordOpen(false)}
568-
disabled={isSubmittingReset}
569-
>
570-
Cancel
571-
</Chip>
572-
<Chip
573-
variant='primary'
574-
flush
575-
onClick={handleForgotPassword}
576-
disabled={!forgotPasswordEmail || isSubmittingReset}
577-
>
578-
{isSubmittingReset ? 'Sending…' : 'Send Reset Link'}
579-
</Chip>
580-
</ChipModalFooter>
562+
<ChipModalFooter
563+
onCancel={() => setForgotPasswordOpen(false)}
564+
cancelDisabled={isSubmittingReset}
565+
primaryAction={{
566+
label: isSubmittingReset ? 'Sending…' : 'Send Reset Link',
567+
onClick: handleForgotPassword,
568+
disabled: !forgotPasswordEmail || isSubmittingReset,
569+
}}
570+
/>
581571
</ChipModal>
582572
</>
583573
)

apps/sim/app/(landing)/integrations/(shell)/[slug]/components/template-card-button.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { cn } from '@/lib/core/utils/cn'
66
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
77

88
interface TemplateCardButtonProps {
9+
/**
10+
* Curated template prompt, already rewritten to `@`-mention form by the
11+
* page's server-side `mentionifyPromptForNames` (registry-free, so the
12+
* landing client bundle never pulls the full block registry). Stored verbatim
13+
* for the home input to consume after signup.
14+
*/
915
prompt: string
1016
className?: string
1117
children: React.ReactNode

apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,37 @@ function sentenceWithTerminalPunctuation(value: string): string {
8787
return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.`
8888
}
8989

90+
function escapeRegex(value: string): string {
91+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
92+
}
93+
94+
/**
95+
* Server-side rewrite of bare integration names in a curated template prompt
96+
* to `@`-mention form (`Slack` → `@Slack`) so the prompt chips with brand
97+
* icons once it is populated into the Mothership home input after signup —
98+
* the home auto-mention pipeline only chips token-starting `@` mentions, so
99+
* curated prompts must opt in.
100+
*
101+
* Unlike the workspace surface, which calls `mentionifyIntegrations` from
102+
* `@/blocks/integration-matcher`, this runs only over the handful of names a
103+
* template actually references (its owner + `otherBlockTypes`) and lives in a
104+
* Server Component, so it never pulls the full block/tool/icon registry into
105+
* the landing client bundle. Whole-token, longest-first matching with
106+
* lookarounds mirrors the canonical matcher; idempotent on already-prefixed
107+
* names.
108+
*/
109+
function mentionifyPromptForNames(prompt: string, names: readonly string[]): string {
110+
const unique = [...new Set(names.filter((n) => n.trim().length >= 2))].sort(
111+
(a, b) => b.length - a.length
112+
)
113+
if (unique.length === 0) return prompt
114+
const regex = new RegExp(
115+
`(?<![A-Za-z0-9_@])(${unique.map(escapeRegex).join('|')})(?![A-Za-z0-9_])`,
116+
'gi'
117+
)
118+
return prompt.replace(regex, (match) => `@${match}`)
119+
}
120+
90121
/**
91122
* Generates targeted FAQs from integration metadata.
92123
* Questions mirror real search queries to drive FAQPage rich snippets.
@@ -652,6 +683,32 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
652683
...template.otherBlockTypes,
653684
]
654685

686+
const resolveDisplayName = (bt: string): string | null => {
687+
const resolvedBt = byType.get(bt)
688+
? bt
689+
: byType.get(`${bt}_v2`)
690+
? `${bt}_v2`
691+
: byType.get(`${bt}_v3`)
692+
? `${bt}_v3`
693+
: bt
694+
return byType.get(resolvedBt)?.name ?? null
695+
}
696+
697+
/**
698+
* The curated template prompt rewritten so the integrations it
699+
* references chip in the home input after signup. Computed
700+
* server-side from the template's own integration set — never the
701+
* full registry — so the visible card text stays raw while the
702+
* stored prompt opts into mention treatment.
703+
*/
704+
const storedPrompt = (template: (typeof matchingTemplates)[number]) =>
705+
mentionifyPromptForNames(
706+
template.prompt,
707+
resolveTypes(template)
708+
.map(resolveDisplayName)
709+
.filter((n): n is string => n !== null)
710+
)
711+
655712
const renderIcons = (allTypes: string[]) =>
656713
allTypes.map((bt, idx) => {
657714
const resolvedBt = byType.get(bt)
@@ -698,7 +755,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
698755
{row.map((template) => (
699756
<TemplateCardButton
700757
key={template.title}
701-
prompt={template.prompt}
758+
prompt={storedPrompt(template)}
702759
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
703760
>
704761
<div className='flex items-center gap-1.5'>
@@ -724,7 +781,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
724781
{lastTemplate && (
725782
<>
726783
<TemplateCardButton
727-
prompt={lastTemplate.prompt}
784+
prompt={storedPrompt(lastTemplate)}
728785
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
729786
>
730787
<div className='flex items-center gap-1.5'>

apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { useCallback, useState } from 'react'
44
import {
5-
Chip,
65
ChipModal,
76
ChipModalBody,
87
ChipModalError,
@@ -138,32 +137,22 @@ export function RequestIntegrationModal() {
138137
)}
139138
</ChipModalBody>
140139

141-
<ChipModalFooter>
142-
{status === 'success' ? (
143-
<Chip variant='primary' flush onClick={() => handleOpenChange(false)}>
144-
Done
145-
</Chip>
146-
) : (
147-
<>
148-
<Chip
149-
variant='filled'
150-
flush
151-
onClick={() => setOpen(false)}
152-
disabled={status === 'submitting'}
153-
>
154-
Cancel
155-
</Chip>
156-
<Chip
157-
variant='primary'
158-
flush
159-
onClick={handleSubmit}
160-
disabled={!canSubmit && status !== 'error'}
161-
>
162-
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
163-
</Chip>
164-
</>
165-
)}
166-
</ChipModalFooter>
140+
{status === 'success' ? (
141+
<ChipModalFooter
142+
onCancel={() => handleOpenChange(false)}
143+
primaryAction={{ label: 'Done', onClick: () => handleOpenChange(false) }}
144+
/>
145+
) : (
146+
<ChipModalFooter
147+
onCancel={() => setOpen(false)}
148+
cancelDisabled={status === 'submitting'}
149+
primaryAction={{
150+
label: status === 'submitting' ? 'Submitting...' : 'Submit request',
151+
onClick: handleSubmit,
152+
disabled: !canSubmit && status !== 'error',
153+
}}
154+
/>
155+
)}
167156
</ChipModal>
168157
</>
169158
)

apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import {
77
Badge,
8-
Chip,
98
ChipModal,
109
ChipModalBody,
1110
ChipModalError,
@@ -442,14 +441,15 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
442441

443442
<ChipModalError>{submitError}</ChipModalError>
444443
</ChipModalBody>
445-
<ChipModalFooter>
446-
<Chip variant='ghost' onClick={handleClose} disabled={isPending}>
447-
Cancel
448-
</Chip>
449-
<Chip variant='primary' onClick={handleConnect} disabled={isDisabled}>
450-
{isPending ? 'Connecting...' : 'Connect'}
451-
</Chip>
452-
</ChipModalFooter>
444+
<ChipModalFooter
445+
onCancel={handleClose}
446+
cancelDisabled={isPending}
447+
primaryAction={{
448+
label: isPending ? 'Connecting...' : 'Connect',
449+
onClick: handleConnect,
450+
disabled: isDisabled,
451+
}}
452+
/>
453453
</ChipModal>
454454
)
455455
}

apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/add-people-modal.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useCallback, useMemo, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import {
7-
Chip,
87
ChipModal,
98
ChipModalBody,
109
ChipModalField,
@@ -154,15 +153,14 @@ export function AddPeopleModal({ credentialId, open, onOpenChange }: AddPeopleMo
154153
disabled={isAdding}
155154
/>
156155
</ChipModalBody>
157-
<ChipModalFooter>
158-
<Chip
159-
variant='primary'
160-
onClick={handleAddPeople}
161-
disabled={emailsToAdd.length === 0 || isAdding}
162-
>
163-
{isAdding ? 'Adding...' : 'Add'}
164-
</Chip>
165-
</ChipModalFooter>
156+
<ChipModalFooter
157+
onCancel={handleClose}
158+
primaryAction={{
159+
label: isAdding ? 'Adding...' : 'Add',
160+
onClick: handleAddPeople,
161+
disabled: emailsToAdd.length === 0 || isAdding,
162+
}}
163+
/>
166164
</ChipModal>
167165
)
168166
}
Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
1+
import { ChipConfirmModal } from '@/components/emcn'
22

33
interface UnsavedChangesModalProps {
44
open: boolean
@@ -14,21 +14,14 @@ interface UnsavedChangesModalProps {
1414
*/
1515
export function UnsavedChangesModal({ open, onOpenChange, onDiscard }: UnsavedChangesModalProps) {
1616
return (
17-
<ChipModal open={open} onOpenChange={onOpenChange} srTitle='Unsaved Changes'>
18-
<ChipModalHeader showDivider={false}>Unsaved Changes</ChipModalHeader>
19-
<ChipModalBody>
20-
<p className='px-2 text-[var(--text-secondary)] text-sm'>
21-
You have unsaved changes. Are you sure you want to discard them?
22-
</p>
23-
</ChipModalBody>
24-
<ChipModalFooter>
25-
<Chip variant='filled' flush onClick={() => onOpenChange(false)}>
26-
Keep Editing
27-
</Chip>
28-
<Chip variant='destructive' flush onClick={onDiscard}>
29-
Discard Changes
30-
</Chip>
31-
</ChipModalFooter>
32-
</ChipModal>
17+
<ChipConfirmModal
18+
open={open}
19+
onOpenChange={onOpenChange}
20+
srTitle='Unsaved Changes'
21+
title='Unsaved Changes'
22+
description='You have unsaved changes. Are you sure you want to discard them?'
23+
dismissLabel='Keep editing'
24+
confirm={{ label: 'Discard Changes', onClick: onDiscard }}
25+
/>
3326
)
3427
}

apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { GitBranch } from 'lucide-react'
55
import { useParams, useRouter } from 'next/navigation'
66
import {
77
Check,
8-
Chip,
98
ChipModal,
109
ChipModalBody,
1110
ChipModalField,
@@ -279,14 +278,13 @@ export const MessageActions = memo(function MessageActions({
279278
}
280279
/>
281280
</ChipModalBody>
282-
<ChipModalFooter>
283-
<Chip variant='filled' flush onClick={() => handleModalClose(false)}>
284-
Cancel
285-
</Chip>
286-
<Chip variant='primary' flush onClick={handleSubmitFeedback}>
287-
Submit
288-
</Chip>
289-
</ChipModalFooter>
281+
<ChipModalFooter
282+
onCancel={() => handleModalClose(false)}
283+
primaryAction={{
284+
label: 'Submit',
285+
onClick: handleSubmitFeedback,
286+
}}
287+
/>
290288
</ChipModal>
291289
</>
292290
)

apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { memo } from 'react'
4-
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
4+
import { ChipConfirmModal } from '@/components/emcn'
55

66
interface DeleteConfirmModalProps {
77
open: boolean
@@ -34,29 +34,28 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
3434
: 'You can restore it from Recently Deleted in Settings.'
3535

3636
return (
37-
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
38-
<ChipModalHeader onClose={() => onOpenChange(false)} showDivider={false}>
39-
{title}
40-
</ChipModalHeader>
41-
<ChipModalBody>
42-
<p className='px-2 text-[var(--text-secondary)] text-sm'>
37+
<ChipConfirmModal
38+
open={open}
39+
onOpenChange={onOpenChange}
40+
srTitle={title}
41+
title={title}
42+
description={
43+
<>
4344
Are you sure you want to delete{' '}
4445
{fileName ? (
4546
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>
4647
) : (
4748
`${totalCount} item${totalCount === 1 ? '' : 's'}`
4849
)}
4950
? {consequence}
50-
</p>
51-
</ChipModalBody>
52-
<ChipModalFooter>
53-
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isPending}>
54-
Cancel
55-
</Chip>
56-
<Chip variant='destructive' flush onClick={onDelete} disabled={isPending}>
57-
{isPending ? 'Deleting...' : 'Delete'}
58-
</Chip>
59-
</ChipModalFooter>
60-
</ChipModal>
51+
</>
52+
}
53+
confirm={{
54+
label: 'Delete',
55+
onClick: onDelete,
56+
pending: isPending,
57+
pendingLabel: 'Deleting...',
58+
}}
59+
/>
6160
)
6261
})

0 commit comments

Comments
 (0)