Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export default function PlaygroundPage() {
const [dateRangeEnd, setDateRangeEnd] = useState('')
const [tagItems, setTagItems] = useState<TagItem[]>([
{ value: 'user@example.com', isValid: true },
{ value: 'invalid-email', isValid: false },
{ value: 'invalid-email', isValid: false, error: 'Invalid email format' },
])

const toggleDarkMode = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,14 @@ export function CredentialSets() {
return false
}

setEmailItems((prev) => [...prev, { value: normalized, isValid }])
setEmailItems((prev) => [
...prev,
{
value: normalized,
isValid,
error: isValid ? undefined : (validation.reason ?? 'Invalid email format'),
},
])
Comment thread
waleedlatif1 marked this conversation as resolved.

if (isValid) {
setEmailError(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? 'Invalid email format' },
])
}

return isValid
Expand Down
41 changes: 19 additions & 22 deletions apps/sim/components/emcn/components/chip-modal/chip-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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,
Expand All @@ -576,7 +581,6 @@ function ChipModalEmailsControl({
errorId,
}: ChipModalEmailsFieldProps & { id: string; errorId: string }) {
const [items, setItems] = React.useState<TagItem[]>([])
const [internalError, setInternalError] = React.useState<string | null>(null)

/**
* Reconcile internal `items` with the consumer's `value` when the latter
Expand All @@ -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]
Expand All @@ -630,34 +635,26 @@ 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 (
<>
<TagInput
variant='block'
items={items}
onAdd={handleAdd}
onRemove={handleRemove}
onInputChange={handleInputChange}
placeholder={placeholder}
placeholderWithTags={derivePlaceholderWithTags(placeholder)}
disabled={disabled}
autoFocus={autoFocus}
id={id}
/>
{banner && (
{error && (
<p id={errorId} role='alert' className={CHIP_MODAL_FIELD_ERROR_CLASS}>
{banner}
{error}
</p>
)}
</>
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/components/emcn/components/tag-input/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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 = (
<ChipTag
variant='invite'
invalid={!item.isValid}
Expand All @@ -174,9 +181,19 @@ const TagInputTag = React.memo(function TagInputTag({
<span className='min-w-0 flex-1 translate-y-[0.5px] truncate font-medium font-sans text-sm leading-5'>
{item.value}
</span>
{showError && <span className='sr-only'>{item.error}</span>}
{suffix}
</ChipTag>
)

if (!showError) return tag

return (
<Tooltip.Root>
<Tooltip.Trigger asChild>{tag}</Tooltip.Trigger>
<Tooltip.Content>{item.error}</Tooltip.Content>
</Tooltip.Root>
)
})

/**
Expand Down
Loading