Skip to content

Commit 7f18d96

Browse files
authored
feat(popover): add expandOnHover, added the ability to change the color of a workflow icon, new workflow naming convention (#2770)
* feat(popover): add expandOnHover, added the ability to change the color of a workflow icon * updated workflow naming conventions
1 parent e347486 commit 7f18d96

File tree

15 files changed

+1031
-427
lines changed

15 files changed

+1031
-427
lines changed

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ function AddMembersModal({
163163
className='flex items-center gap-[10px] rounded-[4px] px-[8px] py-[6px] hover:bg-[var(--surface-2)]'
164164
>
165165
<Checkbox checked={isSelected} />
166-
<Avatar size='xs'>
166+
<Avatar size='sm'>
167167
{member.user?.image && (
168168
<AvatarImage src={member.user.image} alt={name} />
169169
)}
@@ -663,7 +663,7 @@ export function AccessControl() {
663663
return (
664664
<div key={member.id} className='flex items-center justify-between'>
665665
<div className='flex flex-1 items-center gap-[12px]'>
666-
<Avatar size='sm'>
666+
<Avatar size='md'>
667667
{member.userImage && <AvatarImage src={member.userImage} alt={name} />}
668668
<AvatarFallback
669669
style={{

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credential-sets/credential-sets.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,10 @@ export function CredentialSets() {
434434
filteredOwnedSets.length === 0 &&
435435
!hasNoContent
436436

437-
// Early returns AFTER all hooks
438437
if (membershipsLoading || invitationsLoading) {
439438
return <CredentialSetsSkeleton />
440439
}
441440

442-
// Detail view for a polling group
443441
if (viewingSet) {
444442
const activeMembers = members.filter((m) => m.status === 'active')
445443
const totalCount = activeMembers.length + pendingInvitations.length
@@ -529,7 +527,7 @@ export function CredentialSets() {
529527
return (
530528
<div key={member.id} className='flex items-center justify-between'>
531529
<div className='flex flex-1 items-center gap-[12px]'>
532-
<Avatar size='sm'>
530+
<Avatar size='md'>
533531
{member.userImage && (
534532
<AvatarImage src={member.userImage} alt={name} />
535533
)}
@@ -583,7 +581,7 @@ export function CredentialSets() {
583581
return (
584582
<div key={invitation.id} className='flex items-center justify-between'>
585583
<div className='flex flex-1 items-center gap-[12px]'>
586-
<Avatar size='sm'>
584+
<Avatar size='md'>
587585
<AvatarFallback
588586
style={{ background: getUserColor(email) }}
589587
className='border-0 text-white'

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
'use client'
22

3+
import { useCallback, useEffect, useMemo, useState } from 'react'
4+
import { Check } from 'lucide-react'
35
import {
46
Popover,
57
PopoverAnchor,
8+
PopoverBackButton,
69
PopoverContent,
710
PopoverDivider,
11+
PopoverFolder,
812
PopoverItem,
913
} from '@/components/emcn'
14+
import { cn } from '@/lib/core/utils/cn'
15+
import { WORKFLOW_COLORS } from '@/lib/workflows/colors'
16+
17+
/**
18+
* Validates a hex color string.
19+
* Accepts 3 or 6 character hex codes with or without #.
20+
*/
21+
function isValidHex(hex: string): boolean {
22+
const cleaned = hex.replace('#', '')
23+
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleaned)
24+
}
25+
26+
/**
27+
* Normalizes a hex color to lowercase 6-character format with #.
28+
*/
29+
function normalizeHex(hex: string): string {
30+
let cleaned = hex.replace('#', '').toLowerCase()
31+
if (cleaned.length === 3) {
32+
cleaned = cleaned
33+
.split('')
34+
.map((c) => c + c)
35+
.join('')
36+
}
37+
return `#${cleaned}`
38+
}
1039

1140
interface ContextMenuProps {
1241
/**
@@ -53,6 +82,14 @@ interface ContextMenuProps {
5382
* Callback when delete is clicked
5483
*/
5584
onDelete: () => void
85+
/**
86+
* Callback when color is changed
87+
*/
88+
onColorChange?: (color: string) => void
89+
/**
90+
* Current workflow color (for showing selected state)
91+
*/
92+
currentColor?: string
5693
/**
5794
* Whether to show the open in new tab option (default: false)
5895
* Set to true for items that can be opened in a new tab
@@ -83,11 +120,21 @@ interface ContextMenuProps {
83120
* Set to true for items that can be exported (like workspaces)
84121
*/
85122
showExport?: boolean
123+
/**
124+
* Whether to show the change color option (default: false)
125+
* Set to true for workflows to allow color customization
126+
*/
127+
showColorChange?: boolean
86128
/**
87129
* Whether the export option is disabled (default: false)
88130
* Set to true when user lacks permissions
89131
*/
90132
disableExport?: boolean
133+
/**
134+
* Whether the change color option is disabled (default: false)
135+
* Set to true when user lacks permissions
136+
*/
137+
disableColorChange?: boolean
91138
/**
92139
* Whether the rename option is disabled (default: false)
93140
* Set to true when user lacks permissions
@@ -134,23 +181,74 @@ export function ContextMenu({
134181
onDuplicate,
135182
onExport,
136183
onDelete,
184+
onColorChange,
185+
currentColor,
137186
showOpenInNewTab = false,
138187
showRename = true,
139188
showCreate = false,
140189
showCreateFolder = false,
141190
showDuplicate = true,
142191
showExport = false,
192+
showColorChange = false,
143193
disableExport = false,
194+
disableColorChange = false,
144195
disableRename = false,
145196
disableDuplicate = false,
146197
disableDelete = false,
147198
disableCreate = false,
148199
disableCreateFolder = false,
149200
}: ContextMenuProps) {
150-
// Section visibility for divider logic
201+
const [hexInput, setHexInput] = useState(currentColor || '#ffffff')
202+
203+
// Sync hexInput when currentColor changes (e.g., opening menu on different workflow)
204+
useEffect(() => {
205+
setHexInput(currentColor || '#ffffff')
206+
}, [currentColor])
207+
208+
const canSubmitHex = useMemo(() => {
209+
if (!isValidHex(hexInput)) return false
210+
const normalized = normalizeHex(hexInput)
211+
if (currentColor && normalized.toLowerCase() === currentColor.toLowerCase()) return false
212+
return true
213+
}, [hexInput, currentColor])
214+
215+
const handleHexSubmit = useCallback(() => {
216+
if (!canSubmitHex || !onColorChange) return
217+
218+
const normalized = normalizeHex(hexInput)
219+
onColorChange(normalized)
220+
setHexInput(normalized)
221+
}, [hexInput, canSubmitHex, onColorChange])
222+
223+
const handleHexKeyDown = useCallback(
224+
(e: React.KeyboardEvent<HTMLInputElement>) => {
225+
if (e.key === 'Enter') {
226+
e.preventDefault()
227+
handleHexSubmit()
228+
}
229+
},
230+
[handleHexSubmit]
231+
)
232+
233+
const handleHexChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
234+
let value = e.target.value.trim()
235+
if (value && !value.startsWith('#')) {
236+
value = `#${value}`
237+
}
238+
value = value.slice(0, 1) + value.slice(1).replace(/[^0-9a-fA-F]/g, '')
239+
setHexInput(value.slice(0, 7))
240+
}, [])
241+
242+
const handleHexFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
243+
e.target.select()
244+
}, [])
245+
151246
const hasNavigationSection = showOpenInNewTab && onOpenInNewTab
152247
const hasEditSection =
153-
(showRename && onRename) || (showCreate && onCreate) || (showCreateFolder && onCreateFolder)
248+
(showRename && onRename) ||
249+
(showCreate && onCreate) ||
250+
(showCreateFolder && onCreateFolder) ||
251+
(showColorChange && onColorChange)
154252
const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport)
155253

156254
return (
@@ -170,10 +268,21 @@ export function ContextMenu({
170268
height: '1px',
171269
}}
172270
/>
173-
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
271+
<PopoverContent
272+
ref={menuRef}
273+
align='start'
274+
side='bottom'
275+
sideOffset={4}
276+
onPointerDownOutside={(e) => e.preventDefault()}
277+
onInteractOutside={(e) => e.preventDefault()}
278+
>
279+
{/* Back button - shown only when in a folder */}
280+
<PopoverBackButton />
281+
174282
{/* Navigation actions */}
175283
{showOpenInNewTab && onOpenInNewTab && (
176284
<PopoverItem
285+
rootOnly
177286
onClick={() => {
178287
onOpenInNewTab()
179288
onClose()
@@ -182,11 +291,12 @@ export function ContextMenu({
182291
Open in new tab
183292
</PopoverItem>
184293
)}
185-
{hasNavigationSection && (hasEditSection || hasCopySection) && <PopoverDivider />}
294+
{hasNavigationSection && (hasEditSection || hasCopySection) && <PopoverDivider rootOnly />}
186295

187296
{/* Edit and create actions */}
188297
{showRename && onRename && (
189298
<PopoverItem
299+
rootOnly
190300
disabled={disableRename}
191301
onClick={() => {
192302
onRename()
@@ -198,6 +308,7 @@ export function ContextMenu({
198308
)}
199309
{showCreate && onCreate && (
200310
<PopoverItem
311+
rootOnly
201312
disabled={disableCreate}
202313
onClick={() => {
203314
onCreate()
@@ -209,6 +320,7 @@ export function ContextMenu({
209320
)}
210321
{showCreateFolder && onCreateFolder && (
211322
<PopoverItem
323+
rootOnly
212324
disabled={disableCreateFolder}
213325
onClick={() => {
214326
onCreateFolder()
@@ -218,11 +330,72 @@ export function ContextMenu({
218330
Create folder
219331
</PopoverItem>
220332
)}
333+
{showColorChange && onColorChange && (
334+
<PopoverFolder
335+
id='color-picker'
336+
title='Change color'
337+
expandOnHover
338+
className={disableColorChange ? 'pointer-events-none opacity-50' : ''}
339+
>
340+
<div className='flex w-[140px] flex-col gap-[8px] p-[2px]'>
341+
{/* Preset colors */}
342+
<div className='grid grid-cols-6 gap-[4px]'>
343+
{WORKFLOW_COLORS.map(({ color, name }) => (
344+
<button
345+
key={color}
346+
type='button'
347+
title={name}
348+
onClick={(e) => {
349+
e.stopPropagation()
350+
onColorChange(color)
351+
}}
352+
className={cn(
353+
'h-[20px] w-[20px] rounded-[4px]',
354+
currentColor?.toLowerCase() === color.toLowerCase() && 'ring-1 ring-white'
355+
)}
356+
style={{ backgroundColor: color }}
357+
/>
358+
))}
359+
</div>
360+
361+
{/* Hex input */}
362+
<div className='flex items-center gap-[4px]'>
363+
<div
364+
className='h-[20px] w-[20px] flex-shrink-0 rounded-[4px]'
365+
style={{
366+
backgroundColor: isValidHex(hexInput) ? normalizeHex(hexInput) : '#ffffff',
367+
}}
368+
/>
369+
<input
370+
type='text'
371+
value={hexInput}
372+
onChange={handleHexChange}
373+
onKeyDown={handleHexKeyDown}
374+
onFocus={handleHexFocus}
375+
onClick={(e) => e.stopPropagation()}
376+
className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase focus:outline-none'
377+
/>
378+
<button
379+
type='button'
380+
disabled={!canSubmitHex}
381+
onClick={(e) => {
382+
e.stopPropagation()
383+
handleHexSubmit()
384+
}}
385+
className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--brand-tertiary-2)] text-white disabled:opacity-40'
386+
>
387+
<Check className='h-[12px] w-[12px]' />
388+
</button>
389+
</div>
390+
</div>
391+
</PopoverFolder>
392+
)}
221393

222394
{/* Copy and export actions */}
223-
{hasEditSection && hasCopySection && <PopoverDivider />}
395+
{hasEditSection && hasCopySection && <PopoverDivider rootOnly />}
224396
{showDuplicate && onDuplicate && (
225397
<PopoverItem
398+
rootOnly
226399
disabled={disableDuplicate}
227400
onClick={() => {
228401
onDuplicate()
@@ -234,6 +407,7 @@ export function ContextMenu({
234407
)}
235408
{showExport && onExport && (
236409
<PopoverItem
410+
rootOnly
237411
disabled={disableExport}
238412
onClick={() => {
239413
onExport()
@@ -245,8 +419,9 @@ export function ContextMenu({
245419
)}
246420

247421
{/* Destructive action */}
248-
{(hasNavigationSection || hasEditSection || hasCopySection) && <PopoverDivider />}
422+
{(hasNavigationSection || hasEditSection || hasCopySection) && <PopoverDivider rootOnly />}
249423
<PopoverItem
424+
rootOnly
250425
disabled={disableDelete}
251426
onClick={() => {
252427
onDelete()

0 commit comments

Comments
 (0)