mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 06:00:38 +00:00
feat(frontend): add more general hotkeys (#12138)
* add admin button hotkey * add duplicate hotkey * generalised hotkeys for actions * move to fnc * fix type checking * more hotkeys! * add the first primary action to the hotkeys * small fixes * use capital hotkeys * add keys for navigation * add panel navigation hotkeys * remove admin hotkey - navigating to attachments is imo more helpful * address conflict on macos
This commit is contained in:
@@ -17,18 +17,22 @@ export type InvenTreeHotkeyItem = [
|
||||
HotkeyItemOptions?
|
||||
];
|
||||
|
||||
export function useInvenTreeHotkeys(hotkeys: InvenTreeHotkeyItem[]) {
|
||||
// Register the hotkeys using the Mantine hook
|
||||
useHotkeys(
|
||||
hotkeys.map(([key, _, handler, options]) => [key, handler, options])
|
||||
);
|
||||
|
||||
// register to helper state to store hotkeys
|
||||
// This allows us to display the hotkeys in the UI
|
||||
const keyelems: [string, string][] = hotkeys.map(([key, description]) => [
|
||||
export function useInvenTreeHotkeys(_keys: InvenTreeHotkeyItem[]) {
|
||||
const keyelems: [string, string][] = _keys.map(([key, description]) => [
|
||||
key,
|
||||
description
|
||||
]);
|
||||
|
||||
const mappedHotkeys: [
|
||||
string,
|
||||
(event: KeyboardEvent) => void,
|
||||
HotkeyItemOptions?
|
||||
][] = _keys.map(([key, _, handler, options]) => [key, handler, options]);
|
||||
// Register the hotkeys using the Mantine hook
|
||||
useHotkeys(mappedHotkeys);
|
||||
|
||||
// register to helper state to store hotkeys
|
||||
// This allows us to display the hotkeys in the UI
|
||||
useEffect(() => {
|
||||
useLocalLibState.getState().addHotkeys(keyelems);
|
||||
return () =>
|
||||
|
||||
@@ -17,6 +17,7 @@ export type PanelType = {
|
||||
disabled?: boolean;
|
||||
showHeadline?: boolean;
|
||||
supportsDirty?: boolean;
|
||||
hotkey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ScanButton({
|
||||
if (hotkey) {
|
||||
useInvenTreeHotkeys([
|
||||
[
|
||||
'mod+b',
|
||||
'mod+Shift+B',
|
||||
t`Open barcode scanner`,
|
||||
() => {
|
||||
open();
|
||||
|
||||
@@ -34,6 +34,7 @@ export type ActionDropdownItem = {
|
||||
hidden?: boolean;
|
||||
onClick: (event?: any) => void;
|
||||
indicator?: Omit<IndicatorProps, 'children'>;
|
||||
hotkey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -184,7 +185,8 @@ export function BarcodeActionDropdown({
|
||||
title: t`Link Barcode`,
|
||||
icon: <IconLink />,
|
||||
tooltip: t`Link a custom barcode to this item`,
|
||||
ChildItem: QRCodeLink
|
||||
ChildItem: QRCodeLink,
|
||||
hotkey: 'mod+L'
|
||||
}),
|
||||
GeneralBarcodeAction({
|
||||
hidden: hidden || !hash || !permission,
|
||||
@@ -220,6 +222,7 @@ function GeneralBarcodeAction({
|
||||
icon: ReactNode;
|
||||
tooltip: string;
|
||||
ChildItem: any;
|
||||
hotkey?: string;
|
||||
}): ActionDropdownItem {
|
||||
const onClick = () => {
|
||||
modals.open({
|
||||
@@ -255,7 +258,8 @@ export function DeleteItemAction(
|
||||
...props,
|
||||
icon: <IconTrash color='red' />,
|
||||
name: t`Delete`,
|
||||
tooltip: props.tooltip ?? t`Delete item`
|
||||
tooltip: props.tooltip ?? t`Delete item`,
|
||||
hotkey: 'mod+X'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,7 +279,8 @@ export function CancelItemAction(
|
||||
...props,
|
||||
icon: <InvenTreeIcon icon='cancel' iconProps={{ color: 'red' }} />,
|
||||
name: t`Cancel`,
|
||||
tooltip: props.tooltip ?? t`Cancel`
|
||||
tooltip: props.tooltip ?? t`Cancel`,
|
||||
hotkey: 'mod+X'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,6 +292,7 @@ export function DuplicateItemAction(
|
||||
...props,
|
||||
icon: <IconCopy color='green' />,
|
||||
name: t`Duplicate`,
|
||||
tooltip: props.tooltip ?? t`Duplicate item`
|
||||
tooltip: props.tooltip ?? t`Duplicate item`,
|
||||
hotkey: 'mod+D'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ export function HotkeyModal({
|
||||
);
|
||||
keys.sort((a, b) => a.key.localeCompare(b.key));
|
||||
return keys;
|
||||
}, []);
|
||||
}, [context, id]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
return {
|
||||
head: ['Hotkey', 'Action'],
|
||||
|
||||
@@ -57,7 +57,7 @@ export function PageDetail({
|
||||
useInvenTreeHotkeys([
|
||||
[
|
||||
'mod+E',
|
||||
title ? t`Edit ${title}` : t`Edit`,
|
||||
t`Edit`,
|
||||
(event) => {
|
||||
if (event.repeat) {
|
||||
return;
|
||||
@@ -68,6 +68,7 @@ export function PageDetail({
|
||||
}
|
||||
]
|
||||
]);
|
||||
useActionHotkeys(actions);
|
||||
|
||||
const pageTitleString = useMemo(
|
||||
() =>
|
||||
@@ -196,3 +197,74 @@ export function PageDetail({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useActionHotkeys(actions: ReactNode[] = []) {
|
||||
const hotkeys = useMemo(() => extractHotkeys(actions), [actions]);
|
||||
|
||||
useInvenTreeHotkeys(
|
||||
hotkeys.map(({ hotkey, onClick, name }) => [
|
||||
hotkey,
|
||||
name,
|
||||
(event) => {
|
||||
if (event.repeat) {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function extractHotkeys(actions: ReactNode[]) {
|
||||
const calcActions = actions
|
||||
.filter(
|
||||
(action) =>
|
||||
action &&
|
||||
typeof action === 'object' &&
|
||||
'hotkey' in action &&
|
||||
action.hotkey
|
||||
)
|
||||
.map((action: any) => {
|
||||
return {
|
||||
hotkey: action?.hotkey,
|
||||
name: action?.name,
|
||||
onClick: action?.onClick
|
||||
};
|
||||
})
|
||||
.filter((action) => action !== null);
|
||||
|
||||
let primaryActionHotkeyAdded = false;
|
||||
// now iterate over the actions to extract more possible hotkeys
|
||||
actions.forEach((action: any) => {
|
||||
const typeName = action?.type?.name;
|
||||
|
||||
// dropdowns - nested actions
|
||||
if (typeName === 'ActionDropdown' || typeName === 'OptionsActionDropdown') {
|
||||
const dropdownActions = action?.props?.actions as any[];
|
||||
dropdownActions.forEach((dropdownAction: any) => {
|
||||
if (dropdownAction.hotkey) {
|
||||
calcActions.push({
|
||||
hotkey: dropdownAction.hotkey,
|
||||
name: dropdownAction.name,
|
||||
onClick: dropdownAction.onClick
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PrimaryActionButton - use the 'mod+A' hotkey if it is enabled
|
||||
if (typeName === 'PrimaryActionButton' && action?.props?.hidden !== true) {
|
||||
if (primaryActionHotkeyAdded) return;
|
||||
|
||||
const hotkey = action?.props?.hotkey ?? 'mod+A';
|
||||
calcActions.push({
|
||||
hotkey,
|
||||
name:
|
||||
action?.props?.tooltip ?? action?.props?.title ?? t`Primary Action`,
|
||||
onClick: action?.props?.onClick
|
||||
});
|
||||
primaryActionHotkeyAdded = true;
|
||||
}
|
||||
});
|
||||
return calcActions;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function AttachmentPanel({
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip />,
|
||||
hotkey: 'mod+Shift+A',
|
||||
notification_dot: async () => {
|
||||
if (!model_type || !model_id) {
|
||||
return null;
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function NotesPanel({
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes />,
|
||||
hotkey: 'mod+Shift+N',
|
||||
notification_dot: has_note ? 'info' : null,
|
||||
content:
|
||||
model_type && model_id ? (
|
||||
|
||||
@@ -37,7 +37,11 @@ import { Boundary } from '@lib/components/Boundary';
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import type { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||
import { identifierString } from '@lib/functions/Conversion';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import {
|
||||
type InvenTreeHotkeyItem,
|
||||
cancelEvent,
|
||||
useInvenTreeHotkeys
|
||||
} from '@lib/functions/Events';
|
||||
import { eventModified, getBaseUrl } from '@lib/functions/Navigation';
|
||||
import { navigateToLink } from '@lib/functions/Navigation';
|
||||
import type {
|
||||
@@ -335,6 +339,22 @@ function BasePanelGroup({
|
||||
}
|
||||
}, [activePanels, panel]);
|
||||
|
||||
// hotkeys
|
||||
const hotkeys = useMemo(() => {
|
||||
const keys: InvenTreeHotkeyItem[] = [];
|
||||
activePanels.forEach((panel) => {
|
||||
if (panel.hotkey) {
|
||||
keys.push([
|
||||
panel.hotkey,
|
||||
t`Navigate to panel ${panel.name}`,
|
||||
() => handlePanelChange(panel.name)
|
||||
]);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}, [activePanels]);
|
||||
useInvenTreeHotkeys(hotkeys);
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
useWindowEvent('beforeunload', (event) => {
|
||||
if (isDirty) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function ParametersPanel({
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
hotkey: 'mod+Shift+P',
|
||||
hidden: hidden ?? false,
|
||||
notification_dot: async () => {
|
||||
if (!model_type || !model_id) {
|
||||
|
||||
Reference in New Issue
Block a user