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>
This commit is contained in:
Emir Karabeg
2026-06-08 18:48:56 -07:00
committed by GitHub
co-authored by Claude Opus 4.8 waleed
parent 7434df9457
commit 4f00baf6b4
79 changed files with 2033 additions and 2261 deletions
+9 -19
View File
@@ -7,7 +7,6 @@ import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -560,24 +559,15 @@ export default function LoginPage({
{resetStatus.type === 'error' ? resetStatus.message : null}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setForgotPasswordOpen(false)}
disabled={isSubmittingReset}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleForgotPassword}
disabled={!forgotPasswordEmail || isSubmittingReset}
>
{isSubmittingReset ? 'Sending…' : 'Send Reset Link'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setForgotPasswordOpen(false)}
cancelDisabled={isSubmittingReset}
primaryAction={{
label: isSubmittingReset ? 'Sending…' : 'Send Reset Link',
onClick: handleForgotPassword,
disabled: !forgotPasswordEmail || isSubmittingReset,
}}
/>
</ChipModal>
</>
)
@@ -6,6 +6,12 @@ import { cn } from '@/lib/core/utils/cn'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
interface TemplateCardButtonProps {
/**
* Curated template prompt, already rewritten to `@`-mention form by the
* page's server-side `mentionifyPromptForNames` (registry-free, so the
* landing client bundle never pulls the full block registry). Stored verbatim
* for the home input to consume after signup.
*/
prompt: string
className?: string
children: React.ReactNode
@@ -87,6 +87,37 @@ function sentenceWithTerminalPunctuation(value: string): string {
return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.`
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/**
* Server-side rewrite of bare integration names in a curated template prompt
* to `@`-mention form (`Slack` → `@Slack`) so the prompt chips with brand
* icons once it is populated into the Mothership home input after signup —
* the home auto-mention pipeline only chips token-starting `@` mentions, so
* curated prompts must opt in.
*
* Unlike the workspace surface, which calls `mentionifyIntegrations` from
* `@/blocks/integration-matcher`, this runs only over the handful of names a
* template actually references (its owner + `otherBlockTypes`) and lives in a
* Server Component, so it never pulls the full block/tool/icon registry into
* the landing client bundle. Whole-token, longest-first matching with
* lookarounds mirrors the canonical matcher; idempotent on already-prefixed
* names.
*/
function mentionifyPromptForNames(prompt: string, names: readonly string[]): string {
const unique = [...new Set(names.filter((n) => n.trim().length >= 2))].sort(
(a, b) => b.length - a.length
)
if (unique.length === 0) return prompt
const regex = new RegExp(
`(?<![A-Za-z0-9_@])(${unique.map(escapeRegex).join('|')})(?![A-Za-z0-9_])`,
'gi'
)
return prompt.replace(regex, (match) => `@${match}`)
}
/**
* Generates targeted FAQs from integration metadata.
* Questions mirror real search queries to drive FAQPage rich snippets.
@@ -652,6 +683,32 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
...template.otherBlockTypes,
]
const resolveDisplayName = (bt: string): string | null => {
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: byType.get(`${bt}_v3`)
? `${bt}_v3`
: bt
return byType.get(resolvedBt)?.name ?? null
}
/**
* The curated template prompt rewritten so the integrations it
* references chip in the home input after signup. Computed
* server-side from the template's own integration set — never the
* full registry — so the visible card text stays raw while the
* stored prompt opts into mention treatment.
*/
const storedPrompt = (template: (typeof matchingTemplates)[number]) =>
mentionifyPromptForNames(
template.prompt,
resolveTypes(template)
.map(resolveDisplayName)
.filter((n): n is string => n !== null)
)
const renderIcons = (allTypes: string[]) =>
allTypes.map((bt, idx) => {
const resolvedBt = byType.get(bt)
@@ -698,7 +755,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{row.map((template) => (
<TemplateCardButton
key={template.title}
prompt={template.prompt}
prompt={storedPrompt(template)}
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'
>
<div className='flex items-center gap-1.5'>
@@ -724,7 +781,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{lastTemplate && (
<>
<TemplateCardButton
prompt={lastTemplate.prompt}
prompt={storedPrompt(lastTemplate)}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<div className='flex items-center gap-1.5'>
@@ -2,7 +2,6 @@
import { useCallback, useState } from 'react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -138,32 +137,22 @@ export function RequestIntegrationModal() {
)}
</ChipModalBody>
<ChipModalFooter>
{status === 'success' ? (
<Chip variant='primary' flush onClick={() => handleOpenChange(false)}>
Done
</Chip>
) : (
<>
<Chip
variant='filled'
flush
onClick={() => setOpen(false)}
disabled={status === 'submitting'}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={!canSubmit && status !== 'error'}
>
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
</Chip>
</>
)}
</ChipModalFooter>
{status === 'success' ? (
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
primaryAction={{ label: 'Done', onClick: () => handleOpenChange(false) }}
/>
) : (
<ChipModalFooter
onCancel={() => setOpen(false)}
cancelDisabled={status === 'submitting'}
primaryAction={{
label: status === 'submitting' ? 'Submitting...' : 'Submit request',
onClick: handleSubmit,
disabled: !canSubmit && status !== 'error',
}}
/>
)}
</ChipModal>
</>
)
@@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Badge,
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -442,14 +441,15 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
<ChipModalError>{submitError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='ghost' onClick={handleClose} disabled={isPending}>
Cancel
</Chip>
<Chip variant='primary' onClick={handleConnect} disabled={isDisabled}>
{isPending ? 'Connecting...' : 'Connect'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleClose}
cancelDisabled={isPending}
primaryAction={{
label: isPending ? 'Connecting...' : 'Connect',
onClick: handleConnect,
disabled: isDisabled,
}}
/>
</ChipModal>
)
}
@@ -4,7 +4,6 @@ import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
@@ -154,15 +153,14 @@ export function AddPeopleModal({ credentialId, open, onOpenChange }: AddPeopleMo
disabled={isAdding}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='primary'
onClick={handleAddPeople}
disabled={emailsToAdd.length === 0 || isAdding}
>
{isAdding ? 'Adding...' : 'Add'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleClose}
primaryAction={{
label: isAdding ? 'Adding...' : 'Add',
onClick: handleAddPeople,
disabled: emailsToAdd.length === 0 || isAdding,
}}
/>
</ChipModal>
)
}
@@ -1,4 +1,4 @@
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'
interface UnsavedChangesModalProps {
open: boolean
@@ -14,21 +14,14 @@ interface UnsavedChangesModalProps {
*/
export function UnsavedChangesModal({ open, onOpenChange, onDiscard }: UnsavedChangesModalProps) {
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle='Unsaved Changes'>
<ChipModalHeader showDivider={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={() => onOpenChange(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={onDiscard}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
<ChipConfirmModal
open={open}
onOpenChange={onOpenChange}
srTitle='Unsaved Changes'
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{ label: 'Discard Changes', onClick: onDiscard }}
/>
)
}
@@ -5,7 +5,6 @@ import { GitBranch } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import {
Check,
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
@@ -279,14 +278,13 @@ export const MessageActions = memo(function MessageActions({
}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => handleModalClose(false)}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleSubmitFeedback}>
Submit
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleModalClose(false)}
primaryAction={{
label: 'Submit',
onClick: handleSubmitFeedback,
}}
/>
</ChipModal>
</>
)
@@ -1,7 +1,7 @@
'use client'
import { memo } from 'react'
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'
interface DeleteConfirmModalProps {
open: boolean
@@ -34,12 +34,13 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
: 'You can restore it from Recently Deleted in Settings.'
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
<ChipModalHeader onClose={() => onOpenChange(false)} showDivider={false}>
{title}
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={open}
onOpenChange={onOpenChange}
srTitle={title}
title={title}
description={
<>
Are you sure you want to delete{' '}
{fileName ? (
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>
@@ -47,16 +48,14 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
`${totalCount} item${totalCount === 1 ? '' : 's'}`
)}
? {consequence}
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={onDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: onDelete,
pending: isPending,
pendingLabel: 'Deleting...',
}}
/>
)
})
@@ -7,12 +7,8 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import {
Button,
Chip,
ChipCombobox,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
Columns2,
type ComboboxOption,
Eye,
@@ -1873,26 +1869,15 @@ export function Files() {
saveRef={saveRef}
/>
<ChipModal
<ChipConfirmModal
open={showUnsavedChangesAlert}
onOpenChange={setShowUnsavedChangesAlert}
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={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={() => setShowUnsavedChangesAlert(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={handleDiscardChanges}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{ label: 'Discard Changes', onClick: handleDiscardChanges }}
/>
</div>
<DeleteConfirmModal
@@ -24,27 +24,12 @@ import {
import { captureEvent } from '@/lib/posthog/client'
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
import { getBareIconStyle } from '@/blocks/icon-color'
import { getIntegrationMatcher } from '@/blocks/integration-matcher'
import type { ModuleTag } from '@/blocks/types'
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import { useOAuthConnections } from '@/hooks/queries/oauth/oauth-connections'
import { useTablesList } from '@/hooks/queries/tables'
/**
* Rewrites bare integration names in `text` to `@`-mention form (`Kalshi` →
* `@Kalshi`) so they chip when the prompt is populated into the input — the
* auto-mention pipeline deliberately ignores un-prefixed names, so curated
* prompts opt in here. Idempotent: names already prefixed are left untouched.
*/
function mentionifyIntegrations(text: string): string {
const { regex } = getIntegrationMatcher()
if (!regex || !text) return text
return text.replace(regex, (match: string, _name: string, offset: number) =>
offset > 0 && text[offset - 1] === '@' ? match : `@${match}`
)
}
type Icon = ComponentType<{ className?: string; style?: CSSProperties }>
type Action =
@@ -121,7 +106,7 @@ const CANDIDATES: readonly Candidate[] = (() => {
id: `${blockType}-${i}`,
blockType,
label: template.title,
prompt: mentionifyIntegrations(template.prompt),
prompt: template.prompt,
icon: template.icon as Icon,
modules: template.modules,
featured: template.featured ?? false,
@@ -1,7 +1,7 @@
'use client'
import React from 'react'
import { Mic } from '@/components/emcn'
import { Mic, Tooltip } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
interface MicButtonProps {
@@ -11,18 +11,23 @@ interface MicButtonProps {
export const MicButton = React.memo(function MicButton({ isListening, onToggle }: MicButtonProps) {
return (
<button
type='button'
onClick={onToggle}
className={cn(
'flex h-[28px] w-[28px] items-center justify-center rounded-full transition-colors',
isListening
? 'bg-red-500 text-white hover:bg-red-600'
: 'text-[var(--text-icon)] hover:bg-[#F7F7F7] dark:hover:bg-[#303030]'
)}
title={isListening ? 'Stop listening' : 'Voice input'}
>
<Mic className='h-[16px] w-[16px]' />
</button>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
onClick={onToggle}
aria-label={isListening ? 'Stop listening' : 'Voice input'}
className={cn(
'flex h-[28px] w-[28px] items-center justify-center rounded-full transition-colors',
isListening
? 'bg-red-500 text-white hover:bg-red-600'
: 'text-[var(--text-icon)] hover:bg-[#F7F7F7] dark:hover:bg-[#303030]'
)}
>
<Mic className='h-[16px] w-[16px]' />
</button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>{isListening ? 'Stop listening' : 'Voice input'}</Tooltip.Content>
</Tooltip.Root>
)
})
@@ -63,6 +63,7 @@ import {
SKILL_CHIP_TRIGGER,
stripMentionTrigger,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
import { mentionifyIntegrations } from '@/blocks/integration-matcher'
import { type SkillDefinition, useSkills } from '@/hooks/queries/skills'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useSpeechToText } from '@/hooks/use-speech-to-text'
@@ -138,9 +139,11 @@ interface UserInputProps {
export interface UserInputHandle {
loadQueuedMessage: (msg: QueuedMessage) => void
/** Populates the textarea with `text` (running it through auto-mention
* chipification), focuses the input, and places the caret at the end.
* Does NOT submit. Safe to call with the same text twice in a row. */
/** Populates the textarea with a CURATED prompt (suggested action, template,
* etc. — never free-form user prose), running it through `mentionifyIntegrations`
* (bare `Slack` → `@Slack`) and then auto-mention chipification so integration
* names chip with brand icons. Focuses the input and places the caret at the
* end. Does NOT submit. Safe to call with the same text twice in a row. */
populatePrompt: (text: string) => void
}
@@ -376,6 +379,12 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
* mounted. Mirrors the previously inline render-phase derivation but
* now runs the prompt through `applyToText` so integration `@`-mentions
* get chipified consistently with paste / draft restore flows.
*
* Deliberately does NOT run `mentionifyIntegrations` here: `defaultValue` is
* seeded from `LandingPromptStorage`, whose producers include the free-form
* landing prompt panel as well as curated CTAs. Curated producers opt their
* bare names in at the store seam (`storeCuratedPrompt`), so prose seeded here
* is never bare-chipped (the scunthorpe constraint).
*/
useEffect(() => {
if (defaultValue === prevDefaultValueRef.current) return
@@ -464,7 +473,12 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
focusTextareaAtEnd()
},
populatePrompt: (text: string) => {
setValue(applyAutoMentions(text))
// `text` is a curated prompt, so opt its bare integration names into
// `@`-mention form before chipification (the auto-mention pipeline only
// chips already-`@`-prefixed names). Curated prompts arriving via the
// `defaultValue` seed are mentionified at their producer instead, since
// that path is also reused for free-form landing prose.
setValue(applyAutoMentions(mentionifyIntegrations(text)))
focusTextareaAtEnd()
},
}),
@@ -5,7 +5,6 @@ import { ArrowLeft, ArrowRight, Plus } from 'lucide-react'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { Chip, ChipDropdown, ChipLink } from '@/components/emcn'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { cn } from '@/lib/core/utils/cn'
import {
blockTypeToIconMap,
@@ -22,6 +21,7 @@ import {
CONNECT_MODE,
CONNECT_QUERY_PARAM,
} from '@/app/workspace/[workspaceId]/integrations/connect-route'
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
import {
getSuggestedSkillsForBlock,
getTemplatesForBlock,
@@ -117,7 +117,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
}
const handleAddInChat = () => {
LandingPromptStorage.store(`Explore ${integration.name}. What can I do?`)
storeCuratedPrompt(`Explore ${integration.name}. What can I do?`)
router.push(`/workspace/${workspaceId}/home`)
}
@@ -247,7 +247,7 @@ function TemplatesSection({ integration, templates, workspaceId }: TemplatesSect
const router = useRouter()
const handleSelect = (prompt: string) => {
LandingPromptStorage.store(prompt)
storeCuratedPrompt(prompt)
router.push(`/workspace/${workspaceId}/home`)
}
@@ -4,7 +4,6 @@ import { type ComponentType, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -276,14 +275,14 @@ function GoogleServiceAccountModal({
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='ghost' onClick={() => onOpenChange(false)}>
Cancel
</Chip>
<Chip variant='primary' onClick={handleSubmit} disabled={isDisabled}>
{isPending ? 'Adding...' : 'Add service account'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
primaryAction={{
label: isPending ? 'Adding...' : 'Add service account',
onClick: handleSubmit,
disabled: isDisabled,
}}
/>
</ChipModal>
)
}
@@ -409,14 +408,14 @@ function AtlassianServiceAccountModal({
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='ghost' onClick={() => onOpenChange(false)}>
Cancel
</Chip>
<Chip variant='primary' onClick={handleSubmit} disabled={isDisabled}>
{isPending ? 'Adding...' : 'Add service account'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
primaryAction={{
label: isPending ? 'Adding...' : 'Add service account',
onClick: handleSubmit,
disabled: isDisabled,
}}
/>
</ChipModal>
)
}
@@ -3,18 +3,23 @@
import { ArrowRight } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Chip } from '@/components/emcn'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { IntegrationsShowcase } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
interface ShowcaseWithExploreProps {
/** Prompt stored for the home page chat to consume after navigation. */
/**
* Prompt stored for the home page chat to consume after navigation. Bare
* integration names are rewritten to `@`-mention form on store so they chip
* in the chat input (mention treatment is opt-in there).
*/
prompt: string
}
/**
* Renders the integrations showcase with an "Explore in chat" CTA pinned into
* the showcase's bottom-right mask notch. Clicking the CTA seeds the home page
* chat with `prompt` and navigates to the workspace home.
* chat with `prompt` (via {@link storeCuratedPrompt} so integration names chip)
* and navigates to the workspace home.
*/
export function ShowcaseWithExplore({ prompt }: ShowcaseWithExploreProps) {
const params = useParams()
@@ -28,7 +33,7 @@ export function ShowcaseWithExplore({ prompt }: ShowcaseWithExploreProps) {
variant='filled'
rightIcon={ArrowRight}
onClick={() => {
LandingPromptStorage.store(prompt)
storeCuratedPrompt(prompt)
router.push(`/workspace/${workspaceId}/home`)
}}
className='absolute right-0 bottom-0 mx-0'
@@ -6,12 +6,9 @@ import { getErrorMessage } from '@sim/utils/errors'
import { useRouter } from 'next/navigation'
import {
Chip,
ChipConfirmModal,
ChipInput,
ChipLink,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipTextarea,
Send,
toast,
@@ -292,35 +289,25 @@ export function ConnectedCredentialDetail({
<CredentialMembersSection credentialId={credential.id} isAdmin={isAdmin} />
</CredentialDetailLayout>
<ChipModal
<ChipConfirmModal
open={showDeleteConfirmDialog}
onOpenChange={setShowDeleteConfirmDialog}
srTitle='Disconnect Integration'
>
<ChipModalHeader showDivider={false}>Disconnect Integration</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Disconnect Integration'
description={
<>
Are you sure you want to disconnect{' '}
<span className='font-medium text-[var(--text-primary)]'>{credential.displayName}</span>
? This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowDeleteConfirmDialog(false)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleConfirmDelete}
disabled={disconnectOAuthService.isPending || deleteCredential.isPending}
>
{disconnectOAuthService.isPending || deleteCredential.isPending
? 'Disconnecting...'
: 'Disconnect'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Disconnect',
onClick: handleConfirmDelete,
pending: disconnectOAuthService.isPending || deleteCredential.isPending,
pendingLabel: 'Disconnecting...',
}}
/>
<AddPeopleModal
credentialId={credential.id}
@@ -1,6 +1,6 @@
'use client'
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'
import type { ChunkData } from '@/lib/knowledge/types'
import { useDeleteChunk } from '@/hooks/queries/kb/knowledge'
@@ -30,23 +30,18 @@ export function DeleteChunkModal({
if (!chunk) return null
return (
<ChipModal open={isOpen} onOpenChange={onClose} srTitle='Delete Chunk'>
<ChipModalHeader onClose={onClose} showDivider={false}>
Delete Chunk
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
Are you sure you want to delete this chunk? This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush disabled={isDeleting} onClick={onClose}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDeleteChunk} disabled={isDeleting}>
{isDeleting ? <>Deleting…</> : <>Delete</>}
</Chip>
</ChipModalFooter>
</ChipModal>
<ChipConfirmModal
open={isOpen}
onOpenChange={onClose}
srTitle='Delete Chunk'
title='Delete Chunk'
description='Are you sure you want to delete this chunk? This action cannot be undone.'
confirm={{
label: 'Delete',
onClick: handleDeleteChunk,
pending: isDeleting,
pendingLabel: 'Deleting...',
}}
/>
)
}
@@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
import {
Badge,
Button,
Chip,
ChipCombobox,
ChipInput,
ChipModal,
@@ -16,7 +15,6 @@ import {
Label,
Trash,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import { ALL_TAG_SLOTS, type AllTagSlot, MAX_TAG_SLOTS } from '@/lib/knowledge/constants'
import type { DocumentTag } from '@/lib/knowledge/tags/types'
@@ -459,7 +457,6 @@ export function DocumentTagsModal({
}}
placeholder='Enter or select tag name'
editable={true}
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
/>
) : (
<ChipInput
@@ -612,7 +609,6 @@ export function DocumentTagsModal({
}}
placeholder='Enter or select tag name'
editable={true}
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
/>
) : (
<ChipInput
@@ -745,11 +741,10 @@ export function DocumentTagsModal({
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => handleClose(false)}>
Close
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleClose(false)}
primaryAction={{ label: 'Close', onClick: () => handleClose(false) }}
/>
</ChipModal>
)
}
@@ -4,16 +4,7 @@ import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } fro
import { createLogger } from '@sim/logger'
import { ChevronDown, ChevronUp, FileText, Pencil, Tag } from 'lucide-react'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import {
Badge,
Chip,
ChipCombobox,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Trash,
} from '@/components/emcn'
import { Badge, ChipCombobox, ChipConfirmModal, Trash } from '@/components/emcn'
import { Database } from '@/components/emcn/icons'
import { SearchHighlight } from '@/components/ui/search-highlight'
import type { ChunkData } from '@/lib/knowledge/types'
@@ -64,33 +55,20 @@ type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
interface UnsavedChangesModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onKeepEditing: () => void
onDiscard: () => void
}
function UnsavedChangesModal({
open,
onOpenChange,
onKeepEditing,
onDiscard,
}: UnsavedChangesModalProps) {
function UnsavedChangesModal({ open, onOpenChange, onDiscard }: UnsavedChangesModalProps) {
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle='Unsaved Changes'>
<ChipModalHeader showDivider={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={onKeepEditing}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={onDiscard}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
<ChipConfirmModal
open={open}
onOpenChange={onOpenChange}
srTitle='Unsaved Changes'
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{ label: 'Discard Changes', onClick: onDiscard }}
/>
)
}
@@ -360,6 +338,13 @@ export function Document({
}
}, [isDirty, isCreatingNewChunk])
const handleUnsavedChangesOpenChange = useCallback((open: boolean) => {
if (!open) {
setShowUnsavedChangesAlert(false)
setPendingAction(null)
}
}, [])
const handleDiscardChanges = useCallback(() => {
setShowUnsavedChangesAlert(false)
const action = pendingAction
@@ -1095,11 +1080,7 @@ export function Document({
<UnsavedChangesModal
open={showUnsavedChangesAlert}
onOpenChange={setShowUnsavedChangesAlert}
onKeepEditing={() => {
setShowUnsavedChangesAlert(false)
setPendingAction(null)
}}
onOpenChange={handleUnsavedChangesOpenChange}
onDiscard={handleDiscardChanges}
/>
</>
@@ -1141,11 +1122,7 @@ export function Document({
<UnsavedChangesModal
open={showUnsavedChangesAlert}
onOpenChange={setShowUnsavedChangesAlert}
onKeepEditing={() => {
setShowUnsavedChangesAlert(false)
setPendingAction(null)
}}
onOpenChange={handleUnsavedChangesOpenChange}
onDiscard={handleDiscardChanges}
/>
</>
@@ -1201,14 +1178,13 @@ export function Document({
isLoading={isBulkOperating}
/>
<ChipModal
<ChipConfirmModal
open={showDeleteDocumentDialog}
onOpenChange={setShowDeleteDocumentDialog}
srTitle='Delete Document'
>
<ChipModalHeader showDivider={false}>Delete Document</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Document'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{effectiveDocumentName}</span>?{' '}
<span className='text-[var(--text-error)]'>
@@ -1224,27 +1200,15 @@ export function Document({
) : (
<>This action cannot be undone.</>
)}
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteDocumentDialog(false)}
disabled={isDeletingDocument}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteDocument}
disabled={isDeletingDocument}
>
{isDeletingDocument ? 'Deleting...' : 'Delete Document'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete Document',
onClick: handleDeleteDocument,
pending: isDeletingDocument,
pendingLabel: 'Deleting...',
}}
/>
<ChunkContextMenu
isOpen={isContextMenuOpen}
@@ -11,14 +11,13 @@ import { usePostHog } from 'posthog-js/react'
import {
Badge,
Button,
Chip,
ChipConfirmModal,
ChipDatePicker,
ChipDropdown,
type ChipDropdownOption,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
chipVariants,
Loader,
@@ -1192,14 +1191,13 @@ export function KnowledgeBase({
<BaseTagsModal open={showTagsModal} onOpenChange={setShowTagsModal} knowledgeBaseId={id} />
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
srTitle='Delete Knowledge Base'
>
<ChipModalHeader showDivider={false}>Delete Knowledge Base</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Knowledge Base'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
<span className='text-[var(--text-error)]'>
@@ -1207,86 +1205,62 @@ export function KnowledgeBase({
{pagination.total === 1 ? '' : 's'} within it will be removed.
</span>{' '}
You can restore it from Recently Deleted in Settings.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteDialog(false)}
disabled={isDeleting}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteKnowledgeBase}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete Knowledge Base'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete Knowledge Base',
onClick: handleDeleteKnowledgeBase,
pending: isDeleting,
pendingLabel: 'Deleting...',
}}
/>
<ChipModal
<ChipConfirmModal
open={showDeleteDocumentModal}
onOpenChange={setShowDeleteDocumentModal}
onOpenChange={(open) => {
setShowDeleteDocumentModal(open)
if (!open) setDocumentToDelete(null)
}}
srTitle='Delete Document'
>
<ChipModalHeader showDivider={false}>Delete Document</ChipModalHeader>
<ChipModalBody>
{(() => {
const docToDelete = documents.find((doc) => doc.id === documentToDelete)
return (
<p className='px-2 text-[var(--text-secondary)] text-sm'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{docToDelete?.filename ?? 'this document'}
title='Delete Document'
description={(() => {
const docToDelete = documents.find((doc) => doc.id === documentToDelete)
return (
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{docToDelete?.filename ?? 'this document'}
</span>
?{' '}
{docToDelete?.connectorId ? (
<span className='text-[var(--text-error)]'>
This document is synced from a connector. Deleting it will permanently exclude it
from future syncs. To temporarily hide it from search, disable it instead.
</span>
?{' '}
{docToDelete?.connectorId ? (
) : (
<>
<span className='text-[var(--text-error)]'>
This document is synced from a connector. Deleting it will permanently exclude
it from future syncs. To temporarily hide it from search, disable it instead.
</span>
) : (
<>
<span className='text-[var(--text-error)]'>
This will permanently delete the document.
</span>{' '}
This action cannot be undone.
</>
)}
</p>
)
})()}
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setShowDeleteDocumentModal(false)
setDocumentToDelete(null)
}}
>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={confirmDeleteDocument}>
Delete Document
</Chip>
</ChipModalFooter>
</ChipModal>
This will permanently delete the document.
</span>{' '}
This action cannot be undone.
</>
)}
</>
)
})()}
confirm={{
label: 'Delete Document',
onClick: confirmDeleteDocument,
}}
/>
<ChipModal
<ChipConfirmModal
open={showBulkDeleteModal}
onOpenChange={setShowBulkDeleteModal}
srTitle='Delete Documents'
>
<ChipModalHeader showDivider={false}>Delete Documents</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Documents'
description={
<>
Are you sure you want to delete {selectedDocuments.size} document
{selectedDocuments.size === 1 ? '' : 's'}?{' '}
<span className='text-[var(--text-error)]'>
@@ -1294,19 +1268,15 @@ export function KnowledgeBase({
{selectedDocuments.size === 1 ? '' : 's'}.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowBulkDeleteModal(false)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={confirmBulkDelete} disabled={isBulkOperating}>
{isBulkOperating
? 'Deleting...'
: `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`,
onClick: confirmBulkDelete,
pending: isBulkOperating,
pendingLabel: 'Deleting...',
}}
/>
<AddDocumentsModal
open={showAddDocumentsModal}
@@ -9,7 +9,6 @@ import {
ButtonGroup,
ButtonGroupItem,
Checkbox,
Chip,
ChipCombobox,
ChipInput,
ChipModal,
@@ -498,19 +497,15 @@ export function AddConnectorModal({
</ChipModalBody>
{step === 'configure' && (
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isCreating}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={!canSubmit || isCreating}
>
{isCreating ? 'Connecting…' : 'Connect & Sync'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={isCreating}
primaryAction={{
label: isCreating ? 'Connecting…' : 'Connect & Sync',
onClick: handleSubmit,
disabled: !canSubmit || isCreating,
}}
/>
)}
</ChipModal>
{showOAuthModal &&
@@ -6,7 +6,6 @@ import { RotateCcw, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Button,
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -258,25 +257,21 @@ export function AddDocumentsModal({
{uploadError && <ChipModalError>{uploadError.message}</ChipModalError>}
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose} disabled={isUploading}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
>
{isUploading
<ChipModalFooter
onCancel={handleClose}
cancelDisabled={isUploading}
primaryAction={{
label: isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Uploading...'
: 'Upload'}
</Chip>
</ChipModalFooter>
: 'Upload',
onClick: handleUpload,
disabled: files.length === 0 || isUploading,
}}
/>
</ChipModal>
)
}
@@ -4,8 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import {
Button,
Chip,
ChipCombobox,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -397,22 +397,23 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => handleClose(false)}>
Close
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleClose(false)}
primaryAction={{ label: 'Close', onClick: () => handleClose(false) }}
/>
</ChipModal>
{/* Delete Tag Confirmation Dialog */}
<ChipModal
<ChipConfirmModal
open={deleteTagDialogOpen}
onOpenChange={setDeleteTagDialogOpen}
onOpenChange={(open) => {
setDeleteTagDialogOpen(open)
if (!open) setSelectedTag(null)
}}
srTitle='Delete Tag'
>
<ChipModalHeader showDivider={false}>Delete Tag</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Tag'
description={
<>
Are you sure you want to delete the &ldquo;
<span className='text-[var(--text-primary)]'>{selectedTag?.displayName}</span>&rdquo;
tag?{' '}
@@ -421,36 +422,25 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.
</span>{' '}
This action cannot be undone.
</p>
{selectedTagUsage && selectedTagUsage.documentCount > 0 && (
<div className='flex flex-col gap-2 px-2'>
<Label>Affected documents:</Label>
<DocumentList
documents={selectedTagUsage.documents}
totalCount={selectedTagUsage.documentCount}
/>
</div>
)}
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
disabled={deleteTagMutation.isPending}
onClick={() => setDeleteTagDialogOpen(false)}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={confirmDeleteTag}
disabled={deleteTagMutation.isPending}
>
{deleteTagMutation.isPending ? 'Deleting...' : 'Delete Tag'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete Tag',
onClick: confirmDeleteTag,
pending: deleteTagMutation.isPending,
pendingLabel: 'Deleting...',
}}
>
{selectedTagUsage && selectedTagUsage.documentCount > 0 && (
<div className='flex flex-col gap-2 px-2'>
<Label>Affected documents:</Label>
<DocumentList
documents={selectedTagUsage.documents}
totalCount={selectedTagUsage.documentCount}
/>
</div>
)}
</ChipConfirmModal>
{/* View Documents Dialog */}
<ChipModal
@@ -485,11 +475,10 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
)}
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setViewDocumentsDialogOpen(false)}>
Close
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setViewDocumentsDialogOpen(false)}
primaryAction={{ label: 'Close', onClick: () => setViewDocumentsDialogOpen(false) }}
/>
</ChipModal>
</>
)
@@ -19,11 +19,7 @@ import {
Badge,
Button,
Checkbox,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
Loader,
Skeleton,
Tooltip,
@@ -183,6 +179,24 @@ export function ConnectorsSection({
[knowledgeBaseId, updateConnector, addToSet, removeFromSet]
)
const handleDeleteConnector = () => {
if (!deleteTarget) return
deleteConnector(
{ knowledgeBaseId, connectorId: deleteTarget, deleteDocuments },
{
onSuccess: () => {
setError(null)
closeDeleteModal()
},
onError: (err) => {
logger.error('Delete connector failed', { error: err.message })
setError(err.message)
closeDeleteModal()
},
}
)
}
if (connectors.length === 0 && !canEdit && !isLoading) return null
return (
@@ -240,62 +254,35 @@ export function ConnectorsSection({
/>
)}
<ChipModal
<ChipConfirmModal
open={deleteTarget !== null}
onOpenChange={closeDeleteModal}
onOpenChange={(open) => {
if (!open) closeDeleteModal()
}}
srTitle='Remove Connector'
title='Remove Connector'
description='This will disconnect the source and stop future syncs. Documents already synced will remain in the knowledge base unless you choose to delete them.'
confirm={{
label: 'Remove',
onClick: handleDeleteConnector,
pending: isDeleting,
pendingLabel: 'Removing...',
}}
>
<ChipModalHeader showDivider={false}>Remove Connector</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
This will disconnect the source and stop future syncs. Documents already synced will
remain in the knowledge base unless you choose to delete them.
</p>
<div className='flex items-center gap-2 px-2'>
<Checkbox
id={deleteDocumentsId}
checked={deleteDocuments}
onCheckedChange={(checked) => setDeleteDocuments(checked === true)}
/>
<label
htmlFor={deleteDocumentsId}
className='cursor-pointer text-[var(--text-secondary)] text-small'
>
Also delete all synced documents
</label>
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={closeDeleteModal} disabled={isDeleting}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
disabled={isDeleting}
onClick={() => {
if (deleteTarget) {
deleteConnector(
{ knowledgeBaseId, connectorId: deleteTarget, deleteDocuments },
{
onSuccess: () => {
setError(null)
closeDeleteModal()
},
onError: (err) => {
logger.error('Delete connector failed', { error: err.message })
setError(err.message)
closeDeleteModal()
},
}
)
}
}}
<div className='flex items-center gap-2 px-2'>
<Checkbox
id={deleteDocumentsId}
checked={deleteDocuments}
onCheckedChange={(checked) => setDeleteDocuments(checked === true)}
/>
<label
htmlFor={deleteDocumentsId}
className='cursor-pointer text-[var(--text-secondary)] text-small'
>
{isDeleting ? 'Removing...' : 'Remove'}
</Chip>
</ChipModalFooter>
</ChipModal>
Also delete all synced documents
</label>
</div>
</ChipConfirmModal>
</div>
)
}
@@ -7,7 +7,6 @@ import {
Button,
ButtonGroup,
ButtonGroupItem,
Chip,
ChipCombobox,
ChipInput,
ChipModal,
@@ -314,14 +313,15 @@ export function EditConnectorModal({
</ChipModalBody>
{activeTab === 'settings' && (
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isSaving}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleSave} disabled={!hasChanges || isSaving}>
{isSaving ? 'Saving…' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={isSaving}
primaryAction={{
label: isSaving ? 'Saving…' : 'Save',
onClick: handleSave,
disabled: !hasChanges || isSaving,
}}
/>
)}
</ChipModal>
)
@@ -4,7 +4,6 @@ import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -102,19 +101,15 @@ export function RenameDocumentModal({
/>
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isSubmitting}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={() => void handleSubmit()}
disabled={isSubmitting || !name?.trim() || name.trim() === initialName}
>
{isSubmitting ? 'Renaming...' : 'Rename'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={isSubmitting}
primaryAction={{
label: isSubmitting ? 'Renaming...' : 'Rename',
onClick: () => void handleSubmit(),
disabled: isSubmitting || !name?.trim() || name.trim() === initialName,
}}
/>
</ChipModal>
)
}
@@ -11,7 +11,6 @@ import { z } from 'zod'
import {
Button,
Checkbox,
Chip,
ChipCombobox,
ChipInput,
ChipModal,
@@ -321,6 +320,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
<ChipModalHeader onClose={() => handleClose(false)}>Create Knowledge Base</ChipModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
<button type='submit' hidden disabled={isSubmitting || !nameValue?.trim()} />
<ChipModalBody className='max-h-[70vh] overflow-y-auto'>
<div ref={scrollContainerRef} className='min-h-0 flex-1'>
<div className='space-y-3'>
@@ -578,18 +578,11 @@ export const CreateBaseModal = memo(function CreateBaseModal({
<ChipModalError>{uploadError?.message || submitStatus?.message}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => handleClose(false)}
type='button'
disabled={isSubmitting}
>
Cancel
</Chip>
<Chip variant='primary' flush type='submit' disabled={isSubmitting || !nameValue?.trim()}>
{isSubmitting
<ChipModalFooter
onCancel={() => handleClose(false)}
cancelDisabled={isSubmitting}
primaryAction={{
label: isSubmitting
? isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
@@ -597,9 +590,11 @@ export const CreateBaseModal = memo(function CreateBaseModal({
? 'Processing...'
: 'Creating...'
: 'Creating...'
: 'Create'}
</Chip>
</ChipModalFooter>
: 'Create',
onClick: handleSubmit(onSubmit),
disabled: isSubmitting || !nameValue?.trim(),
}}
/>
</form>
</ChipModal>
)
@@ -1,7 +1,7 @@
'use client'
import { memo } from 'react'
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'
interface DeleteKnowledgeBaseModalProps {
/**
@@ -38,12 +38,13 @@ export const DeleteKnowledgeBaseModal = memo(function DeleteKnowledgeBaseModal({
knowledgeBaseName,
}: DeleteKnowledgeBaseModalProps) {
return (
<ChipModal open={isOpen} onOpenChange={onClose} srTitle='Delete Knowledge Base'>
<ChipModalHeader onClose={onClose} showDivider={false}>
Delete Knowledge Base
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={isOpen}
onOpenChange={onClose}
srTitle='Delete Knowledge Base'
title='Delete Knowledge Base'
description={
<>
{knowledgeBaseName ? (
<>
Are you sure you want to delete{' '}
@@ -61,16 +62,14 @@ export const DeleteKnowledgeBaseModal = memo(function DeleteKnowledgeBaseModal({
</>
)}{' '}
You can restore it from Recently Deleted in Settings.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={onClose} disabled={isDeleting}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={onConfirm} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: onConfirm,
pending: isDeleting,
pendingLabel: 'Deleting...',
}}
/>
)
})
@@ -4,7 +4,6 @@ import { memo, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -156,19 +155,15 @@ export const EditKnowledgeBaseModal = memo(function EditKnowledgeBaseModal({
)}
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isSubmitting}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={!isValid || !isDirty || isSubmitting}
>
{isSubmitting ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={isSubmitting}
primaryAction={{
label: isSubmitting ? 'Saving...' : 'Save',
onClick: handleSubmit,
disabled: !isValid || !isDirty || isSubmitting,
}}
/>
</ChipModal>
)
})
@@ -4,12 +4,12 @@ import type { ReactNode } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { Plus, X } from 'lucide-react'
import { X } from 'lucide-react'
import {
Badge,
Button,
Chip,
ChipCombobox,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalFooter,
@@ -508,6 +508,16 @@ export const NotificationSettings = memo(function NotificationSettings({
}
}
const handleBackToList = () => {
resetForm()
setShowForm(false)
}
const handleAddNew = () => {
resetForm()
setShowForm(true)
}
const handleEdit = (subscription: NotificationSubscription) => {
setActiveTab(subscription.notificationType)
setEditingId(subscription.id)
@@ -1229,86 +1239,58 @@ export const NotificationSettings = memo(function NotificationSettings({
</ChipModalBody>
<ChipModalFooter
leading={
displayForm && hasSubscriptions ? (
<Chip
variant='filled'
flush
onClick={() => {
resetForm()
setShowForm(false)
}}
>
Back
</Chip>
) : undefined
onCancel={handleClose}
secondaryAction={
displayForm && hasSubscriptions
? { label: 'Back', onClick: handleBackToList }
: undefined
}
>
{displayForm ? (
<Chip
variant='primary'
flush
onClick={handleSave}
disabled={createNotification.isPending || updateNotification.isPending}
>
{createNotification.isPending || updateNotification.isPending
? editingId
? 'Updating...'
: 'Creating...'
: editingId
? 'Update'
: 'Create'}
</Chip>
) : (
<Chip
variant='primary'
flush
leftIcon={Plus}
onClick={() => {
resetForm()
setShowForm(true)
}}
disabled={isLoading}
>
Add
</Chip>
)}
</ChipModalFooter>
primaryAction={
displayForm
? {
label:
createNotification.isPending || updateNotification.isPending
? editingId
? 'Updating...'
: 'Creating...'
: editingId
? 'Update'
: 'Create',
onClick: handleSave,
disabled: createNotification.isPending || updateNotification.isPending,
}
: {
label: 'Add',
onClick: handleAddNew,
disabled: isLoading,
}
}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onOpenChange={(next) => {
if (!next) setDeletingId(null)
setShowDeleteDialog(next)
}}
srTitle='Delete Notification'
>
<ChipModalHeader showDivider={false}>Delete Notification</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Notification'
description={
<>
<span className='text-[var(--text-error)]'>
This will permanently remove the notification and stop all deliveries.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
disabled={deleteNotification.isPending}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDelete}
disabled={deleteNotification.isPending}
>
{deleteNotification.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteNotification.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
})
@@ -6,7 +6,6 @@ import { getErrorMessage } from '@sim/utils/errors'
import {
ButtonGroup,
ButtonGroupItem,
Chip,
ChipCombobox,
ChipInput,
ChipModal,
@@ -511,27 +510,21 @@ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: Sch
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={
!isFormValid || createScheduleMutation.isPending || updateScheduleMutation.isPending
}
>
{isEditing
<ChipModalFooter
onCancel={handleClose}
primaryAction={{
label: isEditing
? updateScheduleMutation.isPending
? 'Saving...'
: 'Save changes'
: createScheduleMutation.isPending
? 'Creating...'
: 'Create'}
</Chip>
</ChipModalFooter>
: 'Create',
onClick: handleSubmit,
disabled:
!isFormValid || createScheduleMutation.isPending || updateScheduleMutation.isPending,
}}
/>
</ChipModal>
)
}
@@ -4,14 +4,7 @@ import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { formatAbsoluteDate } from '@sim/utils/formatting'
import { useParams } from 'next/navigation'
import {
Chip,
ChipCombobox,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
} from '@/components/emcn'
import { ChipCombobox, ChipConfirmModal } from '@/components/emcn'
import { Calendar } from '@/components/emcn/icons'
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
import type {
@@ -219,6 +212,11 @@ export function ScheduledTasks() {
}
}
const handleDeleteDialogOpenChange = (open: boolean) => {
setIsDeleteDialogOpen(open)
if (!open) setActiveTask(null)
}
const handlePause = async () => {
if (!activeTask) return
try {
@@ -443,43 +441,27 @@ export function ScheduledTasks() {
schedule={activeTask ?? undefined}
/>
<ChipModal
<ChipConfirmModal
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
onOpenChange={handleDeleteDialogOpenChange}
srTitle='Delete Scheduled Task'
>
<ChipModalHeader showDivider={false}>Delete Scheduled Task</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Scheduled Task'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{activeTask?.jobTitle || 'this task'}
</span>
? This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setIsDeleteDialogOpen(false)
setActiveTask(null)
}}
disabled={deleteSchedule.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDelete}
disabled={deleteSchedule.isPending}
>
{deleteSchedule.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteSchedule.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -5,17 +5,7 @@ import { createLogger } from '@sim/logger'
import { formatDate } from '@sim/utils/formatting'
import { Info, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Chip,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Search,
Switch,
Tooltip,
} from '@/components/emcn'
import { Chip, ChipConfirmModal, ChipInput, Search, Switch, Tooltip } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
@@ -339,44 +329,33 @@ export function ApiKeys() {
/>
{/* Delete Confirmation Dialog */}
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onOpenChange={(open) => {
if (!open) {
setShowDeleteDialog(false)
setDeleteKey(null)
}
}}
srTitle='Delete API key'
>
<ChipModalHeader showDivider={false}>Delete API key</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete API key'
description={
<>
Deleting{' '}
<span className='font-medium text-[var(--text-primary)]'>{deleteKey?.name}</span>{' '}
<span className='text-[var(--text-error)]'>
will immediately revoke access for any integrations using it.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setShowDeleteDialog(false)
setDeleteKey(null)
}}
disabled={deleteApiKeyMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteKey}
disabled={deleteApiKeyMutation.isPending}
>
{deleteApiKeyMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDeleteKey,
pending: deleteApiKeyMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
</div>
)
}
@@ -6,7 +6,6 @@ import { getErrorMessage } from '@sim/utils/errors'
import {
ButtonGroup,
ButtonGroupItem,
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -155,23 +154,17 @@ export function CreateApiKeyModal({
/>
<ChipModalError>{createError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleCreateKey}
disabled={
<ChipModalFooter
onCancel={handleClose}
primaryAction={{
label: createApiKeyMutation.isPending ? 'Creating...' : 'Create',
onClick: handleCreateKey,
disabled:
!keyName.trim() ||
createApiKeyMutation.isPending ||
(keyType === 'workspace' && !canManageWorkspaceKeys)
}
>
{createApiKeyMutation.isPending ? 'Creating...' : 'Create'}
</Chip>
</ChipModalFooter>
(keyType === 'workspace' && !canManageWorkspaceKeys),
}}
/>
</ChipModal>
{/* New API Key Dialog - shows the created key */}
@@ -193,11 +186,10 @@ export function CreateApiKeyModal({
{newKey && <SecretReveal value={newKey.key} />}
</ChipModalField>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='primary' flush onClick={() => setShowNewKeyDialog(false)}>
Done
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setShowNewKeyDialog(false)}
primaryAction={{ label: 'Done', onClick: () => setShowNewKeyDialog(false) }}
/>
</ChipModal>
</>
)
@@ -8,6 +8,7 @@ import { useParams } from 'next/navigation'
import {
Button,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -527,39 +528,31 @@ export function BYOK() {
</ChipModalField>
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setEditingProvider(null)
setApiKeyInput('')
setShowApiKey(false)
setError(null)
}}
disabled={upsertKey.isPending}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSave}
disabled={!apiKeyInput.trim() || upsertKey.isPending}
>
{upsertKey.isPending ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => {
setEditingProvider(null)
setApiKeyInput('')
setShowApiKey(false)
setError(null)
}}
cancelDisabled={upsertKey.isPending}
primaryAction={{
label: upsertKey.isPending ? 'Saving...' : 'Save',
onClick: handleSave,
disabled: !apiKeyInput.trim() || upsertKey.isPending,
}}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={!!deleteConfirmProvider}
onOpenChange={() => setDeleteConfirmProvider(null)}
onOpenChange={(open) => {
if (!open) setDeleteConfirmProvider(null)
}}
srTitle='Delete API Key'
>
<ChipModalHeader showDivider={false}>Delete API Key</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete API Key'
description={
<>
Are you sure you want to delete the{' '}
<span className='font-medium text-[var(--text-primary)]'>
{PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name}
@@ -569,17 +562,15 @@ export function BYOK() {
This workspace will revert to using platform hosted keys.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setDeleteConfirmProvider(null)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDelete} disabled={deleteKey.isPending}>
{deleteKey.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteKey.isPending,
pendingLabel: 'Deleting...',
}}
/>
</div>
)
}
@@ -7,6 +7,7 @@ import { formatDate } from '@sim/utils/formatting'
import { Plus } from 'lucide-react'
import {
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -275,27 +276,18 @@ export function Copilot() {
/>
<ChipModalError>{createError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setIsCreateDialogOpen(false)
setNewKeyName('')
setCreateError(null)
}}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleCreateKey}
disabled={!newKeyName.trim() || generateKey.isPending}
>
{generateKey.isPending ? 'Creating...' : 'Create'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => {
setIsCreateDialogOpen(false)
setNewKeyName('')
setCreateError(null)
}}
primaryAction={{
label: generateKey.isPending ? 'Creating...' : 'Create',
onClick: handleCreateKey,
disabled: !newKeyName.trim() || generateKey.isPending,
}}
/>
</ChipModal>
{/* New API Key Dialog */}
@@ -322,29 +314,34 @@ export function Copilot() {
{newKey && <SecretReveal value={newKey} />}
</ChipModalField>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='primary'
flush
onClick={() => {
<ChipModalFooter
onCancel={() => {
setShowNewKeyDialog(false)
setNewKey(null)
}}
primaryAction={{
label: 'Done',
onClick: () => {
setShowNewKeyDialog(false)
setNewKey(null)
}}
>
Done
</Chip>
</ChipModalFooter>
},
}}
/>
</ChipModal>
{/* Delete Confirmation Dialog */}
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onOpenChange={(open) => {
if (!open) {
setShowDeleteDialog(false)
setDeleteKey(null)
}
}}
srTitle='Delete API key'
>
<ChipModalHeader showDivider={false}>Delete API key</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete API key'
description={
<>
Deleting{' '}
<span className='font-medium text-[var(--text-primary)]'>
{deleteKey?.name || 'Unnamed Key'}
@@ -353,30 +350,15 @@ export function Copilot() {
will immediately revoke access for any integrations using it.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setShowDeleteDialog(false)
setDeleteKey(null)
}}
disabled={deleteKeyMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteKey}
disabled={deleteKeyMutation.isPending}
>
{deleteKeyMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDeleteKey,
pending: deleteKeyMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -11,6 +11,7 @@ import {
ButtonGroup,
ButtonGroupItem,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -781,78 +782,61 @@ export function CredentialSets() {
</ChipModalField>
<ChipModalError>{createError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleCloseCreateModal}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleCreateCredentialSet}
disabled={!newSetName.trim() || createCredentialSet.isPending}
>
{createCredentialSet.isPending ? 'Creating...' : 'Create'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleCloseCreateModal}
primaryAction={{
label: createCredentialSet.isPending ? 'Creating...' : 'Create',
onClick: handleCreateCredentialSet,
disabled: !newSetName.trim() || createCredentialSet.isPending,
}}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={!!leavingMembership}
onOpenChange={() => setLeavingMembership(null)}
onOpenChange={(open) => {
if (!open) setLeavingMembership(null)
}}
srTitle='Leave Polling Group'
>
<ChipModalHeader showDivider={false}>Leave Polling Group</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Leave Polling Group'
description={
<>
Are you sure you want to leave{' '}
<span className='font-medium text-[var(--text-primary)]'>
{leavingMembership?.name}
</span>
? Your email account will no longer be polled in workflows using this group.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setLeavingMembership(null)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={confirmLeave}
disabled={leaveCredentialSet.isPending}
>
{leaveCredentialSet.isPending ? 'Leaving...' : 'Leave'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Leave',
onClick: confirmLeave,
pending: leaveCredentialSet.isPending,
pendingLabel: 'Leaving...',
}}
/>
<ChipModal
<ChipConfirmModal
open={!!deletingSet}
onOpenChange={() => setDeletingSet(null)}
onOpenChange={(open) => {
if (!open) setDeletingSet(null)
}}
srTitle='Delete Polling Group'
>
<ChipModalHeader showDivider={false}>Delete Polling Group</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Polling Group'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{deletingSet?.name}</span>?{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setDeletingSet(null)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={confirmDelete}
disabled={deleteCredentialSet.isPending}
>
{deleteCredentialSet.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: confirmDelete,
pending: deleteCredentialSet.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -5,15 +5,7 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Chip,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Search,
} from '@/components/emcn'
import { Chip, ChipConfirmModal, ChipInput, Search } from '@/components/emcn'
import { CustomToolModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
import { useCustomTools, useDeleteCustomTool } from '@/hooks/queries/custom-tools'
@@ -187,28 +179,22 @@ export function CustomTools() {
}
/>
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onOpenChange={(open) => {
if (!open) setShowDeleteDialog(false)
}}
srTitle='Delete Custom Tool'
>
<ChipModalHeader showDivider={false}>Delete Custom Tool</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Custom Tool'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{toolToDelete?.name}</span>?{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowDeleteDialog(false)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDeleteTool}>
Delete
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{ label: 'Delete', onClick: handleDeleteTool }}
/>
</>
)
}
@@ -530,28 +530,19 @@ export function General() {
</p>
<ChipModalError>{resetPassword.error?.message}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowResetPasswordModal(false)}
disabled={resetPassword.isPending || resetPassword.isSuccess}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleResetPasswordConfirm}
disabled={resetPassword.isPending || resetPassword.isSuccess}
>
{resetPassword.isPending
<ChipModalFooter
onCancel={() => setShowResetPasswordModal(false)}
cancelDisabled={resetPassword.isPending || resetPassword.isSuccess}
primaryAction={{
label: resetPassword.isPending
? 'Sending...'
: resetPassword.isSuccess
? 'Sent'
: 'Send Reset Email'}
</Chip>
</ChipModalFooter>
: 'Send Reset Email',
onClick: handleResetPasswordConfirm,
disabled: resetPassword.isPending || resetPassword.isSuccess,
}}
/>
</ChipModal>
</div>
)
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
import {
Chip,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalField,
@@ -95,20 +95,23 @@ export function InboxEnableToggle() {
Leave blank for an auto-generated address.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setIsEnableOpen(false)}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleEnable} disabled={toggleInbox.isPending}>
Enable
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setIsEnableOpen(false)}
primaryAction={{
label: 'Enable',
onClick: handleEnable,
disabled: toggleInbox.isPending,
}}
/>
</ChipModal>
<ChipModal open={isDisableOpen} onOpenChange={setIsDisableOpen} srTitle='Disable email inbox'>
<ChipModalHeader showDivider={false}>Disable email inbox</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={isDisableOpen}
onOpenChange={setIsDisableOpen}
srTitle='Disable email inbox'
title='Disable email inbox'
description={
<>
Are you sure you want to disable the inbox
{config?.address && (
<>
@@ -118,30 +121,19 @@ export function InboxEnableToggle() {
)}
? Any emails sent to this address after disabling will not be delivered. This action
cannot be undone.
</p>
<p className='mt-2 px-2 text-[var(--text-secondary)] text-sm'>
Your existing conversations and task history will be preserved.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
disabled={toggleInbox.isPending}
onClick={() => setIsDisableOpen(false)}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
disabled={toggleInbox.isPending}
onClick={handleDisable}
>
{toggleInbox.isPending ? 'Disabling...' : 'Disable inbox'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Disable inbox',
onClick: handleDisable,
pending: toggleInbox.isPending,
pendingLabel: 'Disabling...',
}}
>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
Your existing conversations and task history will be preserved.
</p>
</ChipConfirmModal>
</>
)
}
@@ -195,7 +195,6 @@ export function InboxSettingsTab() {
<Chip
variant='ghost'
flush
className='text-[var(--text-muted)] hover-hover:text-[var(--text-error)]'
leftIcon={Trash2}
aria-label='Remove sender'
onClick={() => handleRemoveSender(sender.id)}
@@ -265,19 +264,14 @@ export function InboxSettingsTab() {
/>
<ChipModalError>{addSenderError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setIsAddSenderOpen(false)}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleAddSender}
disabled={!newSenderEmail.trim() || addSender.isPending}
>
Add
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setIsAddSenderOpen(false)}
primaryAction={{
label: 'Add',
onClick: handleAddSender,
disabled: !newSenderEmail.trim() || addSender.isPending,
}}
/>
</ChipModal>
<ChipModal
@@ -285,7 +279,9 @@ export function InboxSettingsTab() {
onOpenChange={setIsEditAddressOpen}
srTitle='Change email address'
>
<ChipModalHeader showDivider={false}>Change email address</ChipModalHeader>
<ChipModalHeader onClose={() => setIsEditAddressOpen(false)}>
Change email address
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
Changing your email address will create a new inbox.{' '}
@@ -310,24 +306,16 @@ export function InboxSettingsTab() {
)}
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
disabled={updateAddress.isPending}
onClick={() => setIsEditAddressOpen(false)}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleEditAddress}
disabled={!newUsername.trim() || updateAddress.isPending}
>
{updateAddress.isPending ? 'Updating...' : 'Change address'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setIsEditAddressOpen(false)}
cancelDisabled={updateAddress.isPending}
primaryAction={{
label: updateAddress.isPending ? 'Updating...' : 'Change address',
onClick: handleEditAddress,
disabled: !newUsername.trim() || updateAddress.isPending,
variant: 'destructive',
}}
/>
</ChipModal>
</>
)
@@ -6,11 +6,11 @@ import { getErrorMessage } from '@sim/utils/errors'
import { ChevronDown, ChevronRight } from 'lucide-react'
import {
Button,
Chip,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
type ChipModalFooterAction,
ChipModalHeader,
ChipTextarea,
SecretInput,
@@ -612,6 +612,40 @@ export function McpServerFormModal({
const title = mode === 'add' ? 'Add New MCP Server' : 'Edit MCP Server'
const submitLabel = mode === 'add' ? 'Add MCP' : 'Save'
const handleToggleJsonMode = () => {
if (testResult) clearTestResult()
setFormMode(formMode === 'form' ? 'json' : 'form')
setJsonError(null)
setSubmitError(null)
}
const secondaryAction: ChipModalFooterAction | undefined =
mode === 'add'
? {
label: formMode === 'form' ? 'Edit JSON' : 'Edit Form',
onClick: handleToggleJsonMode,
}
: formMode === 'form'
? {
label: testButtonLabel,
onClick: handleTestConnection,
disabled: isTestingConnection || !isFormValid || isDomainBlocked,
}
: undefined
const primaryAction: ChipModalFooterAction =
formMode === 'json'
? {
label: isSubmitting ? 'Adding...' : submitLabel,
onClick: handleSubmitJson,
disabled: isSubmitting || !jsonInput.trim(),
}
: {
label: isSubmitting ? (mode === 'add' ? 'Adding...' : 'Saving...') : submitLabel,
onClick: handleSubmitForm,
disabled: isSubmitDisabled,
}
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title} size='lg'>
<ChipModalHeader onClose={() => onOpenChange(false)}>{title}</ChipModalHeader>
@@ -770,62 +804,10 @@ export function McpServerFormModal({
{submitError && <p className='w-full text-[var(--text-error)] text-small'>{submitError}</p>}
</ChipModalBody>
<ChipModalFooter
leading={
<>
{mode === 'add' && (
<Chip
type='button'
variant='filled'
flush
onClick={() => {
if (testResult) clearTestResult()
setFormMode(formMode === 'form' ? 'json' : 'form')
setJsonError(null)
setSubmitError(null)
}}
>
{formMode === 'form' ? 'Edit JSON' : 'Edit Form'}
</Chip>
)}
{mode === 'edit' && formMode === 'form' && (
<Chip
type='button'
variant='filled'
flush
onClick={handleTestConnection}
disabled={isTestingConnection || !isFormValid || isDomainBlocked}
>
{testButtonLabel}
</Chip>
)}
</>
}
>
<Chip type='button' variant='filled' flush onClick={() => onOpenChange(false)}>
Cancel
</Chip>
{formMode === 'json' ? (
<Chip
type='button'
variant='primary'
flush
onClick={handleSubmitJson}
disabled={isSubmitting || !jsonInput.trim()}
>
{isSubmitting ? 'Adding...' : submitLabel}
</Chip>
) : (
<Chip
type='button'
variant='primary'
flush
onClick={handleSubmitForm}
disabled={isSubmitDisabled}
>
{isSubmitting ? (mode === 'add' ? 'Adding...' : 'Saving...') : submitLabel}
</Chip>
)}
</ChipModalFooter>
onCancel={() => onOpenChange(false)}
secondaryAction={secondaryAction}
primaryAction={primaryAction}
/>
</ChipModal>
)
}
@@ -9,11 +9,8 @@ import {
Badge,
Button,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Search,
Tooltip,
} from '@/components/emcn'
@@ -690,32 +687,24 @@ export function MCP({ initialServerId }: MCPProps) {
allowedMcpDomains={allowedMcpDomains}
/>
<ChipModal
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={(open) => {
if (!open) setServerToDeleteId(null)
}}
srTitle='Delete MCP Server'
>
<ChipModalHeader showDivider={false}>Delete MCP Server</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete MCP Server'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{servers.find((s) => s.id === serverToDeleteId)?.name || 'this server'}
</span>
? This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setServerToDeleteId(null)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={confirmDeleteServer}>
Delete
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{ label: 'Delete', onClick: confirmDeleteServer }}
/>
</>
)
}
@@ -1,6 +1,5 @@
import {
Button,
Chip,
ChipInput,
ChipModal,
ChipModalBody,
@@ -164,24 +163,15 @@ export function NoOrganizationView({
/>
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setCreateOrgDialogOpen(false)}
disabled={isCreatingOrg}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={onCreateOrganization}
disabled={isCreatingOrg || !orgName.trim()}
>
{isCreatingOrg ? 'Creating...' : 'Create Organization'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setCreateOrgDialogOpen(false)}
cancelDisabled={isCreatingOrg}
primaryAction={{
label: isCreatingOrg ? 'Creating...' : 'Create Organization',
onClick: onCreateOrganization,
disabled: isCreatingOrg || !orgName.trim(),
}}
/>
</ChipModal>
</div>
)
@@ -3,7 +3,6 @@
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import {
Chip,
ChipDropdown,
type ChipDropdownOption,
ChipModal,
@@ -169,11 +168,14 @@ export function OrganizationInviteModal({
onChange={(role) => setInviteRole(role as PermissionType)}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='primary' onClick={handleSend} disabled={isSendDisabled}>
{isSubmitting ? 'Sending...' : 'Send invites'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
primaryAction={{
label: isSubmitting ? 'Sending...' : 'Send invites',
onClick: handleSend,
disabled: isSendDisabled,
}}
/>
</ChipModal>
)
}
@@ -1,11 +1,4 @@
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalFooter,
ChipModalHeader,
} from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'
interface RemoveMemberDialogProps {
open: boolean
@@ -36,13 +29,20 @@ export function RemoveMemberDialog({
? 'Remove External Member'
: 'Remove Team Member'
const errorMessage =
error instanceof Error && error.message ? error.message : error ? String(error) : null
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
<ChipModalHeader onClose={() => onOpenChange(false)} showDivider={false}>
{title}
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={open}
onOpenChange={(next) => {
if (!next) onCancel()
onOpenChange(next)
}}
srTitle={title}
title={title}
description={
<>
{isSelfRemoval ? (
'Are you sure you want to leave this organization? You will lose access to all team resources.'
) : isExternalRemoval ? (
@@ -60,20 +60,19 @@ export function RemoveMemberDialog({
</>
)}{' '}
This action cannot be undone.
</>
}
confirm={{
label: isSelfRemoval ? 'Leave Organization' : 'Remove',
onClick: () => onConfirmRemove(),
pending: isSubmitting,
}}
>
{errorMessage ? (
<p role='alert' className='mt-1 px-2 text-[var(--text-error)] text-caption'>
{errorMessage}
</p>
<ChipModalError>
{error instanceof Error && error.message ? error.message : error ? String(error) : null}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush disabled={isSubmitting} onClick={onCancel}>
Cancel
</Chip>
<Chip variant='destructive' flush disabled={isSubmitting} onClick={() => onConfirmRemove()}>
{isSelfRemoval ? 'Leave Organization' : 'Remove'}
</Chip>
</ChipModalFooter>
</ChipModal>
) : null}
</ChipConfirmModal>
)
}
@@ -7,12 +7,8 @@ import {
AvatarImage,
Badge,
Banner,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Search,
Skeleton,
} from '@/components/emcn'
@@ -86,9 +82,20 @@ export function TransferOwnershipDialog({
}
return (
<ChipModal open={open} onOpenChange={handleClose} srTitle='Leave organization'>
<ChipModalHeader showDivider={false}>Leave organization</ChipModalHeader>
<ChipModalBody>
<ChipConfirmModal
open={open}
onOpenChange={handleClose}
srTitle='Leave organization'
title='Leave organization'
confirm={{
label: 'Transfer & leave',
onClick: handleConfirm,
pending: isSubmitting,
pendingLabel: 'Transferring...',
disabled: !selectedUserId || !hasCandidates || isLoadingMembers,
}}
>
<div className='flex flex-col gap-4'>
{isLoadingMembers ? (
<div className='space-y-3'>
<Skeleton className='h-4 w-3/4' />
@@ -203,20 +210,7 @@ export function TransferOwnershipDialog({
{error instanceof Error && error.message ? error.message : String(error)}
</p>
)}
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => handleClose(false)} disabled={isSubmitting}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleConfirm}
disabled={!selectedUserId || isSubmitting || !hasCandidates || isLoadingMembers}
>
{isSubmitting ? 'Transferring...' : 'Transfer & leave'}
</Chip>
</ChipModalFooter>
</ChipModal>
</div>
</ChipConfirmModal>
)
}
@@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
import {
ButtonGroup,
ButtonGroupItem,
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -129,19 +128,14 @@ export function CreateWorkflowMcpServerModal({
</ChipModalField>
<ChipModalError>{createServerMutation.error?.message}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleCreateServer}
disabled={!isFormValid || createServerMutation.isPending}
>
{createServerMutation.isPending ? 'Adding...' : 'Add Server'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
primaryAction={{
label: createServerMutation.isPending ? 'Adding...' : 'Add Server',
onClick: handleCreateServer,
disabled: !isFormValid || createServerMutation.isPending,
}}
/>
</ChipModal>
)
}
@@ -11,6 +11,7 @@ import {
ButtonGroup,
ButtonGroupItem,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -159,6 +160,62 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
}
}
const handleSaveToolEdit = async () => {
if (!toolToView) return
try {
const currentSchema = toolToView.parameterSchema as Record<string, unknown>
const currentProperties = (currentSchema?.properties || {}) as Record<
string,
{ type?: string; description?: string }
>
const updatedProperties: Record<string, { type?: string; description?: string }> = {}
for (const [name, prop] of Object.entries(currentProperties)) {
updatedProperties[name] = {
...prop,
description: editingParameterDescriptions[name]?.trim() || undefined,
}
}
const updatedSchema = {
...currentSchema,
properties: updatedProperties,
}
await updateToolMutation.mutateAsync({
workspaceId,
serverId,
toolId: toolToView.id,
toolDescription: editingDescription.trim() || undefined,
parameterSchema: updatedSchema,
})
setToolToView(null)
setEditingDescription('')
setEditingParameterDescriptions({})
} catch (err) {
logger.error('Failed to update tool:', err)
}
}
const isSaveToolDisabled = (() => {
if (updateToolMutation.isPending) return true
if (!toolToView) return true
const descriptionChanged = editingDescription.trim() !== (toolToView.toolDescription || '')
const schema = toolToView.parameterSchema as
| { properties?: Record<string, { type?: string; description?: string }> }
| undefined
const properties = schema?.properties || {}
const paramDescriptionsChanged = Object.keys(properties).some((name) => {
const original = properties[name]?.description || ''
const edited = editingParameterDescriptions[name]?.trim() || ''
return original !== edited
})
return !descriptionChanged && !paramDescriptionsChanged
})()
const tools = data?.tools ?? []
const availableWorkflows = useMemo(() => {
@@ -620,33 +677,25 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
</div>
</div>
<ChipModal
<ChipConfirmModal
open={!!toolToDelete}
onOpenChange={(open) => !open && setToolToDelete(null)}
srTitle='Remove Workflow'
>
<ChipModalHeader showDivider={false}>Remove Workflow</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Remove Workflow'
description={
<>
Are you sure you want to remove{' '}
<span className='font-medium text-[var(--text-primary)]'>{toolToDelete?.toolName}</span>{' '}
from this server? The workflow will remain deployed and can be added back later.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setToolToDelete(null)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteTool}
disabled={deleteToolMutation.isPending}
>
{deleteToolMutation.isPending ? 'Removing...' : 'Remove'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Remove',
onClick: handleDeleteTool,
pending: deleteToolMutation.isPending,
pendingLabel: 'Removing...',
}}
/>
<ChipModal
open={!!toolToView}
@@ -722,73 +771,14 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
})()}
</ChipModalField>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setToolToView(null)}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={async () => {
if (!toolToView) return
try {
const currentSchema = toolToView.parameterSchema as Record<string, unknown>
const currentProperties = (currentSchema?.properties || {}) as Record<
string,
{ type?: string; description?: string }
>
const updatedProperties: Record<string, { type?: string; description?: string }> =
{}
for (const [name, prop] of Object.entries(currentProperties)) {
updatedProperties[name] = {
...prop,
description: editingParameterDescriptions[name]?.trim() || undefined,
}
}
const updatedSchema = {
...currentSchema,
properties: updatedProperties,
}
await updateToolMutation.mutateAsync({
workspaceId,
serverId,
toolId: toolToView.id,
toolDescription: editingDescription.trim() || undefined,
parameterSchema: updatedSchema,
})
setToolToView(null)
setEditingDescription('')
setEditingParameterDescriptions({})
} catch (err) {
logger.error('Failed to update tool:', err)
}
}}
disabled={(() => {
if (updateToolMutation.isPending) return true
if (!toolToView) return true
const descriptionChanged =
editingDescription.trim() !== (toolToView.toolDescription || '')
const schema = toolToView.parameterSchema as
| { properties?: Record<string, { type?: string; description?: string }> }
| undefined
const properties = schema?.properties || {}
const paramDescriptionsChanged = Object.keys(properties).some((name) => {
const original = properties[name]?.description || ''
const edited = editingParameterDescriptions[name]?.trim() || ''
return original !== edited
})
return !descriptionChanged && !paramDescriptionsChanged
})()}
>
{updateToolMutation.isPending ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setToolToView(null)}
primaryAction={{
label: updateToolMutation.isPending ? 'Saving...' : 'Save',
onClick: handleSaveToolEdit,
disabled: isSaveToolDisabled,
}}
/>
</ChipModal>
<ChipModal
@@ -834,26 +824,17 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
: null}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setShowAddWorkflow(false)
setSelectedWorkflowId(null)
}}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleAddWorkflow}
disabled={!selectedWorkflowId || addToolMutation.isPending}
>
{addToolMutation.isPending ? 'Adding...' : 'Add Workflow'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => {
setShowAddWorkflow(false)
setSelectedWorkflowId(null)
}}
primaryAction={{
label: addToolMutation.isPending ? 'Adding...' : 'Add Workflow',
onClick: handleAddWorkflow,
disabled: !selectedWorkflowId || addToolMutation.isPending,
}}
/>
</ChipModal>
<ChipModal
@@ -900,25 +881,19 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
</div>
</ChipModalField>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowEditServer(false)}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSaveServerEdit}
disabled={
<ChipModalFooter
onCancel={() => setShowEditServer(false)}
primaryAction={{
label: updateServerMutation.isPending ? 'Saving...' : 'Save',
onClick: handleSaveServerEdit,
disabled:
!editServerName.trim() ||
updateServerMutation.isPending ||
(editServerName === server.name &&
editServerDescription === (server.description || '') &&
editServerIsPublic === server.isPublic)
}
>
{updateServerMutation.isPending ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
editServerIsPublic === server.isPublic),
}}
/>
</ChipModal>
<CreateApiKeyModal
@@ -1090,28 +1065,20 @@ export function WorkflowMcpServers() {
isLoadingWorkflows={isLoadingWorkflows}
/>
<ChipModal
<ChipConfirmModal
open={!!serverToDelete}
onOpenChange={(open) => !open && setServerToDelete(null)}
srTitle='Delete MCP Server'
>
<ChipModalHeader showDivider={false}>Delete MCP Server</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete MCP Server'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{serverToDelete?.name}</span>?
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setServerToDelete(null)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDeleteServer}>
Delete
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{ label: 'Delete', onClick: handleDeleteServer }}
/>
</>
)
}
@@ -3,7 +3,6 @@
import { useCallback, useMemo, useState } from 'react'
import { useParams } from 'next/navigation'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -222,21 +221,22 @@ export function SkillModal({
{showFooter && (
<ChipModalFooter
leading={
isEditing && onDelete ? (
<Chip variant='destructive' flush onClick={() => onDelete(initialValues.id)}>
Delete
</Chip>
) : undefined
onCancel={() => onOpenChange(false)}
secondaryAction={
isEditing && onDelete
? {
label: 'Delete',
onClick: () => onDelete(initialValues.id),
variant: 'destructive',
}
: undefined
}
>
<Chip variant='filled' flush onClick={() => onOpenChange(false)}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleSave} disabled={saving || !hasChanges}>
{saving ? 'Saving...' : isEditing ? 'Update' : 'Create'}
</Chip>
</ChipModalFooter>
primaryAction={{
label: saving ? 'Saving...' : isEditing ? 'Update' : 'Create',
onClick: handleSave,
disabled: saving || !hasChanges,
}}
/>
)}
</ChipModal>
)
@@ -5,15 +5,7 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { ArrowRight, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Chip,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Search,
} from '@/components/emcn'
import { Chip, ChipConfirmModal, ChipInput, Search } from '@/components/emcn'
import { SkillTile } from '@/app/workspace/[workspaceId]/components'
import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header'
import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore'
@@ -200,24 +192,23 @@ export function Skills() {
initialValues={editingSkill ?? undefined}
/>
<ChipModal open={showDeleteDialog} onOpenChange={setShowDeleteDialog} srTitle='Delete Skill'>
<ChipModalHeader showDivider={false}>Delete Skill</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
srTitle='Delete Skill'
title='Delete Skill'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{skillToDelete?.name}</span>?{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowDeleteDialog(false)}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDeleteSkill}>
Delete
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDeleteSkill,
}}
/>
</div>
)
}
@@ -6,7 +6,7 @@ import { getErrorMessage } from '@sim/utils/errors'
import { useParams } from 'next/navigation'
import {
Checkbox,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -103,8 +103,8 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
deleteRowMutation.isPending ||
deleteRowsMutation.isPending
const handleFormSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const handleFormSubmit = async (e?: React.FormEvent) => {
e?.preventDefault()
setError(null)
try {
@@ -153,38 +153,33 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
const isSingleRow = deleteCount === 1
return (
<ChipModal
<ChipConfirmModal
open={isOpen}
onOpenChange={handleClose}
srTitle={`Delete ${isSingleRow ? 'Row' : `${deleteCount} Rows`}`}
>
<ChipModalHeader showDivider={false}>
Delete {isSingleRow ? 'Row' : `${deleteCount} Rows`}
</ChipModalHeader>
<ChipModalBody>
{error && (
<div className='rounded-lg border border-[var(--status-error-border)] bg-[var(--status-error-bg)] px-3.5 py-3 text-[var(--status-error-text)] text-small'>
{error}
</div>
)}
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title={`Delete ${isSingleRow ? 'Row' : `${deleteCount} Rows`}`}
description={
<>
{error && (
<span className='mb-3 block rounded-lg border border-[var(--status-error-border)] bg-[var(--status-error-bg)] px-3.5 py-3 text-[var(--status-error-text)] text-small'>
{error}
</span>
)}
Are you sure you want to delete {isSingleRow ? 'this row' : `these ${deleteCount} rows`}
?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all data in {isSingleRow ? 'this row' : 'these rows'}.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose} disabled={isSubmitting}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDelete} disabled={isSubmitting}>
{isSubmitting ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: isSubmitting,
pendingLabel: 'Deleting...',
}}
/>
)
}
@@ -207,6 +202,7 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
</ChipModalHeader>
<ChipModalBody className='max-h-[60vh] overflow-y-auto'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-4'>
<button type='submit' hidden disabled={isSubmitting} />
<ErrorMessage error={error} />
{columns.map((column) => (
@@ -219,20 +215,21 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
))}
</form>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose} disabled={isSubmitting}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleFormSubmit} disabled={isSubmitting}>
{isSubmitting
<ChipModalFooter
onCancel={handleClose}
cancelDisabled={isSubmitting}
primaryAction={{
label: isSubmitting
? isAddMode
? 'Adding...'
: 'Updating...'
: isAddMode
? 'Add Row'
: 'Update Row'}
</Chip>
</ChipModalFooter>
: 'Update Row',
onClick: handleFormSubmit,
disabled: isSubmitting,
}}
/>
</ChipModal>
)
}
@@ -4,14 +4,7 @@ import { useCallback, useMemo, useReducer, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams, useRouter } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
toast,
} from '@/components/emcn'
import { ChipConfirmModal, toast } from '@/components/emcn'
import { Download, Pencil, Table as TableIcon, Trash, Upload } from '@/components/emcn/icons'
import type { RunLimit, RunMode } from '@/lib/api/contracts/tables'
import { captureEvent } from '@/lib/posthog/client'
@@ -515,6 +508,13 @@ export function Table({
}
}
const handleConfirmDeleteColumns = () => {
if (!deletingColumns) return
const names = deletingColumns
setDeletingColumns(null)
confirmDeleteColumnsSinkRef.current?.(names)
}
const columnConfig = slideout.kind === 'column' ? slideout.config : null
const workflowConfig = slideout.kind === 'workflow' ? slideout.config : null
const executionId = slideout.kind === 'execution' ? slideout.executionId : null
@@ -709,7 +709,7 @@ export function Table({
}}
/>
)}
<ChipModal
<ChipConfirmModal
open={deletingColumns !== null}
onOpenChange={(open) => {
if (!open) setDeletingColumns(null)
@@ -719,14 +719,13 @@ export function Table({
? `Delete ${deletingColumns.length} Columns`
: 'Delete Column'
}
>
<ChipModalHeader showDivider={false}>
{deletingColumns && deletingColumns.length > 1
title={
deletingColumns && deletingColumns.length > 1
? `Delete ${deletingColumns.length} Columns`
: 'Delete Column'}
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
: 'Delete Column'
}
description={
<>
{deletingColumns && deletingColumns.length > 1 ? (
<>
Are you sure you want to delete{' '}
@@ -749,62 +748,36 @@ export function Table({
{deletingColumns && deletingColumns.length > 1 ? 'these columns' : 'this column'}.
</span>{' '}
You can undo this action.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setDeletingColumns(null)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={() => {
if (!deletingColumns) return
const names = deletingColumns
setDeletingColumns(null)
confirmDeleteColumnsSinkRef.current?.(names)
}}
>
Delete
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleConfirmDeleteColumns,
}}
/>
{!embedded && (
<ChipModal
<ChipConfirmModal
open={showDeleteTableConfirm}
onOpenChange={setShowDeleteTableConfirm}
srTitle='Delete Table'
>
<ChipModalHeader showDivider={false}>Delete Table</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Table'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{tableData?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>
All {tableData?.rowCount ?? 0} rows will be removed.
</span>{' '}
You can restore it from Recently Deleted in Settings.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteTableConfirm(false)}
disabled={deleteTableMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDeleteTable}
disabled={deleteTableMutation.isPending}
>
{deleteTableMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDeleteTable,
pending: deleteTableMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
)}
</div>
)
@@ -7,7 +7,6 @@ import {
Button,
ButtonGroup,
ButtonGroupItem,
Chip,
ChipCombobox,
ChipModal,
ChipModalBody,
@@ -478,30 +477,22 @@ export function ImportCsvDialog({
</div>
)}
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => onOpenChange(false)}
disabled={importMutation.isPending}
>
Cancel
</Chip>
<Chip
variant={mode === 'replace' ? 'destructive' : 'primary'}
flush
onClick={handleSubmit}
disabled={!canSubmit}
>
{importMutation.isPending
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={importMutation.isPending}
primaryAction={{
label: importMutation.isPending
? mode === 'replace'
? 'Replacing...'
: 'Importing...'
: mode === 'replace'
? 'Replace rows'
: 'Append rows'}
</Chip>
</ChipModalFooter>
: 'Append rows',
onClick: handleSubmit,
disabled: !canSubmit,
variant: mode === 'replace' ? 'destructive' : 'primary',
}}
/>
</ChipModal>
)
}
@@ -4,16 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams, useRouter } from 'next/navigation'
import type { ComboboxOption } from '@/components/emcn'
import {
Chip,
ChipCombobox,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
toast,
Upload,
} from '@/components/emcn'
import { ChipCombobox, ChipConfirmModal, toast, Upload } from '@/components/emcn'
import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons'
import type { TableDefinition } from '@/lib/table'
import { generateUniqueTableName } from '@/lib/table/constants'
@@ -591,39 +582,31 @@ export function Tables() {
/>
)}
<ChipModal
<ChipConfirmModal
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
onOpenChange={(open) => {
setIsDeleteDialogOpen(open)
if (!open) setActiveTable(null)
}}
srTitle='Delete Table'
>
<ChipModalHeader showDivider={false}>Delete Table</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Table'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{activeTable?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>
All {activeTable?.rowCount} rows will be removed.
</span>{' '}
You can restore it from Recently Deleted in Settings.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setIsDeleteDialogOpen(false)
setActiveTable(null)
}}
disabled={deleteTable.isPending}
>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDelete} disabled={deleteTable.isPending}>
{deleteTable.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteTable.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -10,7 +10,6 @@ export type CommandId =
| 'accept-diff-changes'
| 'add-agent'
| 'add-workflow'
| 'add-task'
| 'goto-logs'
| 'open-search'
| 'open-workflow-search-replace'
@@ -59,11 +58,6 @@ export const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
shortcut: 'Mod+Shift+P',
allowInEditable: false,
},
'add-task': {
id: 'add-task',
shortcut: 'Mod+Shift+K',
allowInEditable: false,
},
'goto-logs': {
id: 'goto-logs',
shortcut: 'Mod+L',
@@ -8,12 +8,8 @@ import {
Button,
ButtonGroup,
ButtonGroupItem,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
Input,
Label,
Loader,
@@ -414,14 +410,13 @@ export function ChatDeploy({
</div>
</form>
<ChipModal
<ChipConfirmModal
open={showDeleteConfirmation}
onOpenChange={setShowDeleteConfirmation}
srTitle='Delete Chat'
>
<ChipModalHeader showDivider={false}>Delete Chat</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Chat'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{existingChat?.title || 'this chat'}
@@ -432,27 +427,15 @@ export function ChatDeploy({
make it unavailable to all users.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteConfirmation(false)}
disabled={deleteChatMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDelete}
disabled={deleteChatMutation.isPending}
>
{deleteChatMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteChatMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -7,7 +7,7 @@ import {
Badge,
ButtonGroup,
ButtonGroupItem,
Chip,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -273,36 +273,29 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
<ChipModalError>{saveError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleCloseAttempt} disabled={isSaving}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleSave} disabled={!hasChanges || isSaving}>
Save
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleCloseAttempt}
cancelDisabled={isSaving}
primaryAction={{
label: 'Save',
onClick: handleSave,
disabled: !hasChanges || isSaving,
}}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={showUnsavedChangesAlert}
onOpenChange={setShowUnsavedChangesAlert}
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={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={() => setShowUnsavedChangesAlert(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={handleDiscardChanges}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{
label: 'Discard Changes',
onClick: handleDiscardChanges,
}}
/>
</>
)
}
@@ -3,6 +3,7 @@
import { useRef, useState } from 'react'
import {
Chip,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -133,46 +134,29 @@ export function VersionDescriptionModal({
{updateMutation.error?.message || generateMutation.error?.message}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={handleCloseAttempt}
disabled={updateMutation.isPending || isGenerating}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSave}
disabled={updateMutation.isPending || isGenerating || !hasChanges}
>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleCloseAttempt}
cancelDisabled={updateMutation.isPending || isGenerating}
primaryAction={{
label: updateMutation.isPending ? 'Saving...' : 'Save',
onClick: handleSave,
disabled: updateMutation.isPending || isGenerating || !hasChanges,
}}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={showUnsavedChangesAlert}
onOpenChange={setShowUnsavedChangesAlert}
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={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={() => setShowUnsavedChangesAlert(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={handleDiscardChanges}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{
label: 'Discard Changes',
onClick: handleDiscardChanges,
}}
/>
</>
)
}
@@ -6,11 +6,7 @@ import {
Button,
ButtonGroup,
ButtonGroupItem,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
Expand,
Label,
Modal,
@@ -278,10 +274,13 @@ export function GeneralDeploy({
</div>
</div>
<ChipModal open={showLoadDialog} onOpenChange={setShowLoadDialog} srTitle='Load Deployment'>
<ChipModalHeader showDivider={false}>Load Deployment</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={showLoadDialog}
onOpenChange={setShowLoadDialog}
srTitle='Load Deployment'
title='Load Deployment'
description={
<>
Are you sure you want to load{' '}
<span className='font-medium text-[var(--text-primary)]'>
{versionToLoadInfo?.name || `v${versionToLoad?.version}`}
@@ -290,31 +289,22 @@ export function GeneralDeploy({
<span className='text-[var(--text-error)]'>
This will replace your current workflow with the deployed version.
</span>
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowLoadDialog(false)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={confirmLoadDeployment}
disabled={revertMutation.isPending}
>
Load deployment
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Load deployment',
onClick: confirmLoadDeployment,
pending: revertMutation.isPending,
}}
/>
<ChipModal
<ChipConfirmModal
open={showPromoteDialog}
onOpenChange={setShowPromoteDialog}
srTitle='Promote to live'
>
<ChipModalHeader showDivider={false}>Promote to live</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Promote to live'
description={
<>
Are you sure you want to promote{' '}
<span className='font-medium text-[var(--text-primary)]'>
{versionToPromoteInfo?.name || `v${versionToPromote?.version}`}
@@ -323,22 +313,15 @@ export function GeneralDeploy({
<span className='text-[var(--text-primary)]'>
This version will become the active deployment and serve all API requests.
</span>
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setShowPromoteDialog(false)}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={confirmPromoteToLive}
disabled={isPromotingVersion}
>
Promote to live
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Promote to live',
onClick: confirmPromoteToLive,
variant: 'primary',
pending: isPromotingVersion,
}}
/>
{workflowToShow && (
<Modal open={showExpandedPreview} onOpenChange={setShowExpandedPreview}>
@@ -8,11 +8,7 @@ import { useParams } from 'next/navigation'
import {
Badge,
Button,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
Loader,
Modal,
ModalBody,
@@ -833,45 +829,36 @@ export function DeployModal({
</ModalContent>
</Modal>
<ChipModal
<ChipConfirmModal
open={Boolean(undeployTargetWorkflowId)}
onOpenChange={(nextOpen) => {
if (!nextOpen) setUndeployTargetWorkflowId(null)
}}
srTitle='Undeploy API'
>
<ChipModalHeader showDivider={false}>Undeploy API</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Undeploy API'
description={
<>
Are you sure you want to undeploy this workflow?{' '}
<span className='text-[var(--text-error)]'>
This will remove the API endpoint and make it unavailable to external users.
</span>
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setUndeployTargetWorkflowId(null)}
disabled={isUndeploying}
>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleUndeploy} disabled={isUndeploying}>
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Undeploy',
onClick: handleUndeploy,
pending: isUndeploying,
pendingLabel: 'Undeploying...',
}}
/>
<ChipModal
<ChipConfirmModal
open={showA2aDeleteConfirm}
onOpenChange={setShowA2aDeleteConfirm}
srTitle='Delete A2A Agent'
>
<ChipModalHeader showDivider={false}>Delete A2A Agent</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete A2A Agent'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{existingA2aAgent?.name || 'this agent'}
@@ -881,22 +868,15 @@ export function DeployModal({
This will permanently remove the agent configuration.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowA2aDeleteConfirm(false)}
disabled={a2aSubmitting}
>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleA2aDelete} disabled={a2aSubmitting}>
{a2aSubmitting ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleA2aDelete,
pending: a2aSubmitting,
pendingLabel: 'Deleting...',
}}
/>
<CreateApiKeyModal
open={isCreateKeyModalOpen}
@@ -6,7 +6,7 @@ import { useParams } from 'next/navigation'
import {
Badge,
Button,
Chip,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalFooter,
@@ -1144,112 +1144,79 @@ try {
{activeSection === 'schema' && (
<ChipModalFooter
leading={
isEditing ? (
<Chip variant='destructive' flush onClick={() => setShowDeleteConfirm(true)}>
Delete
</Chip>
) : undefined
onCancel={handleClose}
secondaryAction={
isEditing
? {
label: 'Delete',
onClick: () => setShowDeleteConfirm(true),
variant: 'destructive',
}
: undefined
}
>
<Chip variant='filled' flush onClick={handleClose}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={() => setActiveSection('code')}
disabled={!isSchemaValid || !!schemaError}
>
Next
</Chip>
</ChipModalFooter>
primaryAction={{
label: 'Next',
onClick: () => setActiveSection('code'),
disabled: !isSchemaValid || !!schemaError,
}}
/>
)}
{activeSection === 'code' && (
<ChipModalFooter
leading={
isEditing ? (
<Chip variant='destructive' flush onClick={() => setShowDeleteConfirm(true)}>
Delete
</Chip>
) : (
<Chip variant='filled' flush onClick={() => setActiveSection('schema')}>
Back
</Chip>
)
onCancel={handleClose}
secondaryAction={
isEditing
? {
label: 'Delete',
onClick: () => setShowDeleteConfirm(true),
variant: 'destructive',
}
: { label: 'Back', onClick: () => setActiveSection('schema') }
}
>
<Chip variant='filled' flush onClick={handleClose}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSave}
disabled={!isSchemaValid || !!schemaError || !hasChanges}
>
{isEditing ? 'Update Tool' : 'Save Tool'}
</Chip>
</ChipModalFooter>
primaryAction={{
label: isEditing ? 'Update Tool' : 'Save Tool',
onClick: handleSave,
disabled: !isSchemaValid || !!schemaError || !hasChanges,
}}
/>
)}
</ChipModal>
<ChipModal
<ChipConfirmModal
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
srTitle='Delete Custom Tool'
>
<ChipModalHeader showDivider={false}>Delete Custom Tool</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Custom Tool'
description={
<>
<span className='text-[var(--text-error)]'>
This will permanently delete the tool and remove it from any workflows that are using
it.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteConfirm(false)}
disabled={deleteToolMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleDelete}
disabled={deleteToolMutation.isPending}
>
{deleteToolMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDelete,
pending: deleteToolMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
<ChipModal
<ChipConfirmModal
open={showDiscardAlert}
onOpenChange={setShowDiscardAlert}
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={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>
</ChipModal>
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{
label: 'Discard Changes',
onClick: handleConfirmDiscard,
}}
/>
</>
)
}
@@ -12,11 +12,7 @@ import {
BubbleChatClose,
BubbleChatPreview,
Button,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -931,14 +927,13 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
</aside>
{/* Delete Confirmation Modal */}
<ChipModal
<ChipConfirmModal
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
srTitle='Delete Workflow'
>
<ChipModalHeader showDivider={false}>Delete Workflow</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Workflow'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{currentWorkflow?.name ?? 'this workflow'}
@@ -948,22 +943,15 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
All associated blocks, executions, and configuration will be removed.
</span>{' '}
You can restore it from Recently Deleted in Settings.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setIsDeleteModalOpen(false)}
disabled={isDeleting}
>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={handleDeleteWorkflow} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleDeleteWorkflow,
pending: isDeleting,
pendingLabel: 'Deleting...',
}}
/>
{/* Floating Variables Modal */}
<Variables readOnly={workflowLocked} />
@@ -10,7 +10,6 @@ import Image from 'next/image'
import { Controller, useForm } from 'react-hook-form'
import { z } from 'zod'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
@@ -255,6 +254,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
<ChipModalHeader onClose={() => onOpenChange(false)}>Help &amp; support</ChipModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
<button type='submit' hidden disabled={helpMutation.isPending || isProcessing} />
<ChipModalBody className='min-h-0'>
<div
ref={scrollContainerRef}
@@ -348,31 +348,21 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
</div>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => onOpenChange(false)}
type='button'
disabled={helpMutation.isPending}
>
Cancel
</Chip>
<Chip
type='submit'
variant='primary'
flush
disabled={helpMutation.isPending || isProcessing}
>
{helpMutation.isPending
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={helpMutation.isPending}
primaryAction={{
label: helpMutation.isPending
? 'Submitting...'
: submitStatus === 'error'
? 'Error'
: submitStatus === 'success'
? 'Success'
: 'Submit'}
</Chip>
</ChipModalFooter>
: 'Submit',
onClick: () => void handleSubmit(onSubmit)(),
disabled: helpMutation.isPending || isProcessing,
}}
/>
</form>
</ChipModal>
)
@@ -3,15 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useParams, usePathname, useRouter } from 'next/navigation'
import {
ChevronDown,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
chipVariants,
} from '@/components/emcn'
import { ChevronDown, ChipConfirmModal, chipVariants } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionAccessState } from '@/lib/billing/client'
import { isEnterprise } from '@/lib/billing/plan-helpers'
@@ -366,26 +358,18 @@ export function SettingsSidebar({
</div>
</div>
<ChipModal
<ChipConfirmModal
open={showDiscardDialog}
onOpenChange={(open) => !open && handleCancelDiscard()}
srTitle='Unsaved changes'
>
<ChipModalHeader showDivider={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={handleCancelDiscard}>
Keep editing
</Chip>
<Chip variant='destructive' flush onClick={handleConfirmDiscard}>
Discard changes
</Chip>
</ChipModalFooter>
</ChipModal>
title='Unsaved changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{
label: 'Discard changes',
onClick: handleConfirmDiscard,
}}
/>
</>
)
}
@@ -1,14 +1,7 @@
'use client'
import { useState } from 'react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
} from '@/components/emcn'
import { ChipConfirmModal, ChipModalField } from '@/components/emcn'
interface DeleteModalProps {
/**
@@ -252,38 +245,41 @@ export function DeleteModal({
}
return (
<ChipModal open={isOpen} onOpenChange={handleClose} srTitle={title}>
<ChipModalHeader showDivider={false}>{title}</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={isOpen}
onOpenChange={handleClose}
srTitle={title}
title={title}
description={
<>
{renderDescription()}{' '}
{restorableTypes.has(itemType)
? 'You can restore it from Recently deleted in Settings.'
: 'This action cannot be undone.'}
</p>
{isWorkspace && workspaceName && (
<ChipModalField
type='input'
title={
<>
Type <span className='font-medium text-[var(--text-primary)]'>{workspaceName}</span>{' '}
to confirm
</>
}
value={confirmationText}
onChange={setConfirmationText}
placeholder={workspaceName}
/>
)}
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleClose} disabled={isDeleting}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={onConfirm} disabled={isDeleting || !isConfirmed}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: onConfirm,
pending: isDeleting,
pendingLabel: 'Deleting...',
disabled: !isConfirmed,
}}
>
{isWorkspace && workspaceName && (
<ChipModalField
type='input'
title={
<>
Type <span className='font-medium text-[var(--text-primary)]'>{workspaceName}</span>{' '}
to confirm
</>
}
value={confirmationText}
onChange={setConfirmationText}
placeholder={workspaceName}
/>
)}
</ChipConfirmModal>
)
}
@@ -2,7 +2,6 @@
import { useEffect, useState } from 'react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -65,19 +64,15 @@ export function CreateWorkspaceModal({
/>
<ChipModalError>{undefined}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isCreating}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={() => void handleSubmit()}
disabled={!name.trim() || isCreating}
>
{isCreating ? 'Creating...' : 'Create'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={isCreating}
primaryAction={{
label: isCreating ? 'Creating...' : 'Create',
onClick: () => void handleSubmit(),
disabled: !name.trim() || isCreating,
}}
/>
</ChipModal>
)
}
@@ -4,7 +4,6 @@ import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
@@ -179,11 +178,15 @@ export function InviteModal({
onChange={(role) => setInviteRole(role as PermissionType)}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='primary' onClick={handleSendInvites} disabled={isSendDisabled}>
{isSubmitting ? 'Sending...' : 'Send invites'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
cancelDisabled={isSubmitting}
primaryAction={{
label: isSubmitting ? 'Sending...' : 'Send invites',
onClick: handleSendInvites,
disabled: isSendDisabled,
}}
/>
</ChipModal>
)
}
@@ -6,10 +6,7 @@ import { MoreHorizontal } from 'lucide-react'
import {
ChevronDown,
Chip,
ChipModal,
ChipModalBody,
ChipModalFooter,
ChipModalHeader,
ChipConfirmModal,
chipVariants,
DropdownMenu,
DropdownMenuContent,
@@ -684,39 +681,26 @@ function WorkspaceHeaderImpl({
itemType='workspace'
itemName={deleteTarget?.name}
/>
<ChipModal
<ChipConfirmModal
open={isLeaveModalOpen}
onOpenChange={() => setIsLeaveModalOpen(false)}
srTitle='Leave workspace'
>
<ChipModalHeader showDivider={false}>Leave workspace</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Leave workspace'
description={
<>
Are you sure you want to leave{' '}
<span className='font-medium text-[var(--text-primary)]'>{leaveTarget?.name}</span>? You
will lose access to all workflows and data in this workspace. This action cannot be
undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setIsLeaveModalOpen(false)}
disabled={isLeavingWorkspace}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleLeaveWorkspace}
disabled={isLeavingWorkspace}
>
{isLeavingWorkspace ? 'Leaving...' : 'Leave workspace'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Leave workspace',
onClick: handleLeaveWorkspace,
pending: isLeavingWorkspace,
pendingLabel: 'Leaving...',
}}
/>
</div>
)
}
@@ -90,7 +90,6 @@ import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useFolderMap, useFolders } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import {
useCreateMothershipChat,
useDeleteMothershipChat,
useDeleteMothershipChats,
useMarkMothershipChatRead,
@@ -109,7 +108,6 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useFolderStore } from '@/stores/folders/store'
import { useSearchModalStore } from '@/stores/modals/search/store'
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import { useProvidersStore } from '@/stores/providers'
import { useSidebarStore } from '@/stores/sidebar/store'
@@ -589,7 +587,6 @@ export const Sidebar = memo(function Sidebar() {
}
}, [activeNavItemHref])
const createChatMutation = useCreateMothershipChat(workspaceId)
const deleteChatMutation = useDeleteMothershipChat(workspaceId)
const deleteChatsMutation = useDeleteMothershipChats(workspaceId)
const markChatReadMutation = useMarkMothershipChatRead(workspaceId)
@@ -608,7 +605,6 @@ export const Sidebar = memo(function Sidebar() {
preventDismiss: preventChatDismiss,
} = useContextMenu()
const isCreatingChatRef = useRef(false)
const contextMenuSelectionRef = useRef<{ chatIds: string[]; names: string[] }>({
chatIds: [],
names: [],
@@ -1134,25 +1130,6 @@ export const Sidebar = memo(function Sidebar() {
onSelect: handleCreateWorkflow,
}
const handleNewChat = useCallback(async () => {
if (!workspaceId || isCreatingChatRef.current) return
isCreatingChatRef.current = true
try {
const { id } = await createChatMutation.mutateAsync()
useMothershipDraftsStore.getState().clearDraft(`${workspaceId}:new`)
navigateToPage(`/workspace/${workspaceId}/chat/${id}`)
} catch {
navigateToPage(`/workspace/${workspaceId}/home`)
} finally {
isCreatingChatRef.current = false
}
}, [workspaceId, navigateToPage])
const chatsPrimaryAction = {
label: 'New chat',
onSelect: handleNewChat,
}
const handleSeeMoreChats = useCallback(() => setVisibleChatCount((prev) => prev + 5), [])
const handleSeeLessChats = useCallback(() => setVisibleChatCount(5), [])
@@ -1241,12 +1218,6 @@ export const Sidebar = memo(function Sidebar() {
handleCreateWorkflow()
},
},
{
id: 'add-task',
handler: () => {
handleNewChat()
},
},
])
)
@@ -1341,27 +1312,6 @@ export const Sidebar = memo(function Sidebar() {
<div className='chats-section flex flex-shrink-0 flex-col'>
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-4'>
<div className='text-[var(--text-muted)] text-small'>Chats</div>
{!isCollapsed && (
<div className='flex items-center justify-center gap-2'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='quiet'
className='h-[18px] w-[18px] rounded-sm p-0'
onClick={handleNewChat}
disabled={createChatMutation.isPending}
>
<Plus className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Shortcut keys={isMac ? '⌘⇧K' : 'Ctrl+Shift+K'}>
New chat
</Tooltip.Shortcut>
</Tooltip.Content>
</Tooltip.Root>
</div>
)}
</div>
{isCollapsed ? (
<CollapsedSidebarMenu
@@ -1369,7 +1319,6 @@ export const Sidebar = memo(function Sidebar() {
hover={chatsHover}
ariaLabel='Chats'
className='mt-2'
primaryAction={chatsPrimaryAction}
>
{chatsLoading ? (
<DropdownMenuItem disabled>
+35
View File
@@ -1,3 +1,4 @@
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { getCanonicalBlocksByCategory } from '@/blocks/registry'
import type { BlockIcon } from '@/blocks/types'
@@ -82,6 +83,40 @@ export function getIntegrationMatcher(): IntegrationMatcher {
return cachedMatcher
}
/**
* Rewrites bare integration names in `text` to `@`-mention form (`Slack` →
* `@Slack`) so they chip when the prompt is populated into the chat input —
* the auto-mention pipeline deliberately ignores un-prefixed names (mention
* treatment is strictly opt-in via a token-starting `@`), so curated prompts
* that should chip must opt in here. Idempotent: names already prefixed with
* `@` are left untouched.
*/
export function mentionifyIntegrations(text: string): string {
const { regex } = getIntegrationMatcher()
if (!regex || !text) return text
return text.replace(regex, (match: string, _name: string, offset: number) =>
offset > 0 && text[offset - 1] === '@' ? match : `@${match}`
)
}
/**
* Stores a CURATED prompt (a suggested action, template, or showcase CTA — never
* free-form user prose) for the home chat input to consume after navigation,
* running it through {@link mentionifyIntegrations} first so its integration
* names chip with brand icons on arrival.
*
* This is the single seam every curated-prompt producer that hands off via
* {@link LandingPromptStorage} must use — it pairs the rewrite with the store so
* a new producer cannot forget the rewrite (the regression class this guards
* against). User-typed prose (e.g. the landing preview panel) intentionally
* bypasses this and calls {@link LandingPromptStorage.store} directly, since
* bare integration names in prose must never be auto-chipped (the scunthorpe
* problem).
*/
export function storeCuratedPrompt(prompt: string): boolean {
return LandingPromptStorage.store(mentionifyIntegrations(prompt))
}
/**
* Lazily builds (once per session) and returns all known integrations sorted
* alphabetically by display name for menu rendering. Shares the underlying
@@ -7,10 +7,10 @@
* / `ModalFooter` — drop your controls in as children.
*
* Body items are declared via the polymorphic `ChipModalField`. Each field
* picks a `type` (`'input'`, `'email'`, `'textarea'`, `'dropdown'`, or
* `'custom'`) and the field owns all chrome internally — consumers describe
* intent, never styling. Custom is the escape hatch for arbitrary content
* (e.g. an `InfoCard`, a `TagInput`).
* picks a `type` (`'input'`, `'email'`, `'textarea'`, `'dropdown'`, `'file'`,
* `'emails'`, or `'custom'`) and the field owns all chrome internally —
* consumers describe intent, never styling. Custom is the escape hatch for
* arbitrary content (e.g. an `InfoCard`, a `TagInput`).
*
* @example
* ```tsx
@@ -28,9 +28,10 @@
* <TagInput items={items} onAdd={add} onRemove={remove} variant='block' />
* </ChipModalField>
* </ChipModalBody>
* <ChipModalFooter>
* <Chip variant='primary' onClick={send}>Send invites</Chip>
* </ChipModalFooter>
* <ChipModalFooter
* onCancel={() => setOpen(false)}
* primaryAction={{ label: 'Send invites', onClick: send }}
* />
* </ChipModal>
* ```
*/
@@ -40,6 +41,7 @@
import * as React from 'react'
import { X } from 'lucide-react'
import { Button } from '@/components/emcn/components/button/button'
import { Chip, type ChipProps } from '@/components/emcn/components/chip/chip'
import {
ChipDropdown,
type ChipDropdownOption,
@@ -124,34 +126,19 @@ ChipModal.displayName = 'ChipModal'
export interface ChipModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
/** Optional leading icon. Pass `null`/omit for a title-only header. */
icon?: React.ComponentType<{ className?: string }> | null
/** When provided, renders a trailing close button. */
onClose?: () => void
/** Invoked when the trailing close button is activated. Always rendered. */
onClose: () => void
/** Accessible label for the close button. */
closeAriaLabel?: string
/**
* Whether to render the divider line below the header title.
* Set to `false` for compact confirmations (destructive / disconnect flows)
* where the body is plain prose rather than labeled fields.
* @default true
*/
showDivider?: boolean
}
/**
* Header row with optional leading icon, title, and optional trailing close.
* Renders an inset divider below the title to match the panel's rhythm.
* Header row with optional leading icon, title, and a trailing close button.
* Always renders an inset divider below the title to match the panel's rhythm.
*/
const ChipModalHeader = React.forwardRef<HTMLDivElement, ChipModalHeaderProps>(
(
{
className,
children,
icon: Icon = null,
onClose,
closeAriaLabel = 'Close',
showDivider = true,
...props
},
{ className, children, icon: Icon = null, onClose, closeAriaLabel = 'Close', ...props },
ref
) => (
<div ref={ref} className={cn('flex flex-col', className)} {...props}>
@@ -160,19 +147,17 @@ const ChipModalHeader = React.forwardRef<HTMLDivElement, ChipModalHeaderProps>(
{Icon ? <Icon className='size-[12px] flex-shrink-0 text-[var(--text-icon)]' /> : null}
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{children}</span>
</div>
{onClose ? (
<Button
type='button'
variant='ghost'
onClick={onClose}
className='relative size-[14px] flex-shrink-0 p-0 before:absolute before:inset-[-14px] before:content-[""]'
>
<X className='size-[14px] text-[var(--text-icon)]' />
<span className='sr-only'>{closeAriaLabel}</span>
</Button>
) : null}
<Button
type='button'
variant='ghost'
onClick={onClose}
className='relative size-[14px] flex-shrink-0 p-0 before:absolute before:inset-[-14px] before:content-[""]'
>
<X className='size-[14px] text-[var(--text-icon)]' />
<span className='sr-only'>{closeAriaLabel}</span>
</Button>
</div>
{showDivider && <ChipModalSeparator className='mt-3' />}
<ChipModalSeparator className='mt-3' />
</div>
)
)
@@ -751,42 +736,274 @@ function ChipModalFileControl({
)
}
export interface ChipModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* A single footer action button. Rendered internally as a {@link Chip} so every
* modal footer stays visually identical — callers describe intent (label,
* handler, optional variant), never JSX or chrome. Encode pending state in the
* `label` and `disabled` (e.g. `saving ? 'Saving...' : 'Save'`).
*/
export interface ChipModalFooterAction {
/** Button label. */
label: React.ReactNode
/** Click handler. */
onClick: () => void
/** Disables the button. */
disabled?: boolean
/**
* Optional leading slot rendered on the left side of the footer — use for
* a destructive secondary action (e.g. a Delete button in an edit flow).
* When provided, the footer switches to `justify-between` automatically.
* Chip variant, restricted to the three footer-appropriate options so a
* footer can never drift from the design system.
* @default 'primary' for `primaryAction`, 'filled' for `secondaryAction`
*/
leading?: React.ReactNode
variant?: Extract<ChipProps['variant'], 'primary' | 'destructive' | 'filled'>
}
export interface ChipModalFooterProps {
/**
* Dismiss handler for the always-present Cancel button. Like the header's
* close (X), Cancel is structural — it always reads "Cancel" and there is no
* prop to relabel or remove it. Its enabled state can be controlled via
* {@link ChipModalFooterProps.cancelDisabled}.
*/
onCancel: () => void
/**
* Disables the Cancel button. Set this while a primary/secondary action is
* in flight (e.g. an async delete or save) so the user cannot dismiss the
* modal and assume the operation was aborted while the mutation keeps running.
* @default false
*/
cancelDisabled?: boolean
/** Primary action, anchored bottom-right (e.g. Save, Create, Delete). */
primaryAction: ChipModalFooterAction
/**
* Optional auxiliary action docked to the far-left, opposite the
* Cancel/primary cluster — e.g. Delete in an edit flow or Back in a wizard.
*/
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.
* Shared footer chrome — the inset separator plus the tinted `--surface-3` bar
* with the standard gutter. Single source of truth so {@link ChipModalFooter}
* and {@link ChipConfirmModal} render an identical footer surface. `leftSlot`
* docks to the far-left (opposite the right-anchored button cluster); when
* omitted the cluster is right-justified.
*/
const ChipModalFooter = React.forwardRef<HTMLDivElement, ChipModalFooterProps>(
({ className, leading, children, ...props }, ref) => (
function ChipModalFooterShell({
leftSlot,
children,
}: {
leftSlot?: React.ReactNode
children: React.ReactNode
}) {
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
leftSlot ? 'justify-between' : 'justify-end'
)}
{...props}
>
{leading && <div>{leading}</div>}
{leftSlot ?? null}
<div className='flex gap-2'>{children}</div>
</div>
</div>
)
)
}
/**
* 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.
*
* For "are you sure?" confirmations, reach for {@link ChipConfirmModal} instead
* — a confirmation's dismiss button is a named decision ("Keep editing"), not
* the structural Cancel this footer guarantees.
*/
function ChipModalFooter({
onCancel,
cancelDisabled,
primaryAction,
secondaryAction,
}: ChipModalFooterProps) {
return (
<ChipModalFooterShell
leftSlot={
secondaryAction ? (
<Chip
variant={secondaryAction.variant ?? 'filled'}
flush
onClick={secondaryAction.onClick}
disabled={secondaryAction.disabled}
>
{secondaryAction.label}
</Chip>
) : undefined
}
>
<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>
</ChipModalFooterShell>
)
}
ChipModalFooter.displayName = 'ChipModalFooter'
/**
* The confirming action of a {@link ChipConfirmModal}. Unlike a
* {@link ChipModalFooterAction}, pending state is first-class: set `pending`
* while the async action runs and the primitive disables BOTH buttons (so the
* dismiss can't be clicked mid-mutation) and swaps in `pendingLabel`.
*/
export interface ChipConfirmAction {
/** Resting button label (e.g. `'Delete'`). */
label: string
/** Invoked when the user confirms. */
onClick: () => void
/**
* Chip variant. Confirmations are usually destructive, so this defaults to
* `'destructive'`; use `'primary'` for a non-destructive confirm (e.g.
* "Promote to live").
* @default 'destructive'
*/
variant?: Extract<ChipProps['variant'], 'primary' | 'destructive' | 'filled'>
/**
* Marks the action in-flight: disables both the confirm and dismiss buttons
* and, when {@link ChipConfirmAction.pendingLabel} is set, shows it in place
* of `label`.
*/
pending?: boolean
/** Label shown while `pending` (e.g. `'Deleting...'`). Falls back to `label`. */
pendingLabel?: string
/** Additional disable condition independent of `pending` (e.g. an unmet "type to confirm"). */
disabled?: boolean
}
export interface ChipConfirmModalProps {
/** Controlled open state. */
open: boolean
/**
* Open-state change handler and the SINGLE dismiss path — the header close
* (X), the dismiss button, Escape, and overlay click all route through
* `onOpenChange(false)`. Put any teardown (clearing the targeted row, etc.)
* here so no dismiss path can skip it.
*/
onOpenChange: (open: boolean) => void
/** Title rendered in the header. */
title: React.ReactNode
/** Optional leading header icon. */
icon?: React.ComponentType<{ className?: string }> | null
/**
* Confirmation copy. Rendered with the standard secondary body styling, so
* pass a plain string or inline `<>…<span className='…text-primary…'>name</span>…</>`
* for emphasis — no wrapping `<p>` needed.
*/
description?: React.ReactNode
/**
* Extra body content below `description` — e.g. a "type the name to confirm"
* {@link ChipModalField}. Most confirmations omit this.
*/
children?: React.ReactNode
/** The confirming action (Delete / Discard / Remove …). */
confirm: ChipConfirmAction
/**
* Label for the dismiss button. In a confirmation the dismiss button is a
* named decision, so this is honest API (unlike a form footer's structural
* Cancel). Defaults to `'Cancel'`; pass `'Keep editing'` for unsaved-changes.
* @default 'Cancel'
*/
dismissLabel?: string
/**
* Panel width. Confirmations are compact, so defaults to `'sm'`.
* @default 'sm'
*/
size?: ChipModalProps['size']
/** Screen-reader title; defaults to the string form of `title` when omitted. */
srTitle?: string
}
/**
* Compact "are you sure?" confirmation dialog. Models the confirmation button
* grammar directly — a named dismiss decision plus a (usually destructive)
* confirm — instead of bending the form footer's structural Cancel to fit.
*
* The primitive owns the safety rails that every hand-rolled confirm modal had
* to remember: a single dismiss path shared by the header X / dismiss button /
* Escape (so teardown can't desync), and disabling dismiss while the confirm is
* in flight. Drop richer body content (a "type to confirm" field) in as
* `children`.
*
* @example
* ```tsx
* <ChipConfirmModal
* open={open}
* onOpenChange={(next) => { if (!next) setTarget(null); setOpen(next) }}
* title='Delete API key'
* description={<>Deleting <strong>{target?.name}</strong> revokes access immediately.</>}
* confirm={{ label: 'Delete', onClick: handleDelete, pending: isDeleting, pendingLabel: 'Deleting...' }}
* />
* ```
*/
function ChipConfirmModal({
open,
onOpenChange,
title,
icon,
description,
children,
confirm,
dismissLabel = 'Cancel',
size = 'sm',
srTitle,
}: ChipConfirmModalProps) {
const dismiss = React.useCallback(() => onOpenChange(false), [onOpenChange])
const confirmLabel = confirm.pending ? (confirm.pendingLabel ?? confirm.label) : confirm.label
return (
<ChipModal
open={open}
onOpenChange={onOpenChange}
size={size}
srTitle={srTitle ?? (typeof title === 'string' ? title : 'Confirm')}
>
<ChipModalHeader icon={icon} onClose={dismiss}>
{title}
</ChipModalHeader>
<ChipModalBody>
{description ? (
<p className='px-2 text-[var(--text-secondary)] text-sm'>{description}</p>
) : null}
{children}
</ChipModalBody>
<ChipModalFooterShell>
<Chip variant='filled' flush onClick={dismiss} disabled={confirm.pending}>
{dismissLabel}
</Chip>
<Chip
variant={confirm.variant ?? 'destructive'}
flush
onClick={confirm.onClick}
disabled={confirm.disabled || confirm.pending}
>
{confirmLabel}
</Chip>
</ChipModalFooterShell>
</ChipModal>
)
}
ChipConfirmModal.displayName = 'ChipConfirmModal'
export interface ChipModalErrorProps extends React.HTMLAttributes<HTMLParagraphElement> {
/** Error message. When falsy the component renders nothing. */
children?: React.ReactNode
@@ -824,6 +1041,7 @@ const ChipModalError = React.forwardRef<HTMLParagraphElement, ChipModalErrorProp
ChipModalError.displayName = 'ChipModalError'
export {
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -32,6 +32,9 @@ export {
} from './chip-dropdown/chip-dropdown'
export { ChipInput, type ChipInputProps } from './chip-input/chip-input'
export {
type ChipConfirmAction,
ChipConfirmModal,
type ChipConfirmModalProps,
ChipModal,
ChipModalBody,
type ChipModalDropdownOption,
@@ -41,6 +44,8 @@ export {
ChipModalField,
type ChipModalFieldProps,
ChipModalFooter,
type ChipModalFooterAction,
type ChipModalFooterProps,
ChipModalHeader,
type ChipModalHeaderProps,
type ChipModalProps,
@@ -10,6 +10,7 @@ import {
AvatarImage,
Checkbox,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -209,26 +210,17 @@ function AddMembersModal({
)}
<ChipModalError>{errorMessage}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
setSearchTerm('')
onOpenChange(false)
}}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={onAddMembers}
disabled={selectedMemberIds.size === 0 || isAdding}
>
{isAdding ? 'Adding...' : 'Add Members'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => {
setSearchTerm('')
onOpenChange(false)
}}
primaryAction={{
label: isAdding ? 'Adding...' : 'Add Members',
onClick: onAddMembers,
disabled: selectedMemberIds.size === 0 || isAdding,
}}
/>
</ChipModal>
)
}
@@ -797,6 +789,31 @@ export function AccessControl() {
}
}, [viewingGroup, editingConfig, workspaceId, updatePermissionGroup])
const handleCloseConfigModal = useCallback(() => {
if (hasConfigChanges) {
setShowUnsavedChanges(true)
} else {
setShowConfigModal(false)
setProviderSearchTerm('')
setIntegrationSearchTerm('')
setPlatformSearchTerm('')
}
}, [hasConfigChanges])
const handleDiscardConfig = useCallback(() => {
setShowUnsavedChanges(false)
setShowConfigModal(false)
setEditingConfig(null)
setProviderSearchTerm('')
setIntegrationSearchTerm('')
setPlatformSearchTerm('')
}, [])
const handleSaveConfigFromUnsaved = useCallback(() => {
setShowUnsavedChanges(false)
handleSaveConfig()
}, [handleSaveConfig])
const handleOpenAddMembersModal = useCallback(() => {
setSelectedMemberIds(new Set())
setAddMembersError(null)
@@ -1375,32 +1392,14 @@ export function AccessControl() {
</div>
)}
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => {
if (hasConfigChanges) {
setShowUnsavedChanges(true)
} else {
setShowConfigModal(false)
setProviderSearchTerm('')
setIntegrationSearchTerm('')
setPlatformSearchTerm('')
}
}}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSaveConfig}
disabled={updatePermissionGroup.isPending || !hasConfigChanges}
>
{updatePermissionGroup.isPending ? 'Saving...' : 'Save'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleCloseConfigModal}
primaryAction={{
label: updatePermissionGroup.isPending ? 'Saving...' : 'Save',
onClick: handleSaveConfig,
disabled: updatePermissionGroup.isPending || !hasConfigChanges,
}}
/>
</ChipModal>
<ChipModal
@@ -1409,39 +1408,27 @@ export function AccessControl() {
size='sm'
srTitle='Unsaved Changes'
>
<ChipModalHeader showDivider={false}>Unsaved Changes</ChipModalHeader>
<ChipModalHeader onClose={() => setShowUnsavedChanges(false)}>
Unsaved Changes
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
You have unsaved changes. Do you want to save them before closing?
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='destructive'
flush
onClick={() => {
setShowUnsavedChanges(false)
setShowConfigModal(false)
setEditingConfig(null)
setProviderSearchTerm('')
setIntegrationSearchTerm('')
setPlatformSearchTerm('')
}}
>
Discard Changes
</Chip>
<Chip
variant='primary'
flush
onClick={() => {
setShowUnsavedChanges(false)
handleSaveConfig()
}}
disabled={updatePermissionGroup.isPending}
>
{updatePermissionGroup.isPending ? 'Saving...' : 'Save Changes'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setShowUnsavedChanges(false)}
secondaryAction={{
label: 'Discard Changes',
onClick: handleDiscardConfig,
variant: 'destructive',
}}
primaryAction={{
label: updatePermissionGroup.isPending ? 'Saving...' : 'Save Changes',
onClick: handleSaveConfigFromUnsaved,
disabled: updatePermissionGroup.isPending,
}}
/>
</ChipModal>
<AddMembersModal
@@ -1562,52 +1549,38 @@ export function AccessControl() {
</ChipModalField>
<ChipModalError>{createError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={handleCloseCreateModal}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleCreatePermissionGroup}
disabled={!newGroupName.trim() || createPermissionGroup.isPending}
>
{createPermissionGroup.isPending ? 'Creating...' : 'Create'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleCloseCreateModal}
primaryAction={{
label: createPermissionGroup.isPending ? 'Creating...' : 'Create',
onClick: handleCreatePermissionGroup,
disabled: !newGroupName.trim() || createPermissionGroup.isPending,
}}
/>
</ChipModal>
<ChipModal
<ChipConfirmModal
open={!!deletingGroup}
onOpenChange={() => setDeletingGroup(null)}
size='sm'
srTitle='Delete Permission Group'
>
<ChipModalHeader showDivider={false}>Delete Permission Group</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Permission Group'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{deletingGroup?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>
All members will be removed from this group.
</span>{' '}
This action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => setDeletingGroup(null)}>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={confirmDelete}
disabled={deletePermissionGroup.isPending}
>
{deletePermissionGroup.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: confirmDelete,
pending: deletePermissionGroup.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -9,6 +9,7 @@ import {
Button,
Callout,
Chip,
ChipConfirmModal,
ChipInput,
ChipModal,
ChipModalBody,
@@ -326,38 +327,25 @@ function DrainRow({ drain, organizationId, expanded, onToggleExpand }: DrainRowP
</TableCell>
</TableRow>
)}
<ChipModal
<ChipConfirmModal
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
srTitle='Delete Drain'
>
<ChipModalHeader showDivider={false}>Delete Drain</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
title='Delete Drain'
description={
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{drain.name}</span>? This
action cannot be undone.
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setShowDeleteConfirm(false)}
disabled={deleteMutation.isPending}
>
Cancel
</Chip>
<Chip
variant='destructive'
flush
onClick={handleConfirmDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: handleConfirmDelete,
pending: deleteMutation.isPending,
pendingLabel: 'Deleting...',
}}
/>
</>
)
}
@@ -501,19 +489,15 @@ function CreateDrainModal({ organizationId, onClose }: CreateDrainModalProps) {
<spec.FormFields state={destState} setState={setDestState} />
</section>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={onClose} disabled={createMutation.isPending}>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={!canSubmit || createMutation.isPending}
>
{createMutation.isPending ? 'Creating...' : 'Create drain'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={onClose}
cancelDisabled={createMutation.isPending}
primaryAction={{
label: createMutation.isPending ? 'Creating...' : 'Create drain',
onClick: handleSubmit,
disabled: !canSubmit || createMutation.isPending,
}}
/>
</ChipModal>
)
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long