Skip to content

Commit 756cf45

Browse files
committed
fix(resources): correct bulk action scopes
1 parent 853c395 commit 756cf45

7 files changed

Lines changed: 176 additions & 64 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const FolderContextMenu = memo(function FolderContextMenu({
6363
const isMultiSelect = selectedCount > 1
6464
const hasMove = Boolean(onMove && moveOptions && moveOptions.length > 0)
6565
const hasActionsAboveDestructive = !isMultiSelect || hasMove
66+
const hasAvailableActions = !isMultiSelect || canEdit
6667

6768
return (
6869
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
@@ -80,48 +81,54 @@ export const FolderContextMenu = memo(function FolderContextMenu({
8081
sideOffset={4}
8182
onCloseAutoFocus={(e) => e.preventDefault()}
8283
>
83-
{!isMultiSelect && (
84-
<>
85-
<DropdownMenuItem onSelect={onOpen}>
86-
<Eye />
87-
Open
88-
</DropdownMenuItem>
89-
<DropdownMenuItem onSelect={onTogglePin}>
90-
<Pin />
91-
{pinned ? 'Unpin' : 'Pin'}
92-
</DropdownMenuItem>
93-
{onCopyId && (
94-
<DropdownMenuItem onSelect={onCopyId}>
95-
<Duplicate />
96-
Copy ID
97-
</DropdownMenuItem>
98-
)}
99-
</>
100-
)}
101-
{canEdit && (
84+
{!hasAvailableActions ? (
85+
<DropdownMenuItem disabled>No actions available</DropdownMenuItem>
86+
) : (
10287
<>
10388
{!isMultiSelect && (
104-
<DropdownMenuItem onSelect={onRename}>
105-
<Pencil />
106-
Rename
107-
</DropdownMenuItem>
89+
<>
90+
<DropdownMenuItem onSelect={onOpen}>
91+
<Eye />
92+
Open
93+
</DropdownMenuItem>
94+
<DropdownMenuItem onSelect={onTogglePin}>
95+
<Pin />
96+
{pinned ? 'Unpin' : 'Pin'}
97+
</DropdownMenuItem>
98+
{onCopyId && (
99+
<DropdownMenuItem onSelect={onCopyId}>
100+
<Duplicate />
101+
Copy ID
102+
</DropdownMenuItem>
103+
)}
104+
</>
108105
)}
109-
{hasMove && (
110-
<DropdownMenuSub>
111-
<DropdownMenuSubTrigger>
112-
<FolderInput />
113-
{selectionActionLabel('Move', selectedCount, 'Move to')}
114-
</DropdownMenuSubTrigger>
115-
<DropdownMenuSubContent>
116-
{renderMoveOptions(moveOptions!, onMove!)}
117-
</DropdownMenuSubContent>
118-
</DropdownMenuSub>
106+
{canEdit && (
107+
<>
108+
{!isMultiSelect && (
109+
<DropdownMenuItem onSelect={onRename}>
110+
<Pencil />
111+
Rename
112+
</DropdownMenuItem>
113+
)}
114+
{hasMove && (
115+
<DropdownMenuSub>
116+
<DropdownMenuSubTrigger>
117+
<FolderInput />
118+
{selectionActionLabel('Move', selectedCount, 'Move to')}
119+
</DropdownMenuSubTrigger>
120+
<DropdownMenuSubContent>
121+
{renderMoveOptions(moveOptions!, onMove!)}
122+
</DropdownMenuSubContent>
123+
</DropdownMenuSub>
124+
)}
125+
{hasActionsAboveDestructive && <DropdownMenuSeparator />}
126+
<DropdownMenuItem onSelect={onDelete}>
127+
<Trash />
128+
{selectionActionLabel('Delete', selectedCount)}
129+
</DropdownMenuItem>
130+
</>
119131
)}
120-
{hasActionsAboveDestructive && <DropdownMenuSeparator />}
121-
<DropdownMenuItem onSelect={onDelete}>
122-
<Trash />
123-
{selectionActionLabel('Delete', selectedCount)}
124-
</DropdownMenuItem>
125132
</>
126133
)}
127134
</DropdownMenuContent>

apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ vi.mock('@sim/emcn/icons', () => ({
2323
FolderInput: () => null,
2424
Pencil: () => null,
2525
Pin: () => null,
26+
Plus: () => null,
2627
SquareArrowUpRight: () => null,
2728
TagIcon: () => null,
2829
Trash: () => null,
@@ -37,6 +38,8 @@ vi.mock('@/app/workspace/[workspaceId]/components/folders/move-options', () => (
3738
}))
3839

3940
import { FolderContextMenu } from '@/app/workspace/[workspaceId]/components/folders/folder-context-menu'
41+
import { ChunkContextMenu } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu'
42+
import { DocumentContextMenu } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu'
4043
import { KnowledgeBaseContextMenu } from '@/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu'
4144
import { TableContextMenu } from '@/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu'
4245

@@ -124,4 +127,62 @@ describe('selection-aware resource context menus', () => {
124127
expect(menu).not.toContain('Copy ID')
125128
expect(menu).not.toContain('Pin')
126129
})
130+
131+
it('explains when a read-only multi-folder selection has no actions', () => {
132+
const menu = renderToStaticMarkup(
133+
<FolderContextMenu
134+
isOpen
135+
position={POSITION}
136+
onClose={() => {}}
137+
onOpen={() => {}}
138+
onRename={() => {}}
139+
onDelete={() => {}}
140+
onTogglePin={() => {}}
141+
pinned={false}
142+
canEdit={false}
143+
selectedCount={2}
144+
/>
145+
)
146+
147+
expect(menu).toContain('No actions available')
148+
expect(menu).not.toContain('Open')
149+
expect(menu).not.toContain('Delete')
150+
})
151+
152+
it('counts only the documents affected by a mixed-selection toggle', () => {
153+
const menu = renderToStaticMarkup(
154+
<DocumentContextMenu
155+
isOpen
156+
position={POSITION}
157+
onClose={() => {}}
158+
hasDocument
159+
selectedCount={25}
160+
enabledCount={7}
161+
disabledCount={18}
162+
onToggleEnabled={() => {}}
163+
onDelete={() => {}}
164+
/>
165+
)
166+
167+
expect(menu).toContain('Enable 18 items')
168+
expect(menu).toContain('Delete 25 items')
169+
})
170+
171+
it('counts only the chunks affected by a multi-selection toggle', () => {
172+
const menu = renderToStaticMarkup(
173+
<ChunkContextMenu
174+
isOpen
175+
position={POSITION}
176+
onClose={() => {}}
177+
hasChunk
178+
selectedCount={3}
179+
enabledCount={3}
180+
onToggleEnabled={() => {}}
181+
onDelete={() => {}}
182+
/>
183+
)
184+
185+
expect(menu).toContain('Disable 3 items')
186+
expect(menu).toContain('Delete 3 items')
187+
})
127188
})

apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import {
33
selectionActionLabel,
44
selectionLabel,
5+
selectionToggleActionLabel,
56
} from '@/app/workspace/[workspaceId]/components/resource/selection-label'
67

78
describe('selection labels', () => {
@@ -20,4 +21,26 @@ describe('selection labels', () => {
2021
it('states the scope of a multi-row action', () => {
2122
expect(selectionActionLabel('Delete', 3)).toBe('Delete 3 items')
2223
})
24+
25+
it('counts only disabled items for a mixed-selection enable action', () => {
26+
expect(
27+
selectionToggleActionLabel({
28+
selectedCount: 5,
29+
enabledCount: 2,
30+
disabledCount: 3,
31+
isSelectedItemEnabled: true,
32+
})
33+
).toBe('Enable 3 items')
34+
})
35+
36+
it('counts enabled items when a selection can only be disabled', () => {
37+
expect(
38+
selectionToggleActionLabel({
39+
selectedCount: 4,
40+
enabledCount: 4,
41+
disabledCount: 0,
42+
isSelectedItemEnabled: true,
43+
})
44+
).toBe('Disable 4 items')
45+
})
2346
})

apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,21 @@ export function selectionActionLabel(
1616
if (selectedCount <= 1) return singleItemLabel
1717
return `${action} ${selectedCount} items`
1818
}
19+
20+
interface SelectionToggleActionLabelOptions {
21+
selectedCount: number
22+
enabledCount: number
23+
disabledCount: number
24+
isSelectedItemEnabled: boolean
25+
}
26+
27+
export function selectionToggleActionLabel({
28+
selectedCount,
29+
enabledCount,
30+
disabledCount,
31+
isSelectedItemEnabled,
32+
}: SelectionToggleActionLabelOptions): string {
33+
if (selectedCount <= 1) return isSelectedItemEnabled ? 'Disable' : 'Enable'
34+
if (disabledCount > 0) return selectionActionLabel('Enable', disabledCount)
35+
return selectionActionLabel('Disable', enabledCount)
36+
}

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
DropdownMenuTrigger,
99
} from '@sim/emcn'
1010
import { Duplicate, Eye, Pencil, Plus, SquareArrowUpRight, Trash } from '@sim/emcn/icons'
11-
import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label'
11+
import {
12+
selectionActionLabel,
13+
selectionToggleActionLabel,
14+
} from '@/app/workspace/[workspaceId]/components/resource/selection-label'
1215

1316
interface ChunkContextMenuProps {
1417
isOpen: boolean
@@ -59,14 +62,12 @@ export function ChunkContextMenu({
5962
disabledCount = 0,
6063
}: ChunkContextMenuProps) {
6164
const isMultiSelect = selectedCount > 1
62-
63-
const getToggleLabel = () => {
64-
if (isMultiSelect) {
65-
if (disabledCount > 0) return 'Enable'
66-
return 'Disable'
67-
}
68-
return isChunkEnabled ? 'Disable' : 'Enable'
69-
}
65+
const toggleLabel = selectionToggleActionLabel({
66+
selectedCount,
67+
enabledCount,
68+
disabledCount,
69+
isSelectedItemEnabled: isChunkEnabled,
70+
})
7071

7172
const hasNavigationSection = !isMultiSelect && !!onOpenInNewTab
7273
const hasEditSection = !isMultiSelect && (!!onEdit || !!onCopyContent)
@@ -119,7 +120,7 @@ export function ChunkContextMenu({
119120
{onToggleEnabled && (
120121
<DropdownMenuItem disabled={disableToggleEnabled} onSelect={onToggleEnabled}>
121122
<Eye />
122-
{selectionActionLabel(getToggleLabel(), selectedCount)}
123+
{toggleLabel}
123124
</DropdownMenuItem>
124125
)}
125126

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ export function KnowledgeBase({
908908
? 0
909909
: pagination.total
910910
: selectedDocumentsList.filter((doc) => !doc.enabled).length
911+
const selectedDocumentCount = isSelectAllMode ? pagination.total : selectedDocuments.size
911912

912913
const handleDocumentContextMenu = useCallback(
913914
(e: React.MouseEvent, docId: string) => {
@@ -1501,11 +1502,11 @@ export function KnowledgeBase({
15011502
onClose={handleContextMenuClose}
15021503
hasDocument={contextMenuDocument !== null}
15031504
isDocumentEnabled={contextMenuDocument?.enabled ?? true}
1504-
selectedCount={selectedDocuments.size}
1505+
selectedCount={selectedDocumentCount}
15051506
enabledCount={enabledCount}
15061507
disabledCount={disabledCount}
15071508
onOpenInNewTab={
1508-
contextMenuDocument && selectedDocuments.size === 1
1509+
contextMenuDocument && selectedDocumentCount === 1
15091510
? () => {
15101511
const urlParams = new URLSearchParams({
15111512
kbName: knowledgeBaseName,
@@ -1519,14 +1520,14 @@ export function KnowledgeBase({
15191520
: undefined
15201521
}
15211522
onOpenSource={
1522-
contextMenuDocument?.sourceUrl && selectedDocuments.size === 1
1523+
contextMenuDocument?.sourceUrl && selectedDocumentCount === 1
15231524
? () => window.open(contextMenuDocument.sourceUrl!, '_blank', 'noopener,noreferrer')
15241525
: undefined
15251526
}
15261527
onRename={contextMenuDocument ? () => handleRenameDocument(contextMenuDocument) : undefined}
15271528
onToggleEnabled={
15281529
contextMenuDocument
1529-
? selectedDocuments.size > 1
1530+
? selectedDocumentCount > 1
15301531
? () => {
15311532
if (disabledCount > 0) {
15321533
handleBulkEnable()
@@ -1538,13 +1539,13 @@ export function KnowledgeBase({
15381539
: undefined
15391540
}
15401541
onViewTags={
1541-
contextMenuDocument && selectedDocuments.size === 1 && userPermissions.canEdit
1542+
contextMenuDocument && selectedDocumentCount === 1 && userPermissions.canEdit
15421543
? () => handleViewDocumentTags(contextMenuDocument)
15431544
: undefined
15441545
}
15451546
onDelete={
15461547
contextMenuDocument
1547-
? selectedDocuments.size > 1
1548+
? selectedDocumentCount > 1
15481549
? handleBulkDelete
15491550
: () => handleDeleteDocument(contextMenuDocument.id)
15501551
: undefined

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
DropdownMenuTrigger,
99
} from '@sim/emcn'
1010
import { Eye, Pencil, Plus, SquareArrowUpRight, TagIcon, Trash } from '@sim/emcn/icons'
11-
import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label'
11+
import {
12+
selectionActionLabel,
13+
selectionToggleActionLabel,
14+
} from '@/app/workspace/[workspaceId]/components/resource/selection-label'
1215

1316
interface DocumentContextMenuProps {
1417
isOpen: boolean
@@ -59,14 +62,12 @@ export function DocumentContextMenu({
5962
disabledCount = 0,
6063
}: DocumentContextMenuProps) {
6164
const isMultiSelect = selectedCount > 1
62-
63-
const getToggleLabel = () => {
64-
if (isMultiSelect) {
65-
if (disabledCount > 0) return 'Enable'
66-
return 'Disable'
67-
}
68-
return isDocumentEnabled ? 'Disable' : 'Enable'
69-
}
65+
const toggleLabel = selectionToggleActionLabel({
66+
selectedCount,
67+
enabledCount,
68+
disabledCount,
69+
isSelectedItemEnabled: isDocumentEnabled,
70+
})
7071

7172
const hasNavigationSection = !isMultiSelect && (!!onOpenInNewTab || !!onOpenSource)
7273
const hasEditSection = !isMultiSelect && (!!onRename || !!onViewTags)
@@ -125,7 +126,7 @@ export function DocumentContextMenu({
125126
{onToggleEnabled && (
126127
<DropdownMenuItem disabled={disableToggleEnabled} onSelect={onToggleEnabled}>
127128
<Eye />
128-
{selectionActionLabel(getToggleLabel(), selectedCount)}
129+
{toggleLabel}
129130
</DropdownMenuItem>
130131
)}
131132

0 commit comments

Comments
 (0)