Skip to content

Commit 4cfec92

Browse files
add select and select all options in context menu
1 parent e95061e commit 4cfec92

5 files changed

Lines changed: 171 additions & 43 deletions

File tree

packages/bruno-app/src/components/Dropdown/StyledWrapper.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ const Wrapper = styled.div`
9393
justify-content: center;
9494
}
9595
96+
.shortcut {
97+
font-size: 11px;
98+
color: ${(props) => props.theme.dropdown.mutedText};
99+
}
100+
96101
.dropdown-tab-count {
97102
margin-left: auto;
98103
font-size: 11px;

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentSelection/RowActionsMenu.js

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,67 @@
11
import React, { useMemo } from 'react';
22
import MenuDropdown from 'ui/MenuDropdown';
3-
import { IconDots, IconUpload, IconEdit, IconCopy, IconTrash } from '@tabler/icons';
3+
import { IconDots, IconUpload, IconEdit, IconCopy, IconTrash, IconCheck, IconChecks } from '@tabler/icons';
4+
import { getPlatformModifierKey, isMacOS } from 'utils/common/platform';
45

5-
const RowActionsMenu = ({ onExport, onRename, onDuplicate, onDelete }) => {
6-
const menuItems = useMemo(() => ([
7-
{
8-
id: 'export',
9-
label: 'Export',
10-
leftSection: IconUpload,
11-
onClick: onExport
12-
},
13-
{ id: 'divider-1', type: 'divider' },
14-
{
15-
id: 'rename',
16-
label: 'Rename',
17-
leftSection: IconEdit,
18-
onClick: onRename
19-
},
20-
{
21-
id: 'duplicate',
22-
label: 'Duplicate',
23-
leftSection: IconCopy,
24-
onClick: onDuplicate
25-
},
26-
{ id: 'divider-2', type: 'divider' },
27-
{
28-
id: 'delete',
29-
label: 'Delete',
30-
leftSection: IconTrash,
31-
className: 'delete-item',
32-
onClick: onDelete
6+
const modKey = getPlatformModifierKey();
7+
const selectShortcut = `${modKey}+Click`;
8+
const selectAllShortcut = isMacOS() ? `${modKey}A` : `${modKey}+A`;
9+
10+
const RowActionsMenu = ({ onExport, onRename, onDuplicate, onDelete, onSelect, onSelectAll, hasSelection, isAllSelected }) => {
11+
const menuItems = useMemo(() => {
12+
const items = [
13+
{
14+
id: 'export',
15+
label: 'Export',
16+
leftSection: IconUpload,
17+
onClick: onExport
18+
},
19+
{ id: 'divider-1', type: 'divider' },
20+
{
21+
id: 'rename',
22+
label: 'Rename',
23+
leftSection: IconEdit,
24+
onClick: onRename
25+
},
26+
{
27+
id: 'duplicate',
28+
label: 'Duplicate',
29+
leftSection: IconCopy,
30+
onClick: onDuplicate
31+
},
32+
{ id: 'divider-2', type: 'divider' }
33+
];
34+
35+
if (!hasSelection) {
36+
items.push({
37+
id: 'select',
38+
label: 'Select',
39+
leftSection: IconCheck,
40+
rightSection: <span className="shortcut">{selectShortcut}</span>,
41+
onClick: onSelect
42+
});
3343
}
34-
]), [onExport, onRename, onDuplicate, onDelete]);
44+
45+
items.push(
46+
{
47+
id: 'select-all',
48+
label: isAllSelected ? 'Unselect all' : 'Select all',
49+
leftSection: IconChecks,
50+
rightSection: <span className="shortcut">{selectAllShortcut}</span>,
51+
onClick: onSelectAll
52+
},
53+
{ id: 'divider-3', type: 'divider' },
54+
{
55+
id: 'delete',
56+
label: 'Delete',
57+
leftSection: IconTrash,
58+
className: 'delete-item',
59+
onClick: onDelete
60+
}
61+
);
62+
63+
return items;
64+
}, [onExport, onRename, onDuplicate, onDelete, onSelect, onSelectAll, hasSelection, isAllSelected]);
3565

3666
return (
3767
<div

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentSelection/SelectionContextMenu.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
import React, { useMemo } from 'react';
22
import MenuDropdown from 'ui/MenuDropdown';
3-
import { IconUpload, IconEdit, IconCopy, IconTrash } from '@tabler/icons';
3+
import { IconUpload, IconEdit, IconCopy, IconTrash, IconCheck, IconChecks } from '@tabler/icons';
4+
import { getPlatformModifierKey, isMacOS } from 'utils/common/platform';
45

5-
const SelectionContextMenu = ({ visible, position, selectedCount, onExport, onRename, onDuplicate, onDelete, onClose }) => {
6+
const modKey = getPlatformModifierKey();
7+
const selectShortcut = `${modKey}+Click`;
8+
const selectAllShortcut = isMacOS() ? `${modKey}A` : `${modKey}+A`;
9+
10+
const SelectionContextMenu = ({
11+
visible,
12+
position,
13+
selectedCount,
14+
onExport,
15+
onRename,
16+
onDuplicate,
17+
onDelete,
18+
onSelect,
19+
onSelectAll,
20+
hasSelection,
21+
isAllSelected,
22+
onClose
23+
}) => {
624
const anchorStyle = {
725
position: 'fixed',
826
left: `${position?.x || 0}px`,
@@ -43,16 +61,36 @@ const SelectionContextMenu = ({ visible, position, selectedCount, onExport, onRe
4361
);
4462
}
4563

46-
items.push({
47-
id: 'delete',
48-
label: selectedCount > 1 ? `Delete (${selectedCount})` : 'Delete',
49-
leftSection: IconTrash,
50-
className: 'delete-item',
51-
onClick: onDelete
52-
});
64+
if (!hasSelection) {
65+
items.push({
66+
id: 'select',
67+
label: 'Select',
68+
leftSection: IconCheck,
69+
rightSection: <span className="shortcut">{selectShortcut}</span>,
70+
onClick: onSelect
71+
});
72+
}
73+
74+
items.push(
75+
{
76+
id: 'select-all',
77+
label: isAllSelected ? 'Unselect all' : 'Select all',
78+
leftSection: IconChecks,
79+
rightSection: <span className="shortcut">{selectAllShortcut}</span>,
80+
onClick: onSelectAll
81+
},
82+
{ id: 'divider-3', type: 'divider' },
83+
{
84+
id: 'delete',
85+
label: selectedCount > 1 ? `Delete (${selectedCount})` : 'Delete',
86+
leftSection: IconTrash,
87+
className: 'delete-item',
88+
onClick: onDelete
89+
}
90+
);
5391

5492
return items;
55-
}, [isSingleSelection, selectedCount, onExport, onRename, onDuplicate, onDelete]);
93+
}, [isSingleSelection, selectedCount, onExport, onRename, onDuplicate, onDelete, onSelect, onSelectAll, hasSelection, isAllSelected]);
5694

5795
return (
5896
<MenuDropdown

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentSelection/useEnvironmentBulkSelection.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ const useEnvironmentBulkSelection = ({
4646
[environments, actionTargetUids]
4747
);
4848

49+
const isAllSelected = useMemo(
50+
() => filteredEnvUids.length > 0 && filteredEnvUids.every((uid) => selectedEnvUids.includes(uid)),
51+
[filteredEnvUids, selectedEnvUids]
52+
);
53+
4954
const openMenuAt = useCallback((e) => {
5055
setMenuPosition({ x: e.clientX, y: e.clientY });
5156
setMenuVisible(true);
@@ -70,6 +75,29 @@ const useEnvironmentBulkSelection = ({
7075
setLastClickedEnvUid(uid);
7176
}, []);
7277

78+
const selectEnvs = useCallback((uids) => {
79+
if (!uids || !uids.length) return;
80+
setSelectedEnvUids((prev) => {
81+
const merged = new Set(prev);
82+
uids.forEach((uid) => merged.add(uid));
83+
return Array.from(merged);
84+
});
85+
setLastClickedEnvUid(uids[uids.length - 1]);
86+
}, []);
87+
88+
// Toggles: selects every filtered environment, or — if they're all
89+
// already selected — clears the selection instead. Shared by the
90+
// "Select all"/"Unselect all" menu item and the Cmd/Ctrl+A shortcut, so
91+
// both always agree on what happens next.
92+
const selectAllEnvs = useCallback(() => {
93+
if (isAllSelected) {
94+
clearSelection();
95+
return;
96+
}
97+
setSelectedEnvUids(filteredEnvUids);
98+
setLastClickedEnvUid(filteredEnvUids.length ? filteredEnvUids[filteredEnvUids.length - 1] : null);
99+
}, [isAllSelected, filteredEnvUids, clearSelection]);
100+
73101
const selectEnvRange = useCallback((toUid) => {
74102
setSelectedEnvUids((prev) => {
75103
const fromIndex = lastClickedEnvUid ? filteredEnvUids.indexOf(lastClickedEnvUid) : -1;
@@ -84,9 +112,6 @@ const useEnvironmentBulkSelection = ({
84112
});
85113
}, [lastClickedEnvUid, filteredEnvUids]);
86114

87-
// Matches the Sidebar's own convention (see useSidebarSelectionClick):
88-
// the multi-select toggle modifier is OS-appropriate — Cmd on macOS,
89-
// Ctrl on Windows/Linux — and it only builds the selection, quietly.
90115
const handleRowInteraction = useCallback((e, env) => {
91116
const isSelectionModifierPressed = isMacOS() ? e.metaKey : e.ctrlKey;
92117

@@ -189,6 +214,25 @@ const useEnvironmentBulkSelection = ({
189214
return () => document.removeEventListener('keydown', handleKeyDown);
190215
}, [hasSelection, clearSelection]);
191216

217+
useEffect(() => {
218+
const handleKeyDown = (e) => {
219+
const isSelectAllShortcut = (isMacOS() ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === 'a';
220+
if (!isSelectAllShortcut) return;
221+
222+
const activeTag = document.activeElement?.tagName;
223+
const isTextFieldFocused = activeTag === 'INPUT' || activeTag === 'TEXTAREA' || document.activeElement?.isContentEditable;
224+
if (isTextFieldFocused) return;
225+
226+
if (!scopeRef.current?.matches(':hover')) return;
227+
228+
e.preventDefault();
229+
selectAllEnvs();
230+
};
231+
232+
document.addEventListener('keydown', handleKeyDown);
233+
return () => document.removeEventListener('keydown', handleKeyDown);
234+
}, [selectAllEnvs]);
235+
192236
return {
193237
scopeRef,
194238
hasSelection,
@@ -197,6 +241,9 @@ const useEnvironmentBulkSelection = ({
197241
actionTargetUids,
198242
actionTargetEnvironmentsList,
199243
selectOnlyEnv,
244+
selectEnvs,
245+
selectAllEnvs,
246+
isAllSelected,
200247
showDeleteModal,
201248
openDeleteModal: () => {
202249
setShowDeleteModal(true);

packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@ const EnvironmentList = ({
589589
visible={bulkSelection.menuVisible}
590590
position={bulkSelection.menuPosition}
591591
selectedCount={bulkSelection.actionTargetUids.length}
592+
onSelect={() => bulkSelection.selectEnvs(bulkSelection.actionTargetUids)}
593+
onSelectAll={bulkSelection.selectAllEnvs}
594+
hasSelection={bulkSelection.hasSelection}
595+
isAllSelected={bulkSelection.isAllSelected}
592596
onExport={bulkSelection.openExportModal}
593597
onRename={bulkSelection.handleRenameSelected}
594598
onDuplicate={bulkSelection.openCopyModal}
@@ -737,6 +741,10 @@ const EnvironmentList = ({
737741
<div className="environment-actions">
738742
{!isEnvMultiSelected && (
739743
<RowActionsMenu
744+
onSelect={() => bulkSelection.selectEnvs([env.uid])}
745+
onSelectAll={bulkSelection.selectAllEnvs}
746+
hasSelection={bulkSelection.hasSelection}
747+
isAllSelected={bulkSelection.isAllSelected}
740748
onExport={() => bulkSelection.startExportForEnv(env)}
741749
onRename={() => bulkSelection.startRenameForEnv(env)}
742750
onDuplicate={() => bulkSelection.startCopyForEnv(env)}

0 commit comments

Comments
 (0)