Skip to content

refactor(emcn): make ChipModal footer/header props-driven and migrate all consumers#4905

Open
emir-karabeg wants to merge 3 commits into
stagingfrom
refactor/chip-modal-migration
Open

refactor(emcn): make ChipModal footer/header props-driven and migrate all consumers#4905
emir-karabeg wants to merge 3 commits into
stagingfrom
refactor/chip-modal-migration

Conversation

@emir-karabeg

@emir-karabeg emir-karabeg commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Make ChipModalFooter props-driven: onCancel, primaryAction, and optional secondaryAction (each a ChipModalFooterAction with label/onClick/variant/disabled) replace hand-composed footer markup, so the modal owns its chrome as the single source of truth
  • Make ChipModalHeader.onClose required and always render the close button + inset divider, dropping the optional onClose/showDivider branches for one consistent header
  • Export ChipModalFooterAction and ChipModalFooterProps from the emcn barrel
  • Migrate all ~70 modal consumers (settings, knowledge, tables, integrations, deploy, workflow sidebar, EE) to the new API — net 1388 insertions / 1707 deletions of consumer boilerplate

Type of Change

  • Refactor (no functional change)

Testing

Tested manually. Opened representative modals (create/edit/delete confirmations, invite, deploy, API key, knowledge base) and verified footer actions, cancel, close, destructive-variant flows, and secondary-action layout all render correctly. bun run lint:check, tsc --noEmit, and bun run check:api-validation clean on touched files.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 8, 2026 9:27pm

Request Review

@cursor

cursor Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Wide UI-only modal refactor with many touchpoints; mistaken cancel/primary wiring or curated vs free-form mention rules could change delete flows or incorrectly chip prose in the home input.

Overview
ChipModal footers across the app no longer compose manual Chip buttons. They now use a props-driven ChipModalFooter (onCancel, primaryAction, optional secondaryAction with variant/disabled). Headers require onClose and always show the close control and divider, dropping showDivider and optional close branches.

Curated prompt handoff to home chat is tightened so integration names chip only when intended. Landing integration template cards rewrite names to @ mentions server-side via mentionifyPromptForNames (template-specific names, no full block registry in the landing bundle). Workspace curated CTAs use storeCuratedPrompt / mentionifyIntegrations at store or populatePrompt time; free-form landing prose seeded through defaultValue is not bare-name mentionified. Suggested actions stop pre-mentionifying prompts at list build time.

Minor follow-ons: mic control gets Tooltip + aria-label; global add-task command id removed; a few modals refactor inline delete/save handlers into named functions for the new footer API.

Reviewed by Cursor Bugbot for commit 94d36b2. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94d36b2. Configure here.

disabled: deleteSchedule.isPending,
variant: 'destructive',
}}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header close skips state reset

Low Severity

Required ChipModalHeader close handlers only hide the delete dialog, while footer Cancel also clears the targeted API key or scheduled task. Dismissing via the new header control leaves that selection in state, unlike Cancel and unlike other migrated delete modals that reset both in onClose.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 94d36b2. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes ChipModalFooter and ChipModalHeader fully declarative — replacing hand-composed JSX children with a typed ChipModalFooterAction prop set — and migrates all ~70 modal consumers to the new API, netting ~320 fewer lines. The addressed cancelDisabled thread from the previous review round has been correctly resolved across all 45 previously-guarded footers.

  • Core component (chip-modal.tsx): ChipModalHeader.onClose is now required (always renders X + divider); ChipModalFooter is now a plain function that renders Cancel + primary + optional secondary Chips from props, with a new cancelDisabled field to guard in-flight mutations.
  • Consumer migrations: 45 footers now correctly thread cancelDisabled with each modal's original pending boolean; mutation-guarded footers like byok, row-modal, remove-member-dialog, and transfer-ownership-dialog are all properly restored.
  • Bundled extras: sidebar.tsx removes the "New chat" button and Mod+Shift+K shortcut; integration-matcher.ts adds mentionifyIntegrations/storeCuratedPrompt; mic-button.tsx upgrades to a Tooltip — these fall outside the stated refactor scope.

Confidence Score: 5/5

Safe to merge — the refactor is mechanically correct across all 70 consumers, mutation guards are fully restored, and the remaining findings are visual regressions scoped to two UI elements.

The refactor's core correctness is high: cancelDisabled is wired properly for every footer that previously disabled Cancel during a mutation, handleFormSubmit's optional-e signature change is correct for both onClick and onSubmit call sites, and the access-control unsaved-changes dialog rearrangement preserves all three outcomes (discard / go-back / save). The two open items are purely visual: an accidentally dropped className on the inbox trash Chip that removed its muted/red-hover styling, and the notifications 'Add' button losing its + icon because ChipModalFooterAction has no leftIcon field. Neither touches data flow or user-facing logic.

inbox-settings-tab.tsx (accidental className removal from sender-list trash Chip) and notifications.tsx (lost leftIcon on the Add button) are the two files worth a quick visual check before merge.

Important Files Changed

Filename Overview
apps/sim/components/emcn/components/chip-modal/chip-modal.tsx Core ChipModal refactor: ChipModalFooter becomes declarative (onCancel, primaryAction, secondaryAction, cancelDisabled), ChipModalHeader.onClose made required and always renders close button + divider. Correctly adds cancelDisabled to guard in-flight mutations.
apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-settings-tab/inbox-settings-tab.tsx Footer migrated correctly, but an unrelated className ('text-[var(--text-muted)] hover-hover:text-[var(--text-error)]') was accidentally removed from the sender-list trash Chip, stripping the destructive-action colour cue.
apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx Footer migrated to declarative API; the 'Add' primary action lost its leftIcon={Plus} because ChipModalFooterAction has no leftIcon field.
apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx Footer migration correct across three modals; however the 'New API Key' informational dialog gains a redundant Cancel button that is wired identically to 'Done'.
apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx Both modals (save key, delete key) correctly threaded with cancelDisabled={upsertKey.isPending}, matching original behaviour.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx handleFormSubmit signature changed to optional e?: React.FormEvent to support both onClick (no args) and onSubmit (FormEvent) call sites — correct. cancelDisabled={isSubmitting} properly guarded on both delete and edit/add modals.
apps/sim/ee/access-control/components/access-control.tsx Unsaved-changes dialog redesigned with secondaryAction=Discard/onCancel=go-back/primaryAction=Save layout; handleCloseConfigModal, handleDiscardConfig, handleSaveConfigFromUnsaved extracted cleanly.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx Removes 'New chat' sidebar button, its keyboard shortcut (Mod+Shift+K), and the useCreateTask / useMothershipDraftsStore hooks — appears intentional alongside the commands-utils removal of the add-task command definition.
apps/sim/blocks/integration-matcher.ts Adds mentionifyIntegrations and storeCuratedPrompt — centralises the 'curated prompts get @-mention-chipped' contract. Idempotency check (offset-1 == '@') is correct for already-prefixed names.

Sequence Diagram

sequenceDiagram
    participant Consumer as Modal Consumer
    participant Footer as ChipModalFooter
    participant Cancel as Cancel Chip
    participant Primary as Primary Chip
    participant Secondary as Secondary Chip (opt.)

    Consumer->>Footer: onCancel, primaryAction, secondaryAction?, cancelDisabled?
    Footer->>Secondary: variant ?? 'filled', disabled?, onClick (left-docked)
    Footer->>Cancel: "variant='filled', disabled=cancelDisabled"
    Footer->>Primary: variant ?? 'primary', disabled?, onClick

    Note over Cancel: Always rendered, always labelled 'Cancel'
    Note over Secondary: Left-docked (justify-between) only when provided
    Note over Primary: Right-anchored alongside Cancel
Loading

Reviews (2): Last reviewed commit: "fix(emcn): restore Cancel disable guard ..." | Re-trigger Greptile

Comment thread apps/sim/components/emcn/components/chip-modal/chip-modal.tsx
Comment on lines 1215 to +1230
onOpenChange={setShowDiscardAlert}
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={false}>Unsaved Changes</ChipModalHeader>
<ChipModalHeader onClose={() => setShowDiscardAlert(false)}>
Unsaved Changes
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowDiscardAlert(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={handleConfirmDiscard}>
Discard Changes
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setShowDiscardAlert(false)}
primaryAction={{
label: 'Discard Changes',
onClick: handleConfirmDiscard,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "Keep Editing" replaced by ambiguous "Cancel" in unsaved-changes alert

The old Unsaved Changes dialog used the label "Keep Editing" for the dismiss action; the new hardcoded label is "Cancel". In an unsaved-changes confirmation context, "Cancel" is ambiguous: it could mean "cancel the discard decision" (stay and keep editing) or "cancel the save" (leave without saving). "Keep Editing" was unambiguous. Because the new ChipModalFooterProps explicitly forbids relabeling the dismiss button, this cannot be corrected at the call site — the API design is the constraint.

The same ambiguity appears in the "Unsaved Changes" dialog in ee/access-control/components/access-control.tsx.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +782 to +818
function ChipModalFooter({ onCancel, primaryAction, secondaryAction }: ChipModalFooterProps) {
return (
<div className='flex flex-col'>
<ChipModalSeparator />
<div
ref={ref}
className={cn(
'flex items-center gap-2 bg-[var(--surface-3)] px-4 pt-2 pb-2',
leading ? 'justify-between' : 'justify-end',
className
secondaryAction ? 'justify-between' : 'justify-end'
)}
{...props}
>
{leading && <div>{leading}</div>}
<div className='flex gap-2'>{children}</div>
{secondaryAction ? (
<Chip
variant={secondaryAction.variant ?? 'filled'}
flush
onClick={secondaryAction.onClick}
disabled={secondaryAction.disabled}
>
{secondaryAction.label}
</Chip>
) : null}
<div className='flex gap-2'>
<Chip variant='filled' flush onClick={onCancel}>
Cancel
</Chip>
<Chip
variant={primaryAction.variant ?? 'primary'}
flush
onClick={primaryAction.onClick}
disabled={primaryAction.disabled}
>
{primaryAction.label}
</Chip>
</div>
</div>
</div>
)
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ChipModalFooter was converted from React.forwardRef (which returns React.ForwardRefExoticComponent, a type that includes displayName) to a plain function declaration. Setting .displayName on a plain function-valued variable is valid JavaScript and TypeScript does allow property augmentation via declaration merging (TS 3.1+), so this won't be a hard compile error — but the component can no longer receive a forwarded ref. If any future consumer needs to measure or focus the footer, this will silently fail. Keeping the component as a forwardRef or at minimum explicitly typing it as React.FC<ChipModalFooterProps> would preserve the ref channel and make the displayName intent more conventional.

Suggested change
function ChipModalFooter({ onCancel, primaryAction, secondaryAction }: ChipModalFooterProps) {
return (
<div className='flex flex-col'>
<ChipModalSeparator />
<div
ref={ref}
className={cn(
'flex items-center gap-2 bg-[var(--surface-3)] px-4 pt-2 pb-2',
leading ? 'justify-between' : 'justify-end',
className
secondaryAction ? 'justify-between' : 'justify-end'
)}
{...props}
>
{leading && <div>{leading}</div>}
<div className='flex gap-2'>{children}</div>
{secondaryAction ? (
<Chip
variant={secondaryAction.variant ?? 'filled'}
flush
onClick={secondaryAction.onClick}
disabled={secondaryAction.disabled}
>
{secondaryAction.label}
</Chip>
) : null}
<div className='flex gap-2'>
<Chip variant='filled' flush onClick={onCancel}>
Cancel
</Chip>
<Chip
variant={primaryAction.variant ?? 'primary'}
flush
onClick={primaryAction.onClick}
disabled={primaryAction.disabled}
>
{primaryAction.label}
</Chip>
</div>
</div>
</div>
)
)
}
function ChipModalFooter({ onCancel, cancelDisabled, primaryAction, secondaryAction }: ChipModalFooterProps) {
return (
<div className='flex flex-col'>
<ChipModalSeparator />
<div
className={cn(
'flex items-center gap-2 bg-[var(--surface-3)] px-4 pt-2 pb-2',
secondaryAction ? 'justify-between' : 'justify-end'
)}
>
{secondaryAction ? (
<Chip
variant={secondaryAction.variant ?? 'filled'}
flush
onClick={secondaryAction.onClick}
disabled={secondaryAction.disabled}
>
{secondaryAction.label}
</Chip>
) : null}
<div className='flex gap-2'>
<Chip variant='filled' flush onClick={onCancel} disabled={cancelDisabled}>
Cancel
</Chip>
<Chip
variant={primaryAction.variant ?? 'primary'}
flush
onClick={primaryAction.onClick}
disabled={primaryAction.disabled}
>
{primaryAction.label}
</Chip>
</div>
</div>
</div>
)
}

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.
@emir-karabeg

Copy link
Copy Markdown
Collaborator Author

Addressed the Cancel-during-mutation regression in 6c7cbdb68:

  • P1 (Cancel cannot be disabled during in-flight mutations): Added cancelDisabled?: boolean to ChipModalFooterProps and threaded it into the Cancel <Chip disabled={cancelDisabled}>. Restored the pre-migration in-flight guard on all 45 footers that previously had one (across 42 files) — using each modal's exact original boolean (isDeleting, isSubmitting, upsertKey.isPending, updateMutation.isPending || isGenerating, etc.). Multi-modal files are mapped correctly (e.g. byok's save modal is guarded, its delete modal isn't; nested "Keep Editing" unsaved-changes dialogs are intentionally left unguarded).
  • The onCancel TSDoc now permits disabling Cancel while keeping it structural — relabel/remove remain forbidden, so the consolidation intent is preserved.

Not changed (by design):

  • "Keep Editing" → "Cancel" relabel: intentional — ChipModalFooter deliberately forbids relabeling Cancel for cross-modal consistency.
  • forwardRef removed from ChipModalFooter: it's a leaf with no ref consumer; .displayName on a function component is valid.

Verified: biome check clean on all 43 files; tsc --noEmit reports no new errors.

@greptile review

…l-migration

# Conflicts:
#	apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants