From ff9d494abcb2b557834a92d99742e82c10b32d1c Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 12 Jun 2026 10:36:17 -0700 Subject: [PATCH 1/2] improvement(emcn): show per-chip error tooltips on invalid email chips --- apps/sim/app/playground/page.tsx | 2 +- .../credential-sets/credential-sets.tsx | 5 ++- .../deploy-modal/components/chat/chat.tsx | 5 ++- .../emcn/components/chip-modal/chip-modal.tsx | 41 +++++++++---------- .../emcn/components/tag-input/tag-input.tsx | 19 ++++++++- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index fe041cff403..26b6d8b9765 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -152,7 +152,7 @@ export default function PlaygroundPage() { const [dateRangeEnd, setDateRangeEnd] = useState('') const [tagItems, setTagItems] = useState([ { value: 'user@example.com', isValid: true }, - { value: 'invalid-email', isValid: false }, + { value: 'invalid-email', isValid: false, error: 'Invalid email format' }, ]) const toggleDarkMode = () => { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx index 70de049d16f..7dfa1bae4a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx @@ -134,7 +134,10 @@ export function CredentialSets() { return false } - setEmailItems((prev) => [...prev, { value: normalized, isValid }]) + setEmailItems((prev) => [ + ...prev, + { value: normalized, isValid, error: isValid ? undefined : validation.reason }, + ]) if (isValid) { setEmailError(null) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 3b7cd45448c..e8c3d33bf7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -655,7 +655,10 @@ function AuthSelector({ setEmailError('') onEmailsChange([...emails, normalized]) } else { - setInvalidEmailItems((prev) => [...prev, { value: normalized, isValid }]) + setInvalidEmailItems((prev) => [ + ...prev, + { value: normalized, isValid, error: validation.reason }, + ]) } return isValid diff --git a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx index d6047abf56c..83eb5fdba10 100644 --- a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx +++ b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx @@ -388,12 +388,14 @@ export interface ChipModalEmailsFieldProps extends ChipModalFieldBaseProps { /** * Optional domain-level validator. Runs AFTER the field's internal format * check passes. Return an error message to reject the email (added as an - * invalid chip and surfaced in the inline banner); return `null` to accept. + * invalid chip whose reason shows in a tooltip on hover); return `null` + * to accept. */ validate?: (email: string) => string | null /** - * External error (e.g. server-side submit failure). Takes precedence over - * the field's internal validation banner while present. + * External error (e.g. server-side submit failure), rendered in the inline + * banner below the field. Per-email rejection reasons are shown on the + * invalid chips themselves, not here. */ error?: React.ReactNode /** Auto-focus the input when the field mounts. */ @@ -561,8 +563,11 @@ function derivePlaceholderWithTags(placeholder: string): string { /** * Internal renderer for {@link ChipModalField} `type='emails'`. Owns the - * chip lifecycle (valid + invalid items, dedupe, inline error banner) and - * lifts only the valid email list up to the consumer via `onChange`. + * chip lifecycle (valid + invalid items, dedupe, per-chip error tooltips) + * and lifts only the valid email list up to the consumer via `onChange`. + * Each rejected entry carries its rejection reason on the chip itself, + * surfaced as a tooltip; the inline banner is reserved for the consumer's + * `error` (e.g. server-side submit failures). */ function ChipModalEmailsControl({ value, @@ -576,7 +581,6 @@ function ChipModalEmailsControl({ errorId, }: ChipModalEmailsFieldProps & { id: string; errorId: string }) { const [items, setItems] = React.useState([]) - const [internalError, setInternalError] = React.useState(null) /** * Reconcile internal `items` with the consumer's `value` when the latter @@ -600,23 +604,24 @@ function ChipModalEmailsControl({ if (!email) return false if (items.some((item) => item.value === email)) return false - if (!quickValidateEmail(email).isValid) { - setItems((prev) => [...prev, { value: email, isValid: false }]) - setInternalError(null) + const formatCheck = quickValidateEmail(email) + if (!formatCheck.isValid) { + setItems((prev) => [ + ...prev, + { value: email, isValid: false, error: formatCheck.reason ?? 'Invalid email format' }, + ]) return false } const reason = validate?.(email) if (reason) { - setItems((prev) => [...prev, { value: email, isValid: false }]) - setInternalError(reason) + setItems((prev) => [...prev, { value: email, isValid: false, error: reason }]) return false } const next = [...items, { value: email, isValid: true }] setItems(next) onChange(next.filter((item) => item.isValid).map((item) => item.value)) - setInternalError(null) return true }, [items, validate, onChange] @@ -630,17 +635,10 @@ function ChipModalEmailsControl({ if (wasValid) { onChange(next.filter((item) => item.isValid).map((item) => item.value)) } - setInternalError(null) }, [items, onChange] ) - const handleInputChange = React.useCallback(() => { - setInternalError(null) - }, []) - - const banner = error ?? internalError - return ( <> - {banner && ( + {error && ( )} diff --git a/apps/sim/components/emcn/components/tag-input/tag-input.tsx b/apps/sim/components/emcn/components/tag-input/tag-input.tsx index 66db9c9c52b..d8776e44d75 100644 --- a/apps/sim/components/emcn/components/tag-input/tag-input.tsx +++ b/apps/sim/components/emcn/components/tag-input/tag-input.tsx @@ -81,6 +81,11 @@ const tagInputVariants = cva( export interface TagItem { value: string isValid: boolean + /** + * Why the item is invalid. Shown in a tooltip on the invalid chip (and as + * screen-reader-only text inside it). Ignored when `isValid` is true. + */ + error?: string } /** @@ -162,7 +167,9 @@ const TagInputTag = React.memo(function TagInputTag({ onRemove(item.value, index, item.isValid) }, [item.value, item.isValid, index, onRemove]) - return ( + const showError = !item.isValid && !!item.error + + const tag = ( {item.value} + {showError && {item.error}} {suffix} ) + + if (!showError) return tag + + return ( + + {tag} + {item.error} + + ) }) /** From fde36b48c33a5332ce75aa8599f88a2dc25b9e67 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 12 Jun 2026 10:45:53 -0700 Subject: [PATCH 2/2] improvement(emcn): fall back to generic reason for invalid email chips --- .../settings/components/credential-sets/credential-sets.tsx | 6 +++++- .../deploy/components/deploy-modal/components/chat/chat.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx index 7dfa1bae4a8..ae9a8d157c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx @@ -136,7 +136,11 @@ export function CredentialSets() { setEmailItems((prev) => [ ...prev, - { value: normalized, isValid, error: isValid ? undefined : validation.reason }, + { + value: normalized, + isValid, + error: isValid ? undefined : (validation.reason ?? 'Invalid email format'), + }, ]) if (isValid) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index e8c3d33bf7c..bac2a8487e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -657,7 +657,7 @@ function AuthSelector({ } else { setInvalidEmailItems((prev) => [ ...prev, - { value: normalized, isValid, error: validation.reason }, + { value: normalized, isValid, error: validation.reason ?? 'Invalid email format' }, ]) }