Skip to content

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

Open
emir-karabeg wants to merge 1 commit 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 1 commit into
stagingfrom
refactor/chip-modal-migration

Conversation

@emir-karabeg
Copy link
Copy Markdown
Collaborator

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

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
Copy link
Copy Markdown

vercel Bot commented Jun 8, 2026

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 6:06pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Jun 8, 2026

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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 8, 2026

Greptile Summary

This PR standardises all ~70 ChipModal consumers in the codebase: ChipModalFooter becomes fully props-driven (onCancel, primaryAction, secondaryAction) and ChipModalHeader.onClose is promoted to required, eliminating ad-hoc footer markup and inconsistent close-button rendering.

  • The new ChipModalFooterAction interface neatly constrains variant choice to the three footer-appropriate options, and the migration is consistent across settings, knowledge, tables, deploy, EE, and sidebar modals.
  • ChipModalFooterProps has no cancelDisabled field, removing the ability — present in ~9 migrated modals — to disable the Cancel button while an async operation is in flight. For destructive flows (delete workflow, remove member, transfer ownership) this allows users to dismiss the confirmation mid-mutation and silently believe the operation was aborted.
  • The hardcoded "Cancel" label replaces the previously customisable dismiss label; in "Unsaved Changes" dialogs that previously read "Keep Editing" the new label is less descriptive.

Confidence Score: 3/5

The migration is structurally sound but the new ChipModalFooter API drops the ability to disable the Cancel button, which several destructive-action modals depended on to prevent mid-mutation dismissal.

The new props-driven API is a genuine improvement in consistency and reduces boilerplate across 70+ call sites. However, removing the Cancel disabled guard from destructive-operation modals (delete workflow, remove member, transfer ownership, delete API key, etc.) means a user who clicks Cancel while "Deleting..." is shown will close the modal and reasonably conclude the operation was aborted — while the mutation continues to completion in the background. This is a silent data-loss-adjacent regression that affects multiple critical flows. A cancelDisabled field on ChipModalFooterProps would close the gap without breaking the new API shape.

apps/sim/components/emcn/components/chip-modal/chip-modal.tsx (ChipModalFooterProps is missing cancelDisabled), apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx, apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/remove-member-dialog/remove-member-dialog.tsx, apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx

Important Files Changed

Filename Overview
apps/sim/components/emcn/components/chip-modal/chip-modal.tsx Core API change: ChipModalFooter becomes props-driven with fixed Cancel/primary/secondary slots; ChipModalHeader.onClose is now required. The new ChipModalFooterProps lacks a cancelDisabled field, removing the ability for consumers (byok, delete-modal, row-modal, etc.) to disable Cancel during in-flight mutations.
apps/sim/components/emcn/components/index.ts Adds ChipModalFooterAction and ChipModalFooterProps to the barrel export; straightforward and correct.
apps/sim/ee/access-control/components/access-control.tsx Migrated to new ChipModalFooter API; inline handlers extracted to named callbacks (handleCloseConfigModal, handleDiscardConfig, handleSaveConfigFromUnsaved). The Unsaved Changes dialog gains a structural Cancel button but loses the descriptive "Discard Changes"-only layout from the old code.
apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx Cancel was previously disabled={upsertKey.isPending}; the new footer has no mechanism to disable Cancel, so users can now dismiss the modal while a key save is in flight and the error state has nowhere to render.
apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/remove-member-dialog/remove-member-dialog.tsx Cancel disabled={isSubmitting} removed; users can now dismiss while a member-remove mutation is in flight, potentially leading them to believe the removal was cancelled when it was not.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx Cancel was disabled={isDeleting}; now always enabled. A user can dismiss the delete-confirmation modal mid-deletion and incorrectly assume the workflow was not deleted.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx handleFormSubmit signature correctly made optional-param to accommodate calling it without an event from the footer onClick. Both form-onSubmit (with event) and footer-onClick (no event) paths are handled correctly.
apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx Complex conditional secondaryAction (toggle JSON/form mode in add mode; test connection in edit+form mode; none in edit+json mode) correctly migrated; inline handler extracted to handleToggleJsonMode.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx Wizard-style footer with conditional secondaryAction (Delete when editing, Back on code step) correctly migrated. "Unsaved Changes" dialog loses the descriptive "Keep Editing" cancel label, replaced by hardcoded "Cancel".
apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx Correctly switches from type='submit' chip to explicit handleSubmit(onSubmit) onClick; React Hook Form handles form validation independently so behaviour is preserved.

Sequence Diagram

sequenceDiagram
    participant Consumer as Consumer Modal
    participant Footer as ChipModalFooter
    participant CancelChip as Cancel Chip
    participant PrimaryChip as Primary Chip
    participant SecondaryChip as Secondary Chip (optional)

    Consumer->>Footer: "props { onCancel, primaryAction, secondaryAction? }"
    Footer->>SecondaryChip: render if secondaryAction (far-left, variant default 'filled')
    Footer->>CancelChip: "render always (label='Cancel', no disabled support)"
    Footer->>PrimaryChip: render always (variant default 'primary', disabled? supported)

    Note over CancelChip: Always enabled — even during in-flight mutations

    CancelChip-->>Consumer: onClick → onCancel()
    PrimaryChip-->>Consumer: onClick → primaryAction.onClick()
    SecondaryChip-->>Consumer: onClick → secondaryAction.onClick()
Loading

Reviews (1): Last reviewed commit: "refactor(emcn): make ChipModal footer/he..." | Re-trigger Greptile

Comment on lines 772 to +783
*/
leading?: React.ReactNode
secondaryAction?: ChipModalFooterAction
}

/**
* Footer row. Renders the leading inset separator and a tinted action bar.
* Pass `leading` to left-dock a secondary action (e.g. Delete in edit mode);
* primary actions always go in `children` and are right-aligned.
* Footer row with a fixed, declarative shape: an optional far-left
* `secondaryAction`, then the always-present Cancel and the right-anchored
* `primaryAction`. Buttons are described via {@link ChipModalFooterAction} and
* rendered as {@link Chip}s, so no footer can drift from the canonical layout.
*/
const ChipModalFooter = React.forwardRef<HTMLDivElement, ChipModalFooterProps>(
({ className, leading, children, ...props }, ref) => (
function ChipModalFooter({ onCancel, primaryAction, secondaryAction }: ChipModalFooterProps) {
return (
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 Cancel button cannot be disabled during in-flight mutations

ChipModalFooterAction exposes a disabled field for the primary and secondary buttons, but there is no equivalent for the always-rendered Cancel button. Across ~9 migrated modals the old Cancel Chip was explicitly disabled while an async operation was in flight: byok.tsx (disabled={upsertKey.isPending}), row-modal.tsx (disabled={isSubmitting}), delete-modal.tsx (disabled={isDeleting}), remove-member-dialog.tsx (disabled={isSubmitting}), transfer-ownership-dialog.tsx (disabled={isSubmitting}), and several more.

The concrete failure: a user clicks Delete (or Remove/Transfer) on a destructive confirmation, sees "Deleting...", then clicks the now-enabled Cancel button. The modal closes and the user assumes the operation was cancelled — but the mutation continues in the background and completes. Data is deleted/removed without the user realising it.

Adding cancelDisabled?: boolean to ChipModalFooterProps and threading it into the Cancel <Chip disabled={cancelDisabled}> would restore the previous safety guard across all consumers.

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>
)
}

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.

1 participant