-
Notifications
You must be signed in to change notification settings - Fork 3.3k
feat: BROS-114: Global Custom Hotkeys #7784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ation Signed-off-by: Michael Malyuk <[email protected]>
Signed-off-by: Michael Malyuk <[email protected]>
Signed-off-by: Michael Malyuk <[email protected]>
Signed-off-by: Michael Malyuk <[email protected]>
Signed-off-by: Michael Malyuk <[email protected]>
✅ Deploy Preview for heartex-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for label-studio-docs-new-theme ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for label-studio-playground ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for label-studio-storybook ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
web/libs/app-common/src/pages/AccountSettings/sections/Hotkeys.jsx
Outdated
Show resolved
Hide resolved
| const api = useAPI(); | ||
|
|
||
| // Function to identify which hotkeys were modified from defaults | ||
| const getModifiedHotkeys = (currentHotkeys) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const getModifiedHotkeys = useMemo(() => {
...
}, [hotkeys]);
| @@ -0,0 +1,599 @@ | |||
| import { useEffect, useState } from "react"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import { useEffect, useState } from "react"; | |
| import { useEffect, useState, useMemo, useReact } from "react"; |
this file misses memoization pattern, which results in the degraded performance and uneccessary rerenders. I'd suggest wrapping places with useMemo and useCallback where it is necessary for:
- prevent recomputations of potentially expensive operations (e.g.
getModifiedHotkeys) - prevent components rerenders due to non-memoized handlers (e.g. handlers in
<HotkeySection>)
web/libs/app-common/src/pages/AccountSettings/sections/Hotkeys.jsx
Outdated
Show resolved
Hide resolved
web/libs/app-common/src/pages/AccountSettings/sections/Hotkeys.jsx
Outdated
Show resolved
Hide resolved
|
|
||
| <script id="app-settings" nonce="{{request.csp_nonce}}"> | ||
|
|
||
| var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|safe }}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not creating mergedEditorKeymap in Django code instead of custom inline js ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Django doesn't know much about the defaults for the keys, and we don't want to have two sets of defaults, one on the backend, and another on the frontend, all of the definitions and processing is done at the frontend level, this follows DRY principle.
| @@ -0,0 +1,88 @@ | |||
|
|
|||
| import json | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test suite should use pytest and follow the principles described in https://github.com/HumanSignal/label-studio-enterprise/blob/develop/.cursor/rules/backend-unit-tests.mdc
Example:
import json
import pytest
from users.serializers import HotkeysSerializer # Import from your actual app
class TestHotkeysSerializer:
"""Tests for the HotkeysSerializer following pytest guidelines"""
@pytest.fixture
def valid_hotkeys(self):
"""Fixture providing valid hotkeys data for testing"""
return {
"editor:save": {"key": "ctrl+s", "active": True},
"editor:open": {"key": "ctrl+o", "active": False},
"editor:cut": {"key": "ctrl+x"},
"navigation:home": {"key": "alt+h", "active": True}
}
@pytest.fixture
def valid_serializer_data(self, valid_hotkeys):
"""Fixture providing valid serializer data"""
return {"custom_hotkeys": valid_hotkeys}
def test_valid_data(self, valid_serializer_data):
"""Test serializer validation with valid hotkeys data.
This test validates:
- Serializer accepts properly formatted hotkeys dictionary
- All required fields are present and valid
- Both active and inactive hotkeys are handled correctly
- Optional 'active' field defaults appropriately
Critical validation: Ensures well-formed hotkey configurations
are accepted without errors for user customization features.
"""
serializer = HotkeysSerializer(data=valid_serializer_data)
assert serializer.is_valid()
def test_invalid_format_not_dict(self):
"""Test serializer rejects non-dictionary custom_hotkeys.
This test validates:
- Serializer properly validates data type of custom_hotkeys field
- Non-dictionary values (lists, strings, etc.) are rejected
- Appropriate validation errors are returned
Critical validation: Prevents malformed data from corrupting
user hotkey preferences and ensures data integrity.
"""
data = {"custom_hotkeys": ["not a dictionary"]}
serializer = HotkeysSerializer(data=data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errors
def test_invalid_action_key_format(self):
"""Test serializer rejects action keys without proper format.
This test validates:
- Action keys must follow 'category:action' format with colon separator
- Malformed action keys (missing colon) are rejected
- Validation error is properly raised and contains expected field
Critical validation: Ensures hotkey action identifiers follow
expected naming convention for proper system integration.
"""
# Missing colon
invalid_data = {
"custom_hotkeys": {
"editorsave": {"key": "ctrl+s"}
}
}
serializer = HotkeysSerializer(data=invalid_data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errors
def test_empty_action_key(self):
"""Test serializer rejects empty action keys.
This test validates:
- Empty string action keys are not allowed
- Validation properly catches and rejects empty identifiers
- Error response includes the problematic field
Critical validation: Prevents creation of hotkeys with
undefined action identifiers that would break functionality.
"""
invalid_data = {
"custom_hotkeys": {
"": {"key": "ctrl+s"}
}
}
serializer = HotkeysSerializer(data=invalid_data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errors
def test_missing_key_in_hotkey_data(self):
"""Test serializer rejects hotkey data without required 'key' field.
This test validates:
- The 'key' field is mandatory for each hotkey configuration
- Hotkey objects missing the 'key' field are rejected
- Validation error properly identifies the missing required field
Critical validation: Ensures all hotkeys have keyboard combinations
defined, preventing non-functional hotkey configurations.
"""
invalid_data = {
"custom_hotkeys": {
"editor:save": {"active": True} # Missing 'key'
}
}
serializer = HotkeysSerializer(data=invalid_data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errors
def test_invalid_key_value(self):
"""Test serializer rejects invalid key values.
This test validates:
- Empty string key values are not allowed
- Key field must contain actual keyboard combination
- Validation catches and rejects meaningless key definitions
Critical validation: Ensures hotkeys have valid keyboard
combinations that can be processed by the frontend system.
"""
invalid_data = {
"custom_hotkeys": {
"editor:save": {"key": ""} # Empty key
}
}
serializer = HotkeysSerializer(data=invalid_data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errors
def test_invalid_active_flag(self):
"""Test serializer rejects non-boolean active flags.
This test validates:
- The 'active' field must be a boolean value when provided
- String representations like 'yes'/'no' are not accepted
- Type validation is properly enforced for optional fields
Critical validation: Ensures hotkey enable/disable state
is properly typed for consistent system behavior.
"""
invalid_data = {
"custom_hotkeys": {
"editor:save": {"key": "ctrl+s", "active": "yes"} # Should be boolean
}
}
serializer = HotkeysSerializer(data=invalid_data)
assert not serializer.is_valid()
assert 'custom_hotkeys' in serializer.errorsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fine with me, but I have a meta question, why are we not using test frameworks provided by the web framework we are using?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
valid question, mostly bc of historical reasons for adopting pytest, which has great support for various extensions.
web/libs/app-common/src/pages/AccountSettings/sections/Hotkeys.scss
Outdated
Show resolved
Hide resolved
Co-authored-by: niklub <[email protected]>
Co-authored-by: niklub <[email protected]>
| return ( | ||
| <div className={styles.accountSettings}> | ||
| <SidebarMenu menuItems={menuItems} path={AccountSettingsPage.path}> | ||
| <div className={contentClassName}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By removing this, we're losing the ability to scroll on a single page - is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no sure I'm fully following the question here. Account settings page was a single long page, instead we're breaking it into a few smaller ones (which is consistent with the project settings).
|
/git merge
|
| // If active is explicitly false, return a copy with key set to null | ||
| if (hotkeyValue && hotkeyValue.active === false) { | ||
| return {...hotkeyValue, key: null}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to have README file describing general principles of hotkeys we use. For example questions regarding this part:
- what does
active: falsemean? when do we have it? - what does
key: nullmean? what do we do with such hotkey?
|
/git merge
|
|
/git merge
|
Reason for Change
Problem:
Label Studio has a rich set of keyboard shortcuts, but users previously had no way to customize them. This limited accessibility and forced users into rigid default behaviors.
Solution:
This PR introduces a fully-featured, user-level hotkey customization system. Users can override default shortcuts by section (e.g. annotation, regions, video), manage them in a dedicated Hotkeys tab in Account Settings, and import/export configurations for reuse or team sharing.
Rollout Strategy
Testing
HotkeysSerializervalidation logicPOST /api/current-user/hotkeyscustom_hotkeysfield toUsermodelRisks
Reviewer Notes
users/serializers.py– Hotkey schema validationHotkey.ts– Lookup/merge logicAccountSettings/sections/Hotkeys– Full UI implementationlookupHotkey('section:action')across the app for dynamic resolutiondefaults.jsGeneral Notes
This upgrade significantly improves usability and flexibility, especially for power users. It also sets the stage for future team-wide profiles or shareable hotkey presets.