Skip to content

Commit bf036a1

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
improvement(ui): share bulk action bar surfaces (#8077)
Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain>
1 parent a39178c commit bf036a1

5 files changed

Lines changed: 171 additions & 152 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/action-bar/action-bar.tsx‎

Lines changed: 43 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { ComponentType } from 'react'
44
import {
5+
BulkActionBar,
56
BulkActionButton,
67
cn,
78
DropdownMenu,
@@ -91,57 +92,48 @@ export function ResourceActionBar({
9192
className
9293
)}
9394
>
94-
<div className='flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
95-
<span
96-
className={cn(
97-
'px-1 text-small',
98-
exceedsLimit ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]'
99-
)}
100-
>
101-
{exceedsLimit
102-
? `${selectedCount} selected · select ${maxSelectable} or fewer`
103-
: `${selectedCount} selected`}
104-
</span>
105-
<div className='flex items-center gap-[5px]'>
106-
{onDownload && (
107-
<ActionButton
108-
icon={Download}
109-
label='Download'
110-
onClick={onDownload}
111-
disabled={actionsDisabled}
112-
/>
113-
)}
114-
{onMove && moveOptions && (
115-
<DropdownMenu>
116-
<Tooltip.Root>
117-
<Tooltip.Trigger asChild>
118-
<DropdownMenuTrigger asChild>
119-
<BulkActionButton aria-label='Move' disabled={actionsDisabled}>
120-
<Folder className='size-[12px]' />
121-
</BulkActionButton>
122-
</DropdownMenuTrigger>
123-
</Tooltip.Trigger>
124-
<Tooltip.Content side='top'>Move</Tooltip.Content>
125-
</Tooltip.Root>
126-
<DropdownMenuContent
127-
side='top'
128-
align='center'
129-
className='max-h-[240px] overflow-y-auto'
130-
>
131-
{renderMoveOptions(moveOptions, onMove)}
132-
</DropdownMenuContent>
133-
</DropdownMenu>
134-
)}
135-
{onDelete && (
136-
<ActionButton
137-
icon={Trash}
138-
label='Delete'
139-
onClick={onDelete}
140-
disabled={actionsDisabled}
141-
/>
142-
)}
143-
</div>
144-
</div>
95+
<BulkActionBar
96+
label={
97+
<span className={exceedsLimit ? 'text-[var(--text-error)]' : undefined}>
98+
{exceedsLimit
99+
? `${selectedCount} selected · select ${maxSelectable} or fewer`
100+
: `${selectedCount} selected`}
101+
</span>
102+
}
103+
>
104+
{onDownload && (
105+
<ActionButton
106+
icon={Download}
107+
label='Download'
108+
onClick={onDownload}
109+
disabled={actionsDisabled}
110+
/>
111+
)}
112+
{onMove && moveOptions && (
113+
<DropdownMenu>
114+
<Tooltip.Root>
115+
<Tooltip.Trigger asChild>
116+
<DropdownMenuTrigger asChild>
117+
<BulkActionButton aria-label='Move' disabled={actionsDisabled}>
118+
<Folder className='size-[12px]' />
119+
</BulkActionButton>
120+
</DropdownMenuTrigger>
121+
</Tooltip.Trigger>
122+
<Tooltip.Content side='top'>Move</Tooltip.Content>
123+
</Tooltip.Root>
124+
<DropdownMenuContent
125+
side='top'
126+
align='center'
127+
className='max-h-[240px] overflow-y-auto'
128+
>
129+
{renderMoveOptions(moveOptions, onMove)}
130+
</DropdownMenuContent>
131+
</DropdownMenu>
132+
)}
133+
{onDelete && (
134+
<ActionButton icon={Trash} label='Delete' onClick={onDelete} disabled={actionsDisabled} />
135+
)}
136+
</BulkActionBar>
145137
</div>
146138
)
147139
}

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx‎

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BulkActionButton, cn, Tooltip } from '@sim/emcn'
1+
import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn'
22
import { Ban, Circle, Trash } from '@sim/emcn/icons'
33
import { domAnimation, LazyMotion, m } from 'framer-motion'
44
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -53,70 +53,70 @@ export function ActionBar({
5353
transition={{ duration: 0.2 }}
5454
className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-[var(--z-dropdown)]', className)}
5555
>
56-
<div className='flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
57-
<span className='px-1 text-[var(--text-secondary)] text-small'>
58-
{isAllSelected ? totalCount : selectedCount} selected
59-
{showSelectAllOption && (
60-
<>
61-
{' · '}
62-
<button
63-
type='button'
64-
onClick={onSelectAll}
65-
className='text-[var(--brand-secondary)] hover-hover:underline'
66-
>
67-
Select all
68-
</button>
69-
</>
70-
)}
71-
{isAllSelected && onClearSelectAll && (
72-
<>
73-
{' · '}
74-
<button
75-
type='button'
76-
onClick={onClearSelectAll}
77-
className='text-[var(--brand-secondary)] hover-hover:underline'
78-
>
79-
Clear
80-
</button>
81-
</>
82-
)}
83-
</span>
56+
<BulkActionBar
57+
label={
58+
<>
59+
{isAllSelected ? totalCount : selectedCount} selected
60+
{showSelectAllOption && (
61+
<>
62+
{' · '}
63+
<button
64+
type='button'
65+
onClick={onSelectAll}
66+
className='text-[var(--brand-secondary)] hover-hover:underline'
67+
>
68+
Select all
69+
</button>
70+
</>
71+
)}
72+
{isAllSelected && onClearSelectAll && (
73+
<>
74+
{' · '}
75+
<button
76+
type='button'
77+
onClick={onClearSelectAll}
78+
className='text-[var(--brand-secondary)] hover-hover:underline'
79+
>
80+
Clear
81+
</button>
82+
</>
83+
)}
84+
</>
85+
}
86+
>
87+
{showEnableButton && (
88+
<Tooltip.Root>
89+
<Tooltip.Trigger asChild>
90+
<BulkActionButton aria-label='Enable' onClick={onEnable} disabled={isLoading}>
91+
<Circle className='size-[12px]' />
92+
</BulkActionButton>
93+
</Tooltip.Trigger>
94+
<Tooltip.Content side='top'>Enable</Tooltip.Content>
95+
</Tooltip.Root>
96+
)}
8497

85-
<div className='flex items-center gap-[5px]'>
86-
{showEnableButton && (
87-
<Tooltip.Root>
88-
<Tooltip.Trigger asChild>
89-
<BulkActionButton aria-label='Enable' onClick={onEnable} disabled={isLoading}>
90-
<Circle className='size-[12px]' />
91-
</BulkActionButton>
92-
</Tooltip.Trigger>
93-
<Tooltip.Content side='top'>Enable</Tooltip.Content>
94-
</Tooltip.Root>
95-
)}
98+
{showDisableButton && (
99+
<Tooltip.Root>
100+
<Tooltip.Trigger asChild>
101+
<BulkActionButton aria-label='Disable' onClick={onDisable} disabled={isLoading}>
102+
<Ban className='size-[12px]' />
103+
</BulkActionButton>
104+
</Tooltip.Trigger>
105+
<Tooltip.Content side='top'>Disable</Tooltip.Content>
106+
</Tooltip.Root>
107+
)}
96108

97-
{showDisableButton && (
98-
<Tooltip.Root>
99-
<Tooltip.Trigger asChild>
100-
<BulkActionButton aria-label='Disable' onClick={onDisable} disabled={isLoading}>
101-
<Ban className='size-[12px]' />
102-
</BulkActionButton>
103-
</Tooltip.Trigger>
104-
<Tooltip.Content side='top'>Disable</Tooltip.Content>
105-
</Tooltip.Root>
106-
)}
107-
108-
{onDelete && canEdit && (
109-
<Tooltip.Root>
110-
<Tooltip.Trigger asChild>
111-
<BulkActionButton aria-label='Delete' onClick={onDelete} disabled={isLoading}>
112-
<Trash className='size-[12px]' />
113-
</BulkActionButton>
114-
</Tooltip.Trigger>
115-
<Tooltip.Content side='top'>Delete</Tooltip.Content>
116-
</Tooltip.Root>
117-
)}
118-
</div>
119-
</div>
109+
{onDelete && canEdit && (
110+
<Tooltip.Root>
111+
<Tooltip.Trigger asChild>
112+
<BulkActionButton aria-label='Delete' onClick={onDelete} disabled={isLoading}>
113+
<Trash className='size-[12px]' />
114+
</BulkActionButton>
115+
</Tooltip.Trigger>
116+
<Tooltip.Content side='top'>Delete</Tooltip.Content>
117+
</Tooltip.Root>
118+
)}
119+
</BulkActionBar>
120120
</m.div>
121121
</LazyMotion>
122122
)

‎apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx‎

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import type React from 'react'
4-
import { BulkActionButton, cn, Tooltip } from '@sim/emcn'
4+
import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn'
55
import { Eye, PlayOutline, RefreshCw, Square } from '@sim/emcn/icons'
66
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
77

@@ -89,47 +89,42 @@ export function TableActionBar({
8989
className
9090
)}
9191
>
92-
<div className='pointer-events-auto flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
93-
<span className='px-1 text-[var(--text-secondary)] text-small'>
94-
{selectedCellCount === 1
92+
<BulkActionBar
93+
className='pointer-events-auto'
94+
label={
95+
selectedCellCount === 1
9596
? 'Selected 1 workflow cell'
96-
: `Selected ${selectedCellCount} workflow cells`}
97-
</span>
97+
: `Selected ${selectedCellCount} workflow cells`
98+
}
99+
>
100+
{showPlay && (
101+
<ActionIconButton label={playLabel} onClick={onPlay} disabled={isLoading}>
102+
<PlayOutline className='size-[12px]' />
103+
</ActionIconButton>
104+
)}
98105

99-
<div className='flex items-center gap-[5px]'>
100-
{showPlay && (
101-
<ActionIconButton label={playLabel} onClick={onPlay} disabled={isLoading}>
102-
<PlayOutline className='size-[12px]' />
103-
</ActionIconButton>
104-
)}
106+
{showRefresh && (
107+
<ActionIconButton label={refreshLabel} onClick={onRefresh} disabled={isLoading}>
108+
<RefreshCw className='size-[12px]' />
109+
</ActionIconButton>
110+
)}
105111

106-
{showRefresh && (
107-
<ActionIconButton label={refreshLabel} onClick={onRefresh} disabled={isLoading}>
108-
<RefreshCw className='size-[12px]' />
109-
</ActionIconButton>
110-
)}
112+
{runningCount > 0 && (
113+
<ActionIconButton label={stopLabel} onClick={onStopWorkflows} disabled={isLoading}>
114+
<Square className='size-[12px]' />
115+
</ActionIconButton>
116+
)}
111117

112-
{runningCount > 0 && (
113-
<ActionIconButton
114-
label={stopLabel}
115-
onClick={onStopWorkflows}
116-
disabled={isLoading}
117-
>
118-
<Square className='size-[12px]' />
119-
</ActionIconButton>
120-
)}
121-
122-
{onViewExecution && (
123-
<ActionIconButton
124-
label='View execution'
125-
onClick={onViewExecution}
126-
disabled={isLoading}
127-
>
128-
<Eye className='size-[12px]' />
129-
</ActionIconButton>
130-
)}
131-
</div>
132-
</div>
118+
{onViewExecution && (
119+
<ActionIconButton
120+
label='View execution'
121+
onClick={onViewExecution}
122+
disabled={isLoading}
123+
>
124+
<Eye className='size-[12px]' />
125+
</ActionIconButton>
126+
)}
127+
</BulkActionBar>
133128
</m.div>
134129
)}
135130
</AnimatePresence>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
2+
import { cn } from '../../lib/cn'
3+
4+
export interface BulkActionBarProps extends HTMLAttributes<HTMLDivElement> {
5+
/** Selection summary, including optional inline selection actions or limit warnings. */
6+
label: ReactNode
7+
children: ReactNode
8+
}
9+
10+
/**
11+
* Shared selection-action surface. Callers own visibility, positioning, animation
12+
* and commands; compose actions with BulkActionButton.
13+
* @example <BulkActionBar label='2 selected'><BulkActionButton aria-label='Delete'><Trash /></BulkActionButton></BulkActionBar>
14+
*/
15+
export const BulkActionBar = forwardRef<HTMLDivElement, BulkActionBarProps>(
16+
({ label, children, className, ...props }, ref) => (
17+
<div
18+
{...props}
19+
ref={ref}
20+
className={cn(
21+
'flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5',
22+
className
23+
)}
24+
>
25+
<span className='px-1 text-[var(--text-secondary)] text-small'>{label}</span>
26+
<div className='flex items-center gap-[5px]'>{children}</div>
27+
</div>
28+
)
29+
)
30+
31+
BulkActionBar.displayName = 'BulkActionBar'

‎packages/emcn/src/components/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { Avatar, AvatarFallback, AvatarImage } from './avatar/avatar'
22
export { Badge, type BadgeProps } from './badge/badge'
33
export { Banner } from './banner/banner'
4+
export { BulkActionBar, type BulkActionBarProps } from './bulk-action-bar/bulk-action-bar'
45
export {
56
BulkActionButton,
67
type BulkActionButtonProps,

0 commit comments

Comments
 (0)