mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-26 22:25:40 +00:00
fix: qol fixes for keybindings (#7709)
* fix: keybindings issues * chore: let SingleLineEditor handle it's own handleSubmit * fix: resolve issues * fix: disable reset default if none are changed * fix: exlude transient request from reopen last closed tabs * fix: updated all hardcoded colors to respective theme colors * chore: pick color from theme --------- Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local> Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
@@ -84,8 +84,8 @@ export default class CodeEditor extends React.Component {
|
||||
this.searchBarRef.current?.focus();
|
||||
});
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
'Cmd-H': this.props.readOnly ? false : 'replace',
|
||||
'Ctrl-H': this.props.readOnly ? false : 'replace',
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
|
||||
@@ -39,7 +39,6 @@ const StyledWrapper = styled.div`
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.input.border};
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tables-container {
|
||||
@@ -75,7 +74,7 @@ const StyledWrapper = styled.div`
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
background: ${(props) => props.theme.table.striped};
|
||||
user-select: none;
|
||||
|
||||
td {
|
||||
@@ -100,9 +99,8 @@ const StyledWrapper = styled.div`
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
height: 30px;
|
||||
|
||||
td {
|
||||
padding: 0 10px !important;
|
||||
padding: 0px 10px !important;
|
||||
border: none !important;
|
||||
vertical-align: middle;
|
||||
background: transparent;
|
||||
@@ -111,7 +109,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
tr:hover:not(.row-editing) td {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
tr.section-heading-row td {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
padding: 6px 10px !important;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -131,8 +129,28 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
tr.section-last-row td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
tr.section-spacer-row {
|
||||
height: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
tr.section-spacer-row td {
|
||||
padding: 0 !important;
|
||||
height: 8px;
|
||||
line-height: 8px;
|
||||
font-size: 0;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0} !important;
|
||||
}
|
||||
|
||||
tr.section-spacer-row:hover td {
|
||||
background: transparent !important;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.keybinding-row {
|
||||
@@ -180,7 +198,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.shortcut-input--editing {
|
||||
outline: 1px solid #E4AE49;
|
||||
outline: 1px solid ${(props) => props.theme.status.warning.border};
|
||||
border-radius: 4px;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
@@ -189,7 +207,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.shortcut-input--error.shortcut-input--editing {
|
||||
outline: 1px solid #CE4F3B;
|
||||
outline: 1px solid ${(props) => props.theme.status.danger.border};
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -220,39 +238,41 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 22px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background: ${(props) => props.theme.background.base};
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
tbody tr.row-success td {
|
||||
background: #2E8A540F;
|
||||
tbody tr.row-success td,
|
||||
tbody tr.row-success:hover td {
|
||||
background: ${(props) => props.theme.status.success.background} !important;
|
||||
}
|
||||
|
||||
tbody tr.row-error td {
|
||||
background: #D32F2F0F;
|
||||
tbody tr.row-error td,
|
||||
tbody tr.row-error:hover td {
|
||||
background: ${(props) => props.theme.status.danger.background} !important;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
color: #2E8A54;
|
||||
color: ${(props) => props.theme.status.success.text};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #CE4F3B;
|
||||
color: ${(props) => props.theme.status.danger.text};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-error-icon {
|
||||
color: #CE4F3B;
|
||||
color: ${(props) => props.theme.status.danger.text};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
@@ -294,6 +314,11 @@ const StyledWrapper = styled.div`
|
||||
border-radius: 6px;
|
||||
padding: 0px 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useRef, useState, useEffect, Fragment } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { KEY_BINDING_SECTIONS } from 'providers/Hotkeys/keyMappings.js';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import ToggleSwitch from 'components/ToggleSwitch/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const SEP = '+bind+';
|
||||
const getOS = () => (isMacOS() ? 'mac' : 'windows');
|
||||
@@ -82,10 +83,10 @@ const renderDisplayValue = (displayValue, os) => {
|
||||
return (
|
||||
<span className="shortcut-pills">
|
||||
{parsed.map((keysArr, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Fragment key={index}>
|
||||
{index > 0 && <span className="shortcut-separator"> - </span>}
|
||||
{renderKeycaps(keysArr, os)}
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
@@ -218,23 +219,21 @@ const RESERVED_BY_OS = {
|
||||
comboSignature(['f12']) // Dashboard (older macOS)
|
||||
]),
|
||||
windows: new Set([
|
||||
// System-level shortcuts (intercepted by Windows before reaching the app)
|
||||
comboSignature(['alt', 'tab']),
|
||||
comboSignature(['alt', 'shift', 'tab']),
|
||||
comboSignature(['alt', 'f4']),
|
||||
comboSignature(['f1']), // Windows Help
|
||||
comboSignature(['alt', 'esc']),
|
||||
comboSignature(['alt', 'space']),
|
||||
comboSignature(['ctrl', 'alt', 'delete']),
|
||||
comboSignature(['command', 'l']),
|
||||
comboSignature(['command', 'd']),
|
||||
comboSignature(['command', 'e']),
|
||||
comboSignature(['command', 'r']),
|
||||
comboSignature(['command', 'i']),
|
||||
comboSignature(['command', 's']),
|
||||
comboSignature(['command', 'a']),
|
||||
comboSignature(['command', 'x']),
|
||||
comboSignature(['command', 'm']),
|
||||
comboSignature(['command', 'tab']),
|
||||
comboSignature(['ctrl', 'shift', 'esc']),
|
||||
// Function keys
|
||||
comboSignature(['f1']), // Windows Help
|
||||
comboSignature(['f11']), // Fullscreen toggle
|
||||
comboSignature(['f12']), // DevTools
|
||||
// Undo/Redo - standard text editing shortcuts that browsers handle natively
|
||||
comboSignature(['ctrl', 'z']),
|
||||
comboSignature(['ctrl', 'y']),
|
||||
comboSignature(['ctrl', 'shift', 'z']),
|
||||
// Toggle Developer Tools
|
||||
comboSignature(['ctrl', 'shift', 'i'])
|
||||
@@ -493,7 +492,7 @@ const Keybindings = () => {
|
||||
if (buildUsedSignatures(action).has(sig)) {
|
||||
return {
|
||||
code: ERROR.DUPLICATE,
|
||||
message: 'That shortcut is already in use.'
|
||||
message: 'This shortcut is already in use.'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -562,9 +561,24 @@ const Keybindings = () => {
|
||||
return next;
|
||||
});
|
||||
|
||||
persistToPreferences(action, def);
|
||||
// Remove the entry from user preferences entirely so falls back to default.
|
||||
// This also keeps `hasCustomizedKeybindings` accurate.
|
||||
const nextKeyBindings = { ...(preferences?.keyBindings || {}) };
|
||||
delete nextKeyBindings[action];
|
||||
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
keyBindings: nextKeyBindings
|
||||
};
|
||||
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
const hasCustomizedKeybindings = useMemo(() => {
|
||||
const userKeyBindings = preferences?.keyBindings || {};
|
||||
return Object.keys(userKeyBindings).length > 0;
|
||||
}, [preferences?.keyBindings]);
|
||||
|
||||
const resetAllKeybindings = () => {
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
@@ -572,6 +586,7 @@ const Keybindings = () => {
|
||||
};
|
||||
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
toast.success('All shortcuts have been reset to default');
|
||||
};
|
||||
|
||||
const startEditing = (action) => {
|
||||
@@ -799,6 +814,7 @@ const Keybindings = () => {
|
||||
onClick={resetAllKeybindings}
|
||||
className="reset-btn"
|
||||
data-testid="reset-all-keybindings-btn"
|
||||
disabled={!hasCustomizedKeybindings}
|
||||
>
|
||||
Reset Default
|
||||
</button>
|
||||
@@ -817,7 +833,7 @@ const Keybindings = () => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedKeyMappings.map((section, sectionIndex) => (
|
||||
<React.Fragment key={section.heading}>
|
||||
<Fragment key={section.heading}>
|
||||
<tr className="section-heading-row">
|
||||
<td colSpan={2}>{section.heading}</td>
|
||||
</tr>
|
||||
@@ -946,7 +962,12 @@ const Keybindings = () => {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
{sectionIndex < groupedKeyMappings.length - 1 && (
|
||||
<tr className="section-spacer-row" aria-hidden="true">
|
||||
<td colSpan={2}> </td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -137,6 +137,12 @@ export default class QueryEditor extends React.Component {
|
||||
this.addOverlay();
|
||||
|
||||
setupLinkAware(editor);
|
||||
|
||||
// Add mousetrap class so Mousetrap captures shortcuts even when CodeMirror is focused
|
||||
const cmInput = editor.getInputField();
|
||||
if (cmInput) {
|
||||
cmInput.classList.add('mousetrap');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
||||
@@ -225,7 +225,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
if (tab.type === 'environment-settings') {
|
||||
if (collection?.environmentsDraft) {
|
||||
const { environmentUid, variables } = collection.environmentsDraft;
|
||||
dispatch(saveEnvironment(variables, environmentUid, collection.uid));
|
||||
if (environmentUid?.startsWith('dotenv:')) {
|
||||
window.dispatchEvent(new Event('dotenv-save'));
|
||||
} else {
|
||||
dispatch(saveEnvironment(variables, environmentUid, collection.uid));
|
||||
}
|
||||
}
|
||||
} else if (tab.type === 'global-environment-settings') {
|
||||
if (globalEnvironmentDraft) {
|
||||
|
||||
@@ -316,18 +316,6 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.defaultPrevented) {
|
||||
const isTextInput
|
||||
= ['input', 'textarea'].includes(e.target.tagName.toLowerCase())
|
||||
|| e.target.isContentEditable;
|
||||
|
||||
if (!isTextInput) {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-medium">
|
||||
@@ -523,6 +511,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
className="flex px-2 items-center flex-grow input-container h-full min-w-0"
|
||||
>
|
||||
<SingleLineEditor
|
||||
onRun={() => formik.handleSubmit()}
|
||||
onPaste={handlePaste}
|
||||
placeholder="Request URL"
|
||||
value={formik.values.requestUrl || ''}
|
||||
|
||||
@@ -35,7 +35,8 @@ taskMiddleware.startListening({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item),
|
||||
preview: task?.preview ?? true
|
||||
preview: task?.preview ?? true,
|
||||
...(item.isTransient ? { isTransient: true } : {})
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const tabsSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
addTab: (state, action) => {
|
||||
const { uid, collectionUid, type, requestPaneTab, preview, exampleUid, itemUid } = action.payload;
|
||||
const { uid, collectionUid, type, requestPaneTab, preview, exampleUid, itemUid, isTransient } = action.payload;
|
||||
|
||||
const nonReplaceableTabTypes = [
|
||||
'variables',
|
||||
@@ -75,7 +75,8 @@ export const tabsSlice = createSlice({
|
||||
: !nonReplaceableTabTypes.includes(type),
|
||||
...(uid ? { folderUid: uid } : {}),
|
||||
...(exampleUid ? { exampleUid } : {}),
|
||||
...(itemUid ? { itemUid } : {})
|
||||
...(itemUid ? { itemUid } : {}),
|
||||
...(isTransient ? { isTransient: true } : {})
|
||||
};
|
||||
|
||||
state.activeTabUid = uid;
|
||||
@@ -103,7 +104,8 @@ export const tabsSlice = createSlice({
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type),
|
||||
...(exampleUid ? { exampleUid } : {}),
|
||||
...(itemUid ? { itemUid } : {})
|
||||
...(itemUid ? { itemUid } : {}),
|
||||
...(isTransient ? { isTransient: true } : {})
|
||||
});
|
||||
state.activeTabUid = uid;
|
||||
},
|
||||
@@ -270,8 +272,9 @@ export const tabsSlice = createSlice({
|
||||
const nonClosableTypes = ['workspaceOverview', 'workspaceEnvironments'];
|
||||
|
||||
// Push closed tabs onto the recently closed stack (LIFO)
|
||||
// Exclude transient requests — they have no persisted file and can't be reopened
|
||||
const closedTabs = state.tabs.filter((t) =>
|
||||
tabUids.includes(t.uid) && !nonClosableTypes.includes(t.type)
|
||||
tabUids.includes(t.uid) && !nonClosableTypes.includes(t.type) && !t.isTransient
|
||||
);
|
||||
if (closedTabs.length > 0) {
|
||||
state.recentlyClosedTabs.push(...closedTabs);
|
||||
|
||||
Reference in New Issue
Block a user