mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-28 14:52:28 +03:00
2311 embed keyboard shortcuts (#2507)
* 2311-feat(front): AppHotKeyScope and CustomHotKeyScopes configured * 2311-feat(front): Groups and Items added * 2311-fix: pr requested changes --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
a67199e0c3
commit
ff42526a09
@ -0,0 +1,47 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
|
||||
import {
|
||||
keyboardShortcutsGeneral,
|
||||
keyboardShortcutsTable,
|
||||
} from '../constants/keyboardShortcuts';
|
||||
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
|
||||
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
|
||||
|
||||
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
|
||||
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
|
||||
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
|
||||
|
||||
export const KeyboardShortcutMenu = () => {
|
||||
const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
||||
const isKeyboardShortcutMenuOpened = useRecoilValue(
|
||||
isKeyboardShortcutMenuOpenedState,
|
||||
);
|
||||
useScopedHotkeys(
|
||||
'shift+?,meta+?,esc',
|
||||
() => {
|
||||
toggleKeyboardShortcutMenu();
|
||||
},
|
||||
AppHotkeyScope.KeyboardShortcutMenu,
|
||||
[toggleKeyboardShortcutMenu],
|
||||
);
|
||||
|
||||
return (
|
||||
isKeyboardShortcutMenuOpened && (
|
||||
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
|
||||
<KeyboardMenuGroup heading="Table">
|
||||
{keyboardShortcutsTable.map((TableShortcut) => (
|
||||
<KeyboardMenuItem shortcut={TableShortcut} />
|
||||
))}
|
||||
</KeyboardMenuGroup>
|
||||
<KeyboardMenuGroup heading="General">
|
||||
{keyboardShortcutsGeneral.map((GeneralShortcut) => (
|
||||
<KeyboardMenuItem shortcut={GeneralShortcut} />
|
||||
))}
|
||||
</KeyboardMenuGroup>
|
||||
</KeyboardMenuDialog>
|
||||
)
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import { IconX } from '@/ui/display/icon';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
|
||||
import {
|
||||
StyledContainer,
|
||||
StyledDialog,
|
||||
StyledHeading,
|
||||
} from './KeyboardShortcutMenuStyles';
|
||||
|
||||
type KeyboardMenuDialogProps = {
|
||||
onClose: () => void;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export const KeyboardMenuDialog = ({
|
||||
onClose,
|
||||
children,
|
||||
}: KeyboardMenuDialogProps) => {
|
||||
return (
|
||||
<StyledDialog>
|
||||
<StyledHeading>
|
||||
Keyboard shortcuts
|
||||
<IconButton variant="tertiary" Icon={IconX} onClick={onClose} />
|
||||
</StyledHeading>
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { StyledGroup, StyledGroupHeading } from './KeyboardShortcutMenuStyles';
|
||||
|
||||
type KeyboardMenuGroupProps = {
|
||||
heading: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export const KeyboardMenuGroup = ({
|
||||
heading,
|
||||
children,
|
||||
}: KeyboardMenuGroupProps) => {
|
||||
return (
|
||||
<StyledGroup>
|
||||
<StyledGroupHeading>{heading}</StyledGroupHeading>
|
||||
{children}
|
||||
</StyledGroup>
|
||||
);
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import {
|
||||
StyledItem,
|
||||
StyledShortcutKey,
|
||||
StyledShortcutKeyContainer,
|
||||
} from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles';
|
||||
import { Shortcut } from '@/keyboard-shortcut-menu/types/Shortcut';
|
||||
|
||||
type KeyboardMenuItemProps = {
|
||||
shortcut: Shortcut;
|
||||
};
|
||||
|
||||
export const KeyboardMenuItem = ({ shortcut }: KeyboardMenuItemProps) => {
|
||||
return (
|
||||
<StyledItem>
|
||||
{shortcut.label}
|
||||
{shortcut.secondHotKey ? (
|
||||
shortcut.areSimultaneous ? (
|
||||
<StyledShortcutKeyContainer>
|
||||
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||
<StyledShortcutKey>{shortcut.secondHotKey}</StyledShortcutKey>
|
||||
</StyledShortcutKeyContainer>
|
||||
) : (
|
||||
<StyledShortcutKeyContainer>
|
||||
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||
then
|
||||
<StyledShortcutKey>{shortcut.secondHotKey}</StyledShortcutKey>
|
||||
</StyledShortcutKeyContainer>
|
||||
)
|
||||
) : (
|
||||
<StyledShortcutKey>{shortcut.firstHotKey}</StyledShortcutKey>
|
||||
)}
|
||||
</StyledItem>
|
||||
);
|
||||
};
|
@ -0,0 +1,88 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
export const StyledDialog = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
left: 50%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
transform: ${() =>
|
||||
useIsMobile() ? 'translateX(-49.5%)' : 'translateX(-50%)'};
|
||||
width: ${() => (useIsMobile() ? 'calc(100% - 40px)' : '100%')};
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
export const StyledHeading = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
justify-content: space-between;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const StyledGroupHeading = styled.label`
|
||||
color: ${({ theme }) => theme.color.gray50};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const StyledGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const StyledItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
height: 24px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const StyledShortcutKey = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.strong};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.underline};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const StyledShortcutKeyContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
@ -0,0 +1,39 @@
|
||||
import { Shortcut, ShortcutType } from '../types/Shortcut';
|
||||
|
||||
export const keyboardShortcutsTable: Shortcut[] = [
|
||||
{
|
||||
label: 'Move right',
|
||||
type: ShortcutType.Table,
|
||||
firstHotKey: '→',
|
||||
areSimultaneous: true,
|
||||
},
|
||||
{
|
||||
label: 'Move left',
|
||||
type: ShortcutType.Table,
|
||||
firstHotKey: '←',
|
||||
areSimultaneous: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear selection',
|
||||
type: ShortcutType.Table,
|
||||
firstHotKey: 'esc',
|
||||
areSimultaneous: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const keyboardShortcutsGeneral: Shortcut[] = [
|
||||
{
|
||||
label: 'Open search',
|
||||
type: ShortcutType.General,
|
||||
firstHotKey: '⌘',
|
||||
secondHotKey: 'K',
|
||||
areSimultaneous: false,
|
||||
},
|
||||
{
|
||||
label: 'Mark as favourite',
|
||||
type: ShortcutType.General,
|
||||
firstHotKey: '⇧',
|
||||
secondHotKey: 'F',
|
||||
areSimultaneous: false,
|
||||
},
|
||||
];
|
@ -0,0 +1,47 @@
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
|
||||
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
|
||||
|
||||
export const useKeyboardShortcutMenu = () => {
|
||||
const [, setIsKeyboardShortcutMenuOpened] = useRecoilState(
|
||||
isKeyboardShortcutMenuOpenedState,
|
||||
);
|
||||
const isKeyboardShortcutMenuOpened = useRecoilValue(
|
||||
isKeyboardShortcutMenuOpenedState,
|
||||
);
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const toggleKeyboardShortcutMenu = () => {
|
||||
if (isKeyboardShortcutMenuOpened === false) {
|
||||
setIsKeyboardShortcutMenuOpened(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
AppHotkeyScope.KeyboardShortcutMenu,
|
||||
);
|
||||
} else {
|
||||
setIsKeyboardShortcutMenuOpened(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
}
|
||||
};
|
||||
|
||||
const openKeyboardShortcutMenu = () => {
|
||||
setIsKeyboardShortcutMenuOpened(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.KeyboardShortcutMenu);
|
||||
};
|
||||
|
||||
const closeKeyboardShortcutMenu = () => {
|
||||
setIsKeyboardShortcutMenuOpened(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
|
||||
return {
|
||||
toggleKeyboardShortcutMenu,
|
||||
openKeyboardShortcutMenu,
|
||||
closeKeyboardShortcutMenu,
|
||||
};
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isKeyboardShortcutMenuOpenedState = atom({
|
||||
key: 'keyboard-shortcut-menu/isKeyboardShortcutMenuOpenedState',
|
||||
default: false,
|
||||
});
|
12
front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
Normal file
12
front/src/modules/keyboard-shortcut-menu/types/Shortcut.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export enum ShortcutType {
|
||||
Table = 'Table',
|
||||
General = 'General',
|
||||
}
|
||||
|
||||
export type Shortcut = {
|
||||
label: string;
|
||||
type: ShortcutType.Table | ShortcutType.General;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
areSimultaneous: boolean;
|
||||
};
|
@ -6,6 +6,7 @@ import { AuthModal } from '@/auth/components/Modal';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||
import { NavbarAnimatedContainer } from '@/ui/navigation/navbar/components/NavbarAnimatedContainer';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { AppNavbar } from '~/AppNavbar';
|
||||
@ -58,6 +59,7 @@ export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
|
||||
return (
|
||||
<StyledLayout>
|
||||
<CommandMenu />
|
||||
<KeyboardShortcutMenu />
|
||||
<NavbarAnimatedContainer>
|
||||
<AppNavbar />
|
||||
</NavbarAnimatedContainer>
|
||||
|
@ -5,6 +5,7 @@ import { HotkeyScope } from '../types/HotkeyScope';
|
||||
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
|
||||
commandMenu: true,
|
||||
goto: false,
|
||||
keyboardShortcutMenu: true,
|
||||
};
|
||||
|
||||
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
||||
@ -12,5 +13,6 @@ export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
||||
customScopes: {
|
||||
commandMenu: true,
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
},
|
||||
};
|
||||
|
@ -15,7 +15,8 @@ const isCustomScopesEqual = (
|
||||
) => {
|
||||
return (
|
||||
customScopesA?.commandMenu === customScopesB?.commandMenu &&
|
||||
customScopesA?.goto === customScopesB?.goto
|
||||
customScopesA?.goto === customScopesB?.goto &&
|
||||
customScopesA?.keyboardShortcutMenu === customScopesB?.keyboardShortcutMenu
|
||||
);
|
||||
};
|
||||
|
||||
@ -54,6 +55,7 @@ export const useSetHotkeyScope = () =>
|
||||
customScopes: {
|
||||
commandMenu: customScopes?.commandMenu ?? true,
|
||||
goto: customScopes?.goto ?? false,
|
||||
keyboardShortcutMenu: customScopes?.keyboardShortcutMenu ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
@ -67,6 +69,10 @@ export const useSetHotkeyScope = () =>
|
||||
scopesToSet.push(AppHotkeyScope.Goto);
|
||||
}
|
||||
|
||||
if (newHotkeyScope?.customScopes?.keyboardShortcutMenu) {
|
||||
scopesToSet.push(AppHotkeyScope.KeyboardShortcutMenu);
|
||||
}
|
||||
|
||||
scopesToSet.push(newHotkeyScope.scope);
|
||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||
set(currentHotkeyScopeState, newHotkeyScope);
|
||||
|
@ -2,4 +2,5 @@ export enum AppHotkeyScope {
|
||||
App = 'app',
|
||||
Goto = 'goto',
|
||||
CommandMenu = 'command-menu',
|
||||
KeyboardShortcutMenu = 'keyboard-shortcut-menu',
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export type CustomHotkeyScopes = {
|
||||
goto?: boolean;
|
||||
commandMenu?: boolean;
|
||||
keyboardShortcutMenu?: boolean;
|
||||
};
|
||||
|
@ -7,7 +7,11 @@ export const InitializeHotkeyStorybookHookEffect = () => {
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
useEffect(() => {
|
||||
setHotkeyScope(AppHotkeyScope.App, { commandMenu: true, goto: false });
|
||||
setHotkeyScope(AppHotkeyScope.App, {
|
||||
commandMenu: true,
|
||||
goto: false,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
}, [setHotkeyScope]);
|
||||
|
||||
return <></>;
|
||||
|
Loading…
Reference in New Issue
Block a user