From 93c74ba4ed45475d515cfdb9532e5c2d2e5b8343 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 22 Aug 2026 12:05:07 -0700 Subject: [PATCH 1/4] fix(resources): make context menus selection-aware --- .../folders/folder-context-menu.tsx | 51 ++++--- .../selection-aware-context-menus.test.tsx | 127 ++++++++++++++++++ .../resource/selection-label.test.ts | 23 ++++ .../components/resource/selection-label.ts | 9 ++ .../file-row-context-menu.tsx | 7 +- .../chunk-context-menu/chunk-context-menu.tsx | 9 +- .../document-context-menu.tsx | 9 +- .../knowledge-base-context-menu.tsx | 23 ++-- .../[workspaceId]/knowledge/knowledge.tsx | 2 + .../table-context-menu/table-context-menu.tsx | 32 ++--- .../workspace/[workspaceId]/tables/tables.tsx | 2 + .../context-menu/context-menu.test.tsx | 13 +- .../components/context-menu/context-menu.tsx | 9 +- .../components/folder-item/folder-item.tsx | 9 +- .../workflow-item/workflow-item.tsx | 5 + .../w/components/sidebar/sidebar.tsx | 1 + 16 files changed, 270 insertions(+), 61 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx index 62d0ec5cacf..3b02e9f5e46 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx @@ -14,6 +14,7 @@ import { import { Duplicate, Eye, FolderInput, Pencil, Pin, Trash } from '@sim/emcn/icons' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/components/folders/move-options' import { renderMoveOptions } from '@/app/workspace/[workspaceId]/components/folders/move-options' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface FolderContextMenuProps { isOpen: boolean @@ -29,6 +30,7 @@ interface FolderContextMenuProps { pinned: boolean moveOptions?: MoveOptionNode[] canEdit: boolean + selectedCount: number } /** @@ -56,8 +58,11 @@ export const FolderContextMenu = memo(function FolderContextMenu({ pinned, moveOptions, canEdit, + selectedCount, }: FolderContextMenuProps) { + const isMultiSelect = selectedCount > 1 const hasMove = Boolean(onMove && moveOptions && moveOptions.length > 0) + const hasActionsAboveDestructive = !isMultiSelect || hasMove return ( !open && onClose()} modal={false}> @@ -75,41 +80,47 @@ export const FolderContextMenu = memo(function FolderContextMenu({ sideOffset={4} onCloseAutoFocus={(e) => e.preventDefault()} > - - - Open - - - - {pinned ? 'Unpin' : 'Pin'} - - {onCopyId && ( - - - Copy ID - + {!isMultiSelect && ( + <> + + + Open + + + + {pinned ? 'Unpin' : 'Pin'} + + {onCopyId && ( + + + Copy ID + + )} + )} {canEdit && ( <> - - - Rename - + {!isMultiSelect && ( + + + Rename + + )} {hasMove && ( - Move to + {selectionActionLabel('Move', selectedCount, 'Move to')} {renderMoveOptions(moveOptions!, onMove!)} )} - + {hasActionsAboveDestructive && } - Delete + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx new file mode 100644 index 00000000000..65019c4e8c3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx @@ -0,0 +1,127 @@ +import type { ReactNode } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/emcn', () => ({ + DropdownMenu: ({ children, open }: { children: ReactNode; open: boolean }) => + open ? <>{children} : null, + DropdownMenuContent: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuItem: ({ children }: { children: ReactNode }) => {children}, + DropdownMenuSeparator: () =>
, + DropdownMenuSub: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuSubContent: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuSubTrigger: ({ children }: { children: ReactNode }) => {children}, + DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}, + Upload: () => null, +})) + +vi.mock('@sim/emcn/icons', () => ({ + Database: () => null, + Download: () => null, + Duplicate: () => null, + Eye: () => null, + FolderInput: () => null, + Pencil: () => null, + Pin: () => null, + SquareArrowUpRight: () => null, + TagIcon: () => null, + Trash: () => null, +})) + +vi.mock('@/app/workspace/[workspaceId]/components/folders', () => ({ + renderMoveOptions: () => Destination, +})) + +vi.mock('@/app/workspace/[workspaceId]/components/folders/move-options', () => ({ + renderMoveOptions: () => Destination, +})) + +import { FolderContextMenu } from '@/app/workspace/[workspaceId]/components/folders/folder-context-menu' +import { KnowledgeBaseContextMenu } from '@/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu' +import { TableContextMenu } from '@/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu' + +const POSITION = { x: 0, y: 0 } +const MOVE_OPTIONS = [{ value: '__root__', label: 'Root', children: [] }] + +describe('selection-aware resource context menus', () => { + it('limits a multi-table menu to actions that can target the selection', () => { + const menu = renderToStaticMarkup( + {}} + onCopyId={() => {}} + onTogglePin={() => {}} + onDelete={() => {}} + onViewSchema={() => {}} + onRename={() => {}} + onImportCsv={() => {}} + onExportCsv={() => {}} + onMove={() => {}} + moveOptions={MOVE_OPTIONS} + selectedCount={3} + /> + ) + + expect(menu).toContain('Move 3 items') + expect(menu).toContain('Delete 3 items') + expect(menu).not.toContain('View Schema') + expect(menu).not.toContain('Rename') + expect(menu).not.toContain('Copy ID') + expect(menu).not.toContain('Pin') + }) + + it('limits a multi-base menu to actions that can target the selection', () => { + const menu = renderToStaticMarkup( + {}} + onOpenInNewTab={() => {}} + onViewTags={() => {}} + onCopyId={() => {}} + onTogglePin={() => {}} + onEdit={() => {}} + onDelete={() => {}} + onMove={() => {}} + moveOptions={MOVE_OPTIONS} + selectedCount={2} + /> + ) + + expect(menu).toContain('Move 2 items') + expect(menu).toContain('Delete 2 items') + expect(menu).not.toContain('Open in new tab') + expect(menu).not.toContain('View tags') + expect(menu).not.toContain('Copy ID') + expect(menu).not.toContain('Pin') + expect(menu).not.toContain('Edit') + }) + + it('uses the same group-action contract when a selected folder opens the menu', () => { + const menu = renderToStaticMarkup( + {}} + onOpen={() => {}} + onRename={() => {}} + onDelete={() => {}} + onCopyId={() => {}} + onMove={() => {}} + onTogglePin={() => {}} + pinned={false} + moveOptions={MOVE_OPTIONS} + canEdit + selectedCount={4} + /> + ) + + expect(menu).toContain('Move 4 items') + expect(menu).toContain('Delete 4 items') + expect(menu).not.toContain('Open') + expect(menu).not.toContain('Rename') + expect(menu).not.toContain('Copy ID') + expect(menu).not.toContain('Pin') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts new file mode 100644 index 00000000000..5937729d5cc --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { + selectionActionLabel, + selectionLabel, +} from '@/app/workspace/[workspaceId]/components/resource/selection-label' + +describe('selection labels', () => { + it('uses the selected item name for a single-row confirmation', () => { + expect(selectionLabel(1, 'Quarterly data')).toBe('Quarterly data') + }) + + it('uses the selection count for a multi-row confirmation', () => { + expect(selectionLabel(3, 'Quarterly data')).toBe('3 selected items') + }) + + it('keeps single-row action labels terse', () => { + expect(selectionActionLabel('Move', 1, 'Move to')).toBe('Move to') + }) + + it('states the scope of a multi-row action', () => { + expect(selectionActionLabel('Delete', 3)).toBe('Delete 3 items') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts index fc812dbee55..585035b175a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts @@ -7,3 +7,12 @@ export function selectionLabel(count: number, firstName: string | undefined): st if (count === 1) return firstName ?? 'selected item' return `${count} selected items` } + +export function selectionActionLabel( + action: string, + selectedCount: number, + singleItemLabel = action +): string { + if (selectedCount <= 1) return singleItemLabel + return `${action} ${selectedCount} items` +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index fbc651748aa..79ffba59465 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -18,6 +18,7 @@ import { import { Download, Link, Pin, Send, Trash } from '@sim/emcn/icons' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/components/folders' import { renderMoveOption } from '@/app/workspace/[workspaceId]/components/folders' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface FileRowContextMenuProps { isOpen: boolean @@ -98,7 +99,7 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ {onDownload && ( - {isMultiSelect ? `Download ${selectedCount} items` : 'Download'} + {selectionActionLabel('Download', selectedCount)} )} {!isMultiSelect && ( @@ -125,7 +126,7 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ - {isMultiSelect ? `Move ${selectedCount} items` : 'Move to'} + {selectionActionLabel('Move', selectedCount, 'Move to')} onMove(moveOptions[0].value)}> @@ -140,7 +141,7 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ {hasActionsAboveDestructive && } - {isMultiSelect ? `Delete ${selectedCount} items` : 'Delete'} + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index 8a73d105395..0faa8eca1c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -8,6 +8,7 @@ import { DropdownMenuTrigger, } from '@sim/emcn' import { Duplicate, Eye, Pencil, Plus, SquareArrowUpRight, Trash } from '@sim/emcn/icons' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface ChunkContextMenuProps { isOpen: boolean @@ -26,7 +27,7 @@ interface ChunkContextMenuProps { disableAddChunk?: boolean disableEdit?: boolean isConnectorDocument?: boolean - selectedCount?: number + selectedCount: number enabledCount?: number disabledCount?: number } @@ -53,7 +54,7 @@ export function ChunkContextMenu({ disableAddChunk = false, disableEdit = false, isConnectorDocument = false, - selectedCount = 1, + selectedCount, enabledCount = 0, disabledCount = 0, }: ChunkContextMenuProps) { @@ -118,7 +119,7 @@ export function ChunkContextMenu({ {onToggleEnabled && ( - {getToggleLabel()} + {selectionActionLabel(getToggleLabel(), selectedCount)} )} @@ -126,7 +127,7 @@ export function ChunkContextMenu({ {onDelete && ( - Delete + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index 3f3cb03f63c..e3916e24179 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -8,6 +8,7 @@ import { DropdownMenuTrigger, } from '@sim/emcn' import { Eye, Pencil, Plus, SquareArrowUpRight, TagIcon, Trash } from '@sim/emcn/icons' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface DocumentContextMenuProps { isOpen: boolean @@ -26,7 +27,7 @@ interface DocumentContextMenuProps { disableToggleEnabled?: boolean disableDelete?: boolean disableAddDocument?: boolean - selectedCount?: number + selectedCount: number enabledCount?: number disabledCount?: number } @@ -53,7 +54,7 @@ export function DocumentContextMenu({ disableToggleEnabled = false, disableDelete = false, disableAddDocument = false, - selectedCount = 1, + selectedCount, enabledCount = 0, disabledCount = 0, }: DocumentContextMenuProps) { @@ -124,7 +125,7 @@ export function DocumentContextMenu({ {onToggleEnabled && ( - {getToggleLabel()} + {selectionActionLabel(getToggleLabel(), selectedCount)} )} @@ -132,7 +133,7 @@ export function DocumentContextMenu({ {onDelete && ( - Delete + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx index 2f5fdad09e7..83ce9100c4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx @@ -22,6 +22,7 @@ import { } from '@sim/emcn/icons' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/components/folders' import { renderMoveOptions } from '@/app/workspace/[workspaceId]/components/folders' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface KnowledgeBaseContextMenuProps { isOpen: boolean @@ -44,6 +45,7 @@ interface KnowledgeBaseContextMenuProps { showDelete?: boolean disableEdit?: boolean disableDelete?: boolean + selectedCount: number } /** @@ -69,11 +71,14 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ showDelete = true, disableEdit = false, disableDelete = false, + selectedCount, }: KnowledgeBaseContextMenuProps) { - const hasNavigationSection = showOpenInNewTab && !!onOpenInNewTab - const hasInfoSection = (showViewTags && !!onViewTags) || !!onCopyId || !!onTogglePin + const isMultiSelect = selectedCount > 1 + const hasNavigationSection = !isMultiSelect && showOpenInNewTab && !!onOpenInNewTab + const hasInfoSection = + !isMultiSelect && ((showViewTags && !!onViewTags) || !!onCopyId || !!onTogglePin) const hasMoveSection = !disableEdit && !!onMove && !!moveOptions && moveOptions.length > 0 - const hasEditSection = (showEdit && !!onEdit) || hasMoveSection + const hasEditSection = (!isMultiSelect && showEdit && !!onEdit) || hasMoveSection const hasDestructiveSection = showDelete && !!onDelete const hasActionsAboveDestructive = hasNavigationSection || hasInfoSection || hasEditSection @@ -105,25 +110,25 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ Open in new tab )} - {showViewTags && onViewTags && ( + {!isMultiSelect && showViewTags && onViewTags && ( View tags )} - {onCopyId && ( + {!isMultiSelect && onCopyId && ( Copy ID )} - {onTogglePin && ( + {!isMultiSelect && onTogglePin && ( {pinned ? 'Unpin' : 'Pin'} )} - {showEdit && onEdit && ( + {!isMultiSelect && showEdit && onEdit && ( Edit @@ -134,7 +139,7 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ - Move to + {selectionActionLabel('Move', selectedCount, 'Move to')} {renderMoveOptions(moveOptions!, onMove!)} @@ -146,7 +151,7 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ {showDelete && onDelete && ( - Delete + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 8ed1934a1a2..2c8258ad4f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -1462,6 +1462,7 @@ export function Knowledge() { showDelete disableEdit={!canEdit} disableDelete={!canEdit} + selectedCount={selectedRowIds.size} /> )} @@ -1479,6 +1480,7 @@ export function Knowledge() { onMove={handleMoveFolderFromMenu} moveOptions={activeFolderMoveOptions} canEdit={canEdit} + selectedCount={selectedRowIds.size} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx index 9bb7fbef27d..b7897ce9b03 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx @@ -14,6 +14,7 @@ import { import { Database, Download, Duplicate, FolderInput, Pencil, Pin, Trash } from '@sim/emcn/icons' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/components/folders' import { renderMoveOptions } from '@/app/workspace/[workspaceId]/components/folders' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface TableContextMenuProps { isOpen: boolean @@ -35,6 +36,7 @@ interface TableContextMenuProps { disableRename?: boolean disableImport?: boolean disableExport?: boolean + selectedCount: number menuRef?: React.RefObject } @@ -56,7 +58,11 @@ export function TableContextMenu({ disableRename = false, disableImport = false, disableExport = false, + selectedCount, }: TableContextMenuProps) { + const isMultiSelect = selectedCount > 1 + const hasMoveAction = !!(onMove && moveOptions && moveOptions.length > 0) + /** * `Move to` needs a NON-EMPTY `moveOptions`, not just the handler — the looser * `onMove` alone draws the rule with nothing above it for a table whose other @@ -66,13 +72,9 @@ export function TableContextMenu({ * group, with both sides built from the items' exact render conditions. */ const hasActionsAboveDestructive = - onViewSchema || - onRename || - onImportCsv || - onExportCsv || - (onMove && moveOptions && moveOptions.length > 0) || - onCopyId || - onTogglePin + hasMoveAction || + (!isMultiSelect && + !!(onViewSchema || onRename || onImportCsv || onExportCsv || onCopyId || onTogglePin)) return ( !open && onClose()} modal={false}> @@ -96,25 +98,25 @@ export function TableContextMenu({ sideOffset={4} onCloseAutoFocus={(e) => e.preventDefault()} > - {onViewSchema && ( + {!isMultiSelect && onViewSchema && ( View Schema )} - {onRename && ( + {!isMultiSelect && onRename && ( Rename )} - {onImportCsv && ( + {!isMultiSelect && onImportCsv && ( Import CSV )} - {onExportCsv && ( + {!isMultiSelect && onExportCsv && ( Export CSV @@ -124,20 +126,20 @@ export function TableContextMenu({ - Move to + {selectionActionLabel('Move', selectedCount, 'Move to')} {renderMoveOptions(moveOptions, onMove)} )} - {onTogglePin && ( + {!isMultiSelect && onTogglePin && ( {pinned ? 'Unpin' : 'Pin'} )} - {onCopyId && ( + {!isMultiSelect && onCopyId && ( Copy ID @@ -147,7 +149,7 @@ export function TableContextMenu({ {onDelete && ( - Delete + {selectionActionLabel('Delete', selectedCount)} )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index c6d3c51f10f..4410f1bd017 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -1395,6 +1395,7 @@ export function Tables() { disableDelete={!canEdit} disableRename={!canEdit} disableImport={!canEdit} + selectedCount={selectedRowIds.size} /> {activeTable && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx index 12b7d7c8902..2b73e2dc4a7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx @@ -35,7 +35,7 @@ function mountSurroundingMenu() { surroundingMenuItem = item } -function renderMenu(onClose: () => void, onDelete: () => void = () => {}) { +function renderMenu(onClose: () => void, onDelete: () => void = () => {}, selectedCount = 1) { ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true mountSurroundingMenu() plainButton = document.createElement('button') @@ -53,6 +53,7 @@ function renderMenu(onClose: () => void, onDelete: () => void = () => {}) { onDelete={onDelete} showRename={false} showDuplicate={false} + selectedCount={selectedCount} /> ) ) @@ -136,6 +137,16 @@ describe('sidebar context menu dismissal', () => { expect(onClose).toHaveBeenCalled() }) + + it('states when delete applies to a multi-item selection', () => { + renderMenu(vi.fn(), vi.fn(), 3) + + expect( + Array.from(document.querySelectorAll('[role="menuitem"]')).some( + (item) => item.textContent === 'Delete 3 items' + ) + ).toBe(true) + }) }) describe('separators', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 3d4cfef1a2e..447b69008e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -26,6 +26,7 @@ import { Unlock, X, } from '@sim/emcn/icons' +import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface ContextMenuProps { isOpen: boolean @@ -96,6 +97,7 @@ interface ContextMenuProps { onUploadLogo?: () => void showUploadLogo?: boolean disableUploadLogo?: boolean + selectedCount?: number } /** @@ -164,6 +166,7 @@ export function ContextMenu({ onUploadLogo, showUploadLogo = false, disableUploadLogo = false, + selectedCount = 1, }: ContextMenuProps) { const hasActionsAboveDestructive = (showOpenInNewTab && onOpenInNewTab) || @@ -345,7 +348,7 @@ export function ContextMenu({ }} > - Duplicate + {selectionActionLabel('Duplicate', selectedCount)} )} {showExport && onExport && ( @@ -357,7 +360,7 @@ export function ContextMenu({ }} > - Export + {selectionActionLabel('Export', selectedCount)} )} {openInNewTabPosition === 'last' && showOpenInNewTab && onOpenInNewTab && ( @@ -394,7 +397,7 @@ export function ContextMenu({ }} > - Delete + {selectionActionLabel('Delete', selectedCount)} )} {showCloseTab && onCloseTab && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 0cc55efc27c..4877ba28400 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -475,6 +475,10 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold const isMixedSelection = useMemo(() => { return capturedSelectionRef.current?.isMixed ?? false }, [isContextMenuOpen]) + const contextMenuSelectedCount = capturedSelectionRef.current + ? capturedSelectionRef.current.workflowIds.length + + capturedSelectionRef.current.folderIds.length + : 1 const hasExportableContent = useMemo(() => { if (!capturedSelectionRef.current) return hasWorkflows @@ -583,8 +587,8 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold onDuplicate={handleDuplicate} onExport={handleExport} onDelete={handleOpenDeleteModal} - showCreate={!isMixedSelection} - showCreateFolder={!isMixedSelection} + showCreate={!isMixedSelection && selectedFolders.size <= 1} + showCreateFolder={!isMixedSelection && selectedFolders.size <= 1} showRename={!isMixedSelection && selectedFolders.size <= 1} showDuplicate={true} showExport={true} @@ -605,6 +609,7 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold showLock={!isMixedSelection && selectedFolders.size <= 1} disableLock={!userPermissions.canAdmin || inheritedFolderLocked} isLocked={effectiveLocked} + selectedCount={contextMenuSelectedCount} /> { return capturedSelectionRef.current?.isMixed ?? false }, [isContextMenuOpen]) + const contextMenuSelectedCount = capturedSelectionRef.current + ? capturedSelectionRef.current.workflowIds.length + + capturedSelectionRef.current.folderIds.length + : 1 const captureSelectionState = useCallback(() => { const store = useFolderStore.getState() @@ -503,6 +507,7 @@ export const WorkflowItem = memo(function WorkflowItem({ showLock={!isMixedSelection && selectedWorkflows.size <= 1} disableLock={!userPermissions.canAdmin || inheritedFolderLocked} isLocked={effectiveLocked} + selectedCount={contextMenuSelectedCount} /> Date: Sat, 22 Aug 2026 15:23:27 -0700 Subject: [PATCH 2/4] fix(resources): correct bulk action scopes --- .../folders/folder-context-menu.tsx | 83 ++++++++++--------- .../selection-aware-context-menus.test.tsx | 61 ++++++++++++++ .../resource/selection-label.test.ts | 23 +++++ .../components/resource/selection-label.ts | 18 ++++ .../chunk-context-menu/chunk-context-menu.tsx | 21 ++--- .../[workspaceId]/knowledge/[id]/base.tsx | 13 +-- .../document-context-menu.tsx | 21 ++--- 7 files changed, 176 insertions(+), 64 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx index 3b02e9f5e46..35f72a05818 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx @@ -63,6 +63,7 @@ export const FolderContextMenu = memo(function FolderContextMenu({ const isMultiSelect = selectedCount > 1 const hasMove = Boolean(onMove && moveOptions && moveOptions.length > 0) const hasActionsAboveDestructive = !isMultiSelect || hasMove + const hasAvailableActions = !isMultiSelect || canEdit return ( !open && onClose()} modal={false}> @@ -80,48 +81,54 @@ export const FolderContextMenu = memo(function FolderContextMenu({ sideOffset={4} onCloseAutoFocus={(e) => e.preventDefault()} > - {!isMultiSelect && ( - <> - - - Open - - - - {pinned ? 'Unpin' : 'Pin'} - - {onCopyId && ( - - - Copy ID - - )} - - )} - {canEdit && ( + {!hasAvailableActions ? ( + No actions available + ) : ( <> {!isMultiSelect && ( - - - Rename - + <> + + + Open + + + + {pinned ? 'Unpin' : 'Pin'} + + {onCopyId && ( + + + Copy ID + + )} + )} - {hasMove && ( - - - - {selectionActionLabel('Move', selectedCount, 'Move to')} - - - {renderMoveOptions(moveOptions!, onMove!)} - - + {canEdit && ( + <> + {!isMultiSelect && ( + + + Rename + + )} + {hasMove && ( + + + + {selectionActionLabel('Move', selectedCount, 'Move to')} + + + {renderMoveOptions(moveOptions!, onMove!)} + + + )} + {hasActionsAboveDestructive && } + + + {selectionActionLabel('Delete', selectedCount)} + + )} - {hasActionsAboveDestructive && } - - - {selectionActionLabel('Delete', selectedCount)} - )} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx index 65019c4e8c3..23b6f46b707 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx @@ -23,6 +23,7 @@ vi.mock('@sim/emcn/icons', () => ({ FolderInput: () => null, Pencil: () => null, Pin: () => null, + Plus: () => null, SquareArrowUpRight: () => null, TagIcon: () => null, Trash: () => null, @@ -37,6 +38,8 @@ vi.mock('@/app/workspace/[workspaceId]/components/folders/move-options', () => ( })) import { FolderContextMenu } from '@/app/workspace/[workspaceId]/components/folders/folder-context-menu' +import { ChunkContextMenu } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu' +import { DocumentContextMenu } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu' import { KnowledgeBaseContextMenu } from '@/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu' import { TableContextMenu } from '@/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu' @@ -124,4 +127,62 @@ describe('selection-aware resource context menus', () => { expect(menu).not.toContain('Copy ID') expect(menu).not.toContain('Pin') }) + + it('explains when a read-only multi-folder selection has no actions', () => { + const menu = renderToStaticMarkup( + {}} + onOpen={() => {}} + onRename={() => {}} + onDelete={() => {}} + onTogglePin={() => {}} + pinned={false} + canEdit={false} + selectedCount={2} + /> + ) + + expect(menu).toContain('No actions available') + expect(menu).not.toContain('Open') + expect(menu).not.toContain('Delete') + }) + + it('counts only the documents affected by a mixed-selection toggle', () => { + const menu = renderToStaticMarkup( + {}} + hasDocument + selectedCount={25} + enabledCount={7} + disabledCount={18} + onToggleEnabled={() => {}} + onDelete={() => {}} + /> + ) + + expect(menu).toContain('Enable 18 items') + expect(menu).toContain('Delete 25 items') + }) + + it('counts only the chunks affected by a multi-selection toggle', () => { + const menu = renderToStaticMarkup( + {}} + hasChunk + selectedCount={3} + enabledCount={3} + onToggleEnabled={() => {}} + onDelete={() => {}} + /> + ) + + expect(menu).toContain('Disable 3 items') + expect(menu).toContain('Delete 3 items') + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts index 5937729d5cc..23ab0d9e4e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { selectionActionLabel, selectionLabel, + selectionToggleActionLabel, } from '@/app/workspace/[workspaceId]/components/resource/selection-label' describe('selection labels', () => { @@ -20,4 +21,26 @@ describe('selection labels', () => { it('states the scope of a multi-row action', () => { expect(selectionActionLabel('Delete', 3)).toBe('Delete 3 items') }) + + it('counts only disabled items for a mixed-selection enable action', () => { + expect( + selectionToggleActionLabel({ + selectedCount: 5, + enabledCount: 2, + disabledCount: 3, + isSelectedItemEnabled: true, + }) + ).toBe('Enable 3 items') + }) + + it('counts enabled items when a selection can only be disabled', () => { + expect( + selectionToggleActionLabel({ + selectedCount: 4, + enabledCount: 4, + disabledCount: 0, + isSelectedItemEnabled: true, + }) + ).toBe('Disable 4 items') + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts index 585035b175a..334f83618d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts @@ -16,3 +16,21 @@ export function selectionActionLabel( if (selectedCount <= 1) return singleItemLabel return `${action} ${selectedCount} items` } + +interface SelectionToggleActionLabelOptions { + selectedCount: number + enabledCount: number + disabledCount: number + isSelectedItemEnabled: boolean +} + +export function selectionToggleActionLabel({ + selectedCount, + enabledCount, + disabledCount, + isSelectedItemEnabled, +}: SelectionToggleActionLabelOptions): string { + if (selectedCount <= 1) return isSelectedItemEnabled ? 'Disable' : 'Enable' + if (disabledCount > 0) return selectionActionLabel('Enable', disabledCount) + return selectionActionLabel('Disable', enabledCount) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index 0faa8eca1c2..e7488424da5 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -8,7 +8,10 @@ import { DropdownMenuTrigger, } from '@sim/emcn' import { Duplicate, Eye, Pencil, Plus, SquareArrowUpRight, Trash } from '@sim/emcn/icons' -import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' +import { + selectionActionLabel, + selectionToggleActionLabel, +} from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface ChunkContextMenuProps { isOpen: boolean @@ -59,14 +62,12 @@ export function ChunkContextMenu({ disabledCount = 0, }: ChunkContextMenuProps) { const isMultiSelect = selectedCount > 1 - - const getToggleLabel = () => { - if (isMultiSelect) { - if (disabledCount > 0) return 'Enable' - return 'Disable' - } - return isChunkEnabled ? 'Disable' : 'Enable' - } + const toggleLabel = selectionToggleActionLabel({ + selectedCount, + enabledCount, + disabledCount, + isSelectedItemEnabled: isChunkEnabled, + }) const hasNavigationSection = !isMultiSelect && !!onOpenInNewTab const hasEditSection = !isMultiSelect && (!!onEdit || !!onCopyContent) @@ -119,7 +120,7 @@ export function ChunkContextMenu({ {onToggleEnabled && ( - {selectionActionLabel(getToggleLabel(), selectedCount)} + {toggleLabel} )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 76a061a032d..0450cba4573 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -908,6 +908,7 @@ export function KnowledgeBase({ ? 0 : pagination.total : selectedDocumentsList.filter((doc) => !doc.enabled).length + const selectedDocumentCount = isSelectAllMode ? pagination.total : selectedDocuments.size const handleDocumentContextMenu = useCallback( (e: React.MouseEvent, docId: string) => { @@ -1501,11 +1502,11 @@ export function KnowledgeBase({ onClose={handleContextMenuClose} hasDocument={contextMenuDocument !== null} isDocumentEnabled={contextMenuDocument?.enabled ?? true} - selectedCount={selectedDocuments.size} + selectedCount={selectedDocumentCount} enabledCount={enabledCount} disabledCount={disabledCount} onOpenInNewTab={ - contextMenuDocument && selectedDocuments.size === 1 + contextMenuDocument && selectedDocumentCount === 1 ? () => { const urlParams = new URLSearchParams({ kbName: knowledgeBaseName, @@ -1519,14 +1520,14 @@ export function KnowledgeBase({ : undefined } onOpenSource={ - contextMenuDocument?.sourceUrl && selectedDocuments.size === 1 + contextMenuDocument?.sourceUrl && selectedDocumentCount === 1 ? () => window.open(contextMenuDocument.sourceUrl!, '_blank', 'noopener,noreferrer') : undefined } onRename={contextMenuDocument ? () => handleRenameDocument(contextMenuDocument) : undefined} onToggleEnabled={ contextMenuDocument - ? selectedDocuments.size > 1 + ? selectedDocumentCount > 1 ? () => { if (disabledCount > 0) { handleBulkEnable() @@ -1538,13 +1539,13 @@ export function KnowledgeBase({ : undefined } onViewTags={ - contextMenuDocument && selectedDocuments.size === 1 && userPermissions.canEdit + contextMenuDocument && selectedDocumentCount === 1 && userPermissions.canEdit ? () => handleViewDocumentTags(contextMenuDocument) : undefined } onDelete={ contextMenuDocument - ? selectedDocuments.size > 1 + ? selectedDocumentCount > 1 ? handleBulkDelete : () => handleDeleteDocument(contextMenuDocument.id) : undefined diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index e3916e24179..8572b5b24f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -8,7 +8,10 @@ import { DropdownMenuTrigger, } from '@sim/emcn' import { Eye, Pencil, Plus, SquareArrowUpRight, TagIcon, Trash } from '@sim/emcn/icons' -import { selectionActionLabel } from '@/app/workspace/[workspaceId]/components/resource/selection-label' +import { + selectionActionLabel, + selectionToggleActionLabel, +} from '@/app/workspace/[workspaceId]/components/resource/selection-label' interface DocumentContextMenuProps { isOpen: boolean @@ -59,14 +62,12 @@ export function DocumentContextMenu({ disabledCount = 0, }: DocumentContextMenuProps) { const isMultiSelect = selectedCount > 1 - - const getToggleLabel = () => { - if (isMultiSelect) { - if (disabledCount > 0) return 'Enable' - return 'Disable' - } - return isDocumentEnabled ? 'Disable' : 'Enable' - } + const toggleLabel = selectionToggleActionLabel({ + selectedCount, + enabledCount, + disabledCount, + isSelectedItemEnabled: isDocumentEnabled, + }) const hasNavigationSection = !isMultiSelect && (!!onOpenInNewTab || !!onOpenSource) const hasEditSection = !isMultiSelect && (!!onRename || !!onViewTags) @@ -125,7 +126,7 @@ export function DocumentContextMenu({ {onToggleEnabled && ( - {selectionActionLabel(getToggleLabel(), selectedCount)} + {toggleLabel} )} From 969950530d7b31d1ac43b12e585843fd9f462d4f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 22 Aug 2026 15:35:51 -0700 Subject: [PATCH 3/4] fix(knowledge): avoid overstating select-all scope --- .../selection-aware-context-menus.test.tsx | 19 +++++++++++++++++++ .../resource/selection-label.test.ts | 12 ++++++++++++ .../components/resource/selection-label.ts | 7 +++++-- .../[workspaceId]/knowledge/[id]/base.tsx | 1 + .../document-context-menu.tsx | 3 +++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx index 23b6f46b707..5a792131fd5 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-aware-context-menus.test.tsx @@ -168,6 +168,25 @@ describe('selection-aware resource context menus', () => { expect(menu).toContain('Delete 25 items') }) + it('does not overstate an unknown select-all toggle count', () => { + const menu = renderToStaticMarkup( + {}} + hasDocument + selectedCount={25} + enabledCount={25} + disabledCount={25} + hasExactToggleCount={false} + onToggleEnabled={() => {}} + /> + ) + + expect(menu).toContain('Enable selected items') + expect(menu).not.toContain('Enable 25 items') + }) + it('counts only the chunks affected by a multi-selection toggle', () => { const menu = renderToStaticMarkup( { }) ).toBe('Disable 4 items') }) + + it('keeps the action selection-aware when the affected subset count is unknown', () => { + expect( + selectionToggleActionLabel({ + selectedCount: 10, + enabledCount: 10, + disabledCount: 10, + isSelectedItemEnabled: true, + hasExactAffectedCount: false, + }) + ).toBe('Enable selected items') + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts index 334f83618d2..f2ef16c6c10 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts @@ -22,6 +22,7 @@ interface SelectionToggleActionLabelOptions { enabledCount: number disabledCount: number isSelectedItemEnabled: boolean + hasExactAffectedCount?: boolean } export function selectionToggleActionLabel({ @@ -29,8 +30,10 @@ export function selectionToggleActionLabel({ enabledCount, disabledCount, isSelectedItemEnabled, + hasExactAffectedCount = true, }: SelectionToggleActionLabelOptions): string { if (selectedCount <= 1) return isSelectedItemEnabled ? 'Disable' : 'Enable' - if (disabledCount > 0) return selectionActionLabel('Enable', disabledCount) - return selectionActionLabel('Disable', enabledCount) + const action = disabledCount > 0 ? 'Enable' : 'Disable' + if (!hasExactAffectedCount) return `${action} selected items` + return selectionActionLabel(action, disabledCount > 0 ? disabledCount : enabledCount) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 0450cba4573..bfd9d3bc424 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -1505,6 +1505,7 @@ export function KnowledgeBase({ selectedCount={selectedDocumentCount} enabledCount={enabledCount} disabledCount={disabledCount} + hasExactToggleCount={!isSelectAllMode || enabledFilter !== 'all'} onOpenInNewTab={ contextMenuDocument && selectedDocumentCount === 1 ? () => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index 8572b5b24f9..c6d9075d0b1 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -33,6 +33,7 @@ interface DocumentContextMenuProps { selectedCount: number enabledCount?: number disabledCount?: number + hasExactToggleCount?: boolean } /** @@ -60,6 +61,7 @@ export function DocumentContextMenu({ selectedCount, enabledCount = 0, disabledCount = 0, + hasExactToggleCount = true, }: DocumentContextMenuProps) { const isMultiSelect = selectedCount > 1 const toggleLabel = selectionToggleActionLabel({ @@ -67,6 +69,7 @@ export function DocumentContextMenu({ enabledCount, disabledCount, isSelectedItemEnabled: isDocumentEnabled, + hasExactAffectedCount: hasExactToggleCount, }) const hasNavigationSection = !isMultiSelect && (!!onOpenInNewTab || !!onOpenSource) From 3d5fb83a57f9fa789aff106fc67b91a97b3118bf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 22 Aug 2026 15:56:00 -0700 Subject: [PATCH 4/4] fix(knowledge): keep select-all actions consistent --- .../components/resource/selection-label.test.ts | 11 +++++++++++ .../components/resource/selection-label.ts | 8 ++++++-- .../workspace/[workspaceId]/knowledge/[id]/base.tsx | 8 +++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts index fa46c2da712..55ca76109ba 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.test.ts @@ -33,6 +33,17 @@ describe('selection labels', () => { ).toBe('Enable 3 items') }) + it('keeps a singular affected count visible within a larger selection', () => { + expect( + selectionToggleActionLabel({ + selectedCount: 5, + enabledCount: 4, + disabledCount: 1, + isSelectedItemEnabled: true, + }) + ).toBe('Enable 1 item') + }) + it('counts enabled items when a selection can only be disabled', () => { expect( selectionToggleActionLabel({ diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts index f2ef16c6c10..b0f88be128b 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/selection-label.ts @@ -14,7 +14,11 @@ export function selectionActionLabel( singleItemLabel = action ): string { if (selectedCount <= 1) return singleItemLabel - return `${action} ${selectedCount} items` + return countedSelectionActionLabel(action, selectedCount) +} + +function countedSelectionActionLabel(action: string, count: number): string { + return `${action} ${count} ${count === 1 ? 'item' : 'items'}` } interface SelectionToggleActionLabelOptions { @@ -35,5 +39,5 @@ export function selectionToggleActionLabel({ if (selectedCount <= 1) return isSelectedItemEnabled ? 'Disable' : 'Enable' const action = disabledCount > 0 ? 'Enable' : 'Disable' if (!hasExactAffectedCount) return `${action} selected items` - return selectionActionLabel(action, disabledCount > 0 ? disabledCount : enabledCount) + return countedSelectionActionLabel(action, disabledCount > 0 ? disabledCount : enabledCount) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index bfd9d3bc424..32a08870988 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -687,6 +687,7 @@ export function KnowledgeBase({ * Handles selecting/deselecting a document */ const handleSelectDocument = (docId: string, checked: boolean) => { + setIsSelectAllMode(false) setSelectedDocuments((prev) => { const newSet = new Set(prev) if (checked) { @@ -918,6 +919,7 @@ export function KnowledgeBase({ const isCurrentlySelected = selectedDocuments.has(doc.id) if (!isCurrentlySelected) { + setIsSelectAllMode(false) setSelectedDocuments(new Set([doc.id])) } @@ -1423,15 +1425,15 @@ export function KnowledgeBase({ srTitle='Delete Documents' title='Delete Documents' text={[ - `Are you sure you want to delete ${selectedDocuments.size} document${selectedDocuments.size === 1 ? '' : 's'}? `, + `Are you sure you want to delete ${selectedDocumentCount} document${selectedDocumentCount === 1 ? '' : 's'}? `, { - text: `This will permanently delete the selected document${selectedDocuments.size === 1 ? '' : 's'}.`, + text: `This will permanently delete the selected document${selectedDocumentCount === 1 ? '' : 's'}.`, error: true, }, ' This action cannot be undone.', ]} confirm={{ - label: `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`, + label: `Delete ${selectedDocumentCount} Document${selectedDocumentCount === 1 ? '' : 's'}`, onClick: confirmBulkDelete, pending: isBulkOperating, pendingLabel: 'Deleting...',