Skip to content

Commit 51f9380

Browse files
committed
feat(emcn): tag input, tooltip shortcut
1 parent b403378 commit 51f9380

File tree

25 files changed

+601
-530
lines changed

25 files changed

+601
-530
lines changed

apps/sim/app/_shell/providers/theme-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
2828
return (
2929
<NextThemesProvider
3030
attribute='class'
31-
defaultTheme='system'
31+
defaultTheme='dark'
3232
enableSystem
3333
disableTransitionOnChange
3434
storageKey='sim-theme'

apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx

Lines changed: 39 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import {
1818
ModalTabsContent,
1919
ModalTabsList,
2020
ModalTabsTrigger,
21+
TagInput,
22+
type TagItem,
2123
} from '@/components/emcn'
2224
import { SlackIcon } from '@/components/icons'
2325
import { Skeleton } from '@/components/ui'
24-
import { cn } from '@/lib/core/utils/cn'
2526
import { quickValidateEmail } from '@/lib/messaging/email/validation'
2627
import {
2728
type NotificationSubscription,
@@ -156,8 +157,7 @@ export function NotificationSettings({
156157
errorCountThreshold: 10,
157158
})
158159

159-
const [emailInputValue, setEmailInputValue] = useState('')
160-
const [invalidEmails, setInvalidEmails] = useState<string[]>([])
160+
const [emailItems, setEmailItems] = useState<TagItem[]>([])
161161

162162
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
163163

@@ -225,8 +225,7 @@ export function NotificationSettings({
225225
})
226226
setFormErrors({})
227227
setEditingId(null)
228-
setEmailInputValue('')
229-
setInvalidEmails([])
228+
setEmailItems([])
230229
}, [])
231230

232231
const handleClose = useCallback(() => {
@@ -243,81 +242,37 @@ export function NotificationSettings({
243242
const normalized = email.trim().toLowerCase()
244243
const validation = quickValidateEmail(normalized)
245244

246-
if (formData.emailRecipients.includes(normalized) || invalidEmails.includes(normalized)) {
245+
if (emailItems.some((item) => item.value === normalized)) {
247246
return false
248247
}
249248

250-
if (!validation.isValid) {
251-
setInvalidEmails((prev) => [...prev, normalized])
252-
setEmailInputValue('')
253-
return false
254-
}
255-
256-
setFormErrors((prev) => ({ ...prev, emailRecipients: '' }))
257-
setFormData((prev) => ({
258-
...prev,
259-
emailRecipients: [...prev.emailRecipients, normalized],
260-
}))
261-
setEmailInputValue('')
262-
return true
263-
},
264-
[formData.emailRecipients, invalidEmails]
265-
)
266-
267-
const handleRemoveEmail = useCallback((emailToRemove: string) => {
268-
setFormData((prev) => ({
269-
...prev,
270-
emailRecipients: prev.emailRecipients.filter((e) => e !== emailToRemove),
271-
}))
272-
}, [])
249+
setEmailItems((prev) => [...prev, { value: normalized, isValid: validation.isValid }])
273250

274-
const handleRemoveInvalidEmail = useCallback((index: number) => {
275-
setInvalidEmails((prev) => prev.filter((_, i) => i !== index))
276-
}, [])
277-
278-
const handleEmailKeyDown = useCallback(
279-
(e: React.KeyboardEvent<HTMLInputElement>) => {
280-
if (['Enter', ',', ' '].includes(e.key) && emailInputValue.trim()) {
281-
e.preventDefault()
282-
addEmail(emailInputValue)
251+
if (validation.isValid) {
252+
setFormErrors((prev) => ({ ...prev, emailRecipients: '' }))
253+
setFormData((prev) => ({
254+
...prev,
255+
emailRecipients: [...prev.emailRecipients, normalized],
256+
}))
283257
}
284258

285-
if (e.key === 'Backspace' && !emailInputValue) {
286-
if (invalidEmails.length > 0) {
287-
handleRemoveInvalidEmail(invalidEmails.length - 1)
288-
} else if (formData.emailRecipients.length > 0) {
289-
handleRemoveEmail(formData.emailRecipients[formData.emailRecipients.length - 1])
290-
}
291-
}
259+
return validation.isValid
292260
},
293-
[
294-
emailInputValue,
295-
addEmail,
296-
invalidEmails,
297-
formData.emailRecipients,
298-
handleRemoveInvalidEmail,
299-
handleRemoveEmail,
300-
]
261+
[emailItems]
301262
)
302263

303-
const handleEmailPaste = useCallback(
304-
(e: React.ClipboardEvent<HTMLInputElement>) => {
305-
e.preventDefault()
306-
const pastedText = e.clipboardData.getData('text')
307-
const pastedEmails = pastedText.split(/[\s,;]+/).filter(Boolean)
308-
309-
let addedCount = 0
310-
pastedEmails.forEach((email) => {
311-
if (addEmail(email)) {
312-
addedCount++
313-
}
314-
})
315-
316-
if (addedCount === 0 && pastedEmails.length === 1) {
317-
setEmailInputValue(emailInputValue + pastedEmails[0])
264+
const handleRemoveEmailItem = useCallback(
265+
(_value: string, index: number, isValid: boolean) => {
266+
const itemToRemove = emailItems[index]
267+
setEmailItems((prev) => prev.filter((_, i) => i !== index))
268+
if (isValid && itemToRemove) {
269+
setFormData((prev) => ({
270+
...prev,
271+
emailRecipients: prev.emailRecipients.filter((e) => e !== itemToRemove.value),
272+
}))
318273
}
319274
},
320-
[addEmail, emailInputValue]
275+
[emailItems]
321276
)
322277

323278
const validateForm = (): boolean => {
@@ -356,8 +311,11 @@ export function NotificationSettings({
356311
} else if (formData.emailRecipients.length > 10) {
357312
errors.emailRecipients = 'Maximum 10 email recipients allowed'
358313
}
359-
if (invalidEmails.length > 0) {
360-
errors.emailRecipients = `Invalid email addresses: ${invalidEmails.join(', ')}`
314+
const invalidEmailValues = emailItems
315+
.filter((item) => !item.isValid)
316+
.map((item) => item.value)
317+
if (invalidEmailValues.length > 0) {
318+
errors.emailRecipients = `Invalid email addresses: ${invalidEmailValues.join(', ')}`
361319
}
362320
}
363321

@@ -536,8 +494,9 @@ export function NotificationSettings({
536494
inactivityHours: subscription.alertConfig?.inactivityHours || 24,
537495
errorCountThreshold: subscription.alertConfig?.errorCountThreshold || 10,
538496
})
539-
setEmailInputValue('')
540-
setInvalidEmails([])
497+
setEmailItems(
498+
(subscription.emailRecipients || []).map((email) => ({ value: email, isValid: true }))
499+
)
541500
setShowForm(true)
542501
}
543502

@@ -692,37 +651,13 @@ export function NotificationSettings({
692651
{activeTab === 'email' && (
693652
<div className='flex flex-col gap-[8px]'>
694653
<Label className='text-[var(--text-secondary)]'>Email Recipients</Label>
695-
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]'>
696-
{invalidEmails.map((email, index) => (
697-
<EmailTag
698-
key={`invalid-${index}`}
699-
email={email}
700-
onRemove={() => handleRemoveInvalidEmail(index)}
701-
isInvalid={true}
702-
/>
703-
))}
704-
{formData.emailRecipients.map((email, index) => (
705-
<EmailTag
706-
key={`valid-${index}`}
707-
email={email}
708-
onRemove={() => handleRemoveEmail(email)}
709-
/>
710-
))}
711-
<input
712-
type='text'
713-
value={emailInputValue}
714-
onChange={(e) => setEmailInputValue(e.target.value)}
715-
onKeyDown={handleEmailKeyDown}
716-
onPaste={handleEmailPaste}
717-
onBlur={() => emailInputValue.trim() && addEmail(emailInputValue)}
718-
placeholder={
719-
formData.emailRecipients.length > 0 || invalidEmails.length > 0
720-
? 'Add another email'
721-
: 'Enter emails'
722-
}
723-
className='min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50'
724-
/>
725-
</div>
654+
<TagInput
655+
items={emailItems}
656+
onAdd={(value) => addEmail(value)}
657+
onRemove={handleRemoveEmailItem}
658+
placeholder='Enter emails'
659+
placeholderWithTags='Add email'
660+
/>
726661
{formErrors.emailRecipients && (
727662
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.emailRecipients}</p>
728663
)}
@@ -1351,37 +1286,3 @@ export function NotificationSettings({
13511286
</>
13521287
)
13531288
}
1354-
1355-
interface EmailTagProps {
1356-
email: string
1357-
onRemove: () => void
1358-
isInvalid?: boolean
1359-
}
1360-
1361-
function EmailTag({ email, onRemove, isInvalid }: EmailTagProps) {
1362-
return (
1363-
<div
1364-
className={cn(
1365-
'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]',
1366-
isInvalid
1367-
? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
1368-
: 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
1369-
)}
1370-
>
1371-
<span className='max-w-[200px] truncate'>{email}</span>
1372-
<button
1373-
type='button'
1374-
onClick={onRemove}
1375-
className={cn(
1376-
'flex-shrink-0 transition-colors focus:outline-none',
1377-
isInvalid
1378-
? 'text-[var(--text-error)] hover:text-[var(--text-error)]'
1379-
: 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
1380-
)}
1381-
aria-label={`Remove ${email}`}
1382-
>
1383-
<X className='h-[12px] w-[12px] translate-y-[0.2px]' />
1384-
</button>
1385-
</div>
1386-
)
1387-
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function BlockContextMenu({
8080
}}
8181
>
8282
<span>Copy</span>
83-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘C</span>
83+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘C</span>
8484
</PopoverItem>
8585
<PopoverItem
8686
className='group'
@@ -91,7 +91,7 @@ export function BlockContextMenu({
9191
}}
9292
>
9393
<span>Paste</span>
94-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
94+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘V</span>
9595
</PopoverItem>
9696
{!hasStarterBlock && (
9797
<PopoverItem
@@ -176,7 +176,7 @@ export function BlockContextMenu({
176176
}}
177177
>
178178
<span>Delete</span>
179-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'></span>
179+
<span className='ml-auto opacity-70 group-hover:opacity-100'></span>
180180
</PopoverItem>
181181
</PopoverContent>
182182
</Popover>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function PaneContextMenu({
6161
}}
6262
>
6363
<span>Undo</span>
64-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘Z</span>
64+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘Z</span>
6565
</PopoverItem>
6666
<PopoverItem
6767
className='group'
@@ -72,7 +72,7 @@ export function PaneContextMenu({
7272
}}
7373
>
7474
<span>Redo</span>
75-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘⇧Z</span>
75+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘⇧Z</span>
7676
</PopoverItem>
7777

7878
{/* Edit and creation actions */}
@@ -86,7 +86,7 @@ export function PaneContextMenu({
8686
}}
8787
>
8888
<span>Paste</span>
89-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
89+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘V</span>
9090
</PopoverItem>
9191
<PopoverItem
9292
className='group'
@@ -97,7 +97,7 @@ export function PaneContextMenu({
9797
}}
9898
>
9999
<span>Add Block</span>
100-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘K</span>
100+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘K</span>
101101
</PopoverItem>
102102
<PopoverItem
103103
className='group'
@@ -108,7 +108,7 @@ export function PaneContextMenu({
108108
}}
109109
>
110110
<span>Auto-layout</span>
111-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⇧L</span>
111+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⇧L</span>
112112
</PopoverItem>
113113

114114
{/* Navigation actions */}
@@ -121,7 +121,7 @@ export function PaneContextMenu({
121121
}}
122122
>
123123
<span>Open Logs</span>
124-
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘L</span>
124+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘L</span>
125125
</PopoverItem>
126126
<PopoverItem
127127
onClick={() => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { memo, useCallback, useMemo } from 'react'
22
import { createLogger } from '@sim/logger'
33
import clsx from 'clsx'
44
import { X } from 'lucide-react'
5-
import { Button } from '@/components/emcn'
5+
import { Button, Tooltip } from '@/components/emcn'
66
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
77
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
88
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
@@ -130,14 +130,21 @@ export const Notifications = memo(function Notifications() {
130130
hasAction ? 'line-clamp-2' : 'line-clamp-4'
131131
}`}
132132
>
133-
<Button
134-
variant='ghost'
135-
onClick={() => removeNotification(notification.id)}
136-
aria-label='Dismiss notification'
137-
className='!p-1.5 -m-1.5 float-right ml-[16px]'
138-
>
139-
<X className='h-3 w-3' />
140-
</Button>
133+
<Tooltip.Root>
134+
<Tooltip.Trigger asChild>
135+
<Button
136+
variant='ghost'
137+
onClick={() => removeNotification(notification.id)}
138+
aria-label='Dismiss notification'
139+
className='!p-1.5 -m-1.5 float-right ml-[16px]'
140+
>
141+
<X className='h-3 w-3' />
142+
</Button>
143+
</Tooltip.Trigger>
144+
<Tooltip.Content>
145+
<Tooltip.Shortcut keys='⌘E'>Clear all</Tooltip.Shortcut>
146+
</Tooltip.Content>
147+
</Tooltip.Root>
141148
{notification.level === 'error' && (
142149
<span className='mr-[6px] mb-[2.75px] inline-block h-[6px] w-[6px] rounded-[2px] bg-[var(--text-error)] align-middle' />
143150
)}

0 commit comments

Comments
 (0)