Feat/better hotkeys scope (#526)

* Working version

* fix

* Fixed console log

* Fix lint

* wip

* Fix

* Fix

* consolelog

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-08 03:53:05 +02:00
committed by GitHub
parent 611cda1f41
commit 66dcc9b2e1
77 changed files with 1240 additions and 454 deletions

View File

@ -39,6 +39,7 @@
"react-tooltip": "^5.13.1",
"recoil": "^0.7.7",
"scroll-into-view": "^1.16.2",
"ts-key-enum": "^2.0.12",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},

View File

@ -1,11 +1,9 @@
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { AnimatePresence, LayoutGroup } from 'framer-motion';
import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
import { RequireOnboarded } from '@/auth/components/RequireOnboarded';
import { RequireOnboarding } from '@/auth/components/RequireOnboarding';
import { AuthModal } from '@/auth/components/ui/Modal';
import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
import { AuthLayout } from '@/ui/layout/AuthLayout';
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
import { CreateProfile } from '~/pages/auth/CreateProfile';
@ -18,6 +16,8 @@ import { Opportunities } from '~/pages/opportunities/Opportunities';
import { People } from '~/pages/people/People';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { AppInternalHooks } from './AppInternalHooks';
/**
* AuthRoutes is used to allow transitions between auth pages with framer-motion.
*/
@ -42,48 +42,44 @@ function AuthRoutes() {
}
export function App() {
useGoToHotkeys('p', '/people');
useGoToHotkeys('c', '/companies');
useGoToHotkeys('o', '/opportunities');
useGoToHotkeys('s', '/settings/profile');
useTrackPageView();
return (
<DefaultLayout>
<Routes>
<Route
path="auth/*"
element={
<RequireOnboarding>
<AuthLayout>
<AuthRoutes />
</AuthLayout>
</RequireOnboarding>
}
/>
<Route
path="*"
element={
<RequireOnboarded>
<Routes>
<Route path="" element={<Navigate to="/people" replace />} />
<Route path="people" element={<People />} />
<Route path="companies" element={<Companies />} />
<Route path="opportunities" element={<Opportunities />} />
<Route
path="settings/*"
element={
<Routes>
<Route path="profile" element={<SettingsProfile />} />
</Routes>
}
/>
</Routes>
</RequireOnboarded>
}
/>
</Routes>
</DefaultLayout>
<>
<AppInternalHooks />
<DefaultLayout>
<Routes>
<Route
path="auth/*"
element={
<RequireOnboarding>
<AuthLayout>
<AuthRoutes />
</AuthLayout>
</RequireOnboarding>
}
/>
<Route
path="*"
element={
<RequireOnboarded>
<Routes>
<Route path="" element={<Navigate to="/people" replace />} />
<Route path="people" element={<People />} />
<Route path="companies" element={<Companies />} />
<Route path="opportunities" element={<Opportunities />} />
<Route
path="settings/*"
element={
<Routes>
<Route path="profile" element={<SettingsProfile />} />
</Routes>
}
/>
</Routes>
</RequireOnboarded>
}
/>
</Routes>
</DefaultLayout>
</>
);
}

View File

@ -0,0 +1,13 @@
import { AnalyticsHook } from './sync-hooks/AnalyticsHook';
import { GotoHotkeysHooks } from './sync-hooks/GotoHotkeysHooks';
import { HotkeysScopeStackAutoSyncHook } from './sync-hooks/HotkeysScopeStackAutoSyncHook';
export function AppInternalHooks() {
return (
<>
<AnalyticsHook />
<GotoHotkeysHooks />
<HotkeysScopeStackAutoSyncHook />
</>
);
}

View File

@ -1,8 +1,10 @@
import React, { StrictMode } from 'react';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { HotkeysProvider } from 'react-hotkeys-hook';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { INITIAL_HOTKEYS_SCOPES } from '@/hotkeys/constants';
import { ThemeType } from '@/ui/themes/themes';
import '@emotion/react';
@ -27,9 +29,11 @@ root.render(
<StrictMode>
<UserProvider>
<ClientConfigProvider>
<BrowserRouter>
<App />
</BrowserRouter>
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}>
<BrowserRouter>
<App />
</BrowserRouter>
</HotkeysProvider>
</ClientConfigProvider>
</UserProvider>
</StrictMode>

View File

@ -2,9 +2,6 @@ import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
import { OnboardingStatus } from '../utils/getOnboardingStatus';
@ -38,9 +35,6 @@ export function RequireOnboarded({
}): JSX.Element {
const navigate = useNavigate();
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const onboardingStatus = useOnboardingStatus();
useEffect(() => {
@ -53,12 +47,6 @@ export function RequireOnboarded({
}
}, [onboardingStatus, navigate]);
useEffect(() => {
if (onboardingStatus === OnboardingStatus.Completed) {
setCaptureHotkeyTypeInFocus(false);
}
}, [setCaptureHotkeyTypeInFocus, onboardingStatus]);
if (onboardingStatus !== OnboardingStatus.Completed) {
return (
<EmptyContainer>

View File

@ -1,6 +1,8 @@
import React from 'react';
import styled from '@emotion/styled';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { Modal as UIModal } from '@/ui/components/modal/Modal';
type Props = React.ComponentProps<'div'>;
@ -17,6 +19,11 @@ const StyledContainer = styled.div`
`;
export function AuthModal({ children, ...restProps }: Props) {
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.Modal,
customScopes: { 'command-menu': false, goto: false },
});
return (
<UIModal isOpen={true}>
<StyledContainer {...restProps}>{children}</StyledContainer>

View File

@ -1,7 +1,8 @@
import React from 'react';
import { useRecoilState } from 'recoil';
import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys';
import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpened';
@ -18,14 +19,20 @@ import {
export function CommandMenu() {
const [open, setOpen] = useRecoilState(isCommandMenuOpenedState);
useDirectHotkeys(
useScopedHotkeys(
'ctrl+k,meta+k',
() => {
setOpen((prevOpen) => !prevOpen);
},
InternalHotkeysScope.CommandMenu,
[setOpen],
);
useHotkeysScopeOnBooleanState(
{ scope: InternalHotkeysScope.CommandMenu },
open,
);
/*
TODO: Allow performing actions on page through CommandBar

View File

@ -1,5 +1,4 @@
import { useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
@ -13,7 +12,11 @@ import { IconArrowUpRight } from '@tabler/icons-react';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import CompanyChip from '@/companies/components/CompanyChip';
import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { PersonChip } from '@/people/components/PersonChip';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
@ -95,6 +98,11 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState('');
useHotkeysScopeOnBooleanState(
{ scope: InternalHotkeysScope.RelationPicker },
isMenuOpen,
);
const theme = useTheme();
const peopleIds =
@ -156,15 +164,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
setSearchFilter('');
}
useHotkeys(
useScopedHotkeys(
['esc', 'enter'],
() => {
exitEditMode();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.RelationPicker,
[exitEditMode],
);
@ -225,19 +230,21 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
)}
</StyledRelationContainer>
{isMenuOpen && (
<StyledMenuWrapper ref={refs.setFloating} style={floatingStyles}>
<MultipleEntitySelect
entities={{
entitiesToSelect,
filteredSelectedEntities,
selectedEntities,
loading: false, // TODO implement skeleton loading
}}
onItemCheckChange={handleCheckItemChange}
onSearchFilterChange={handleFilterChange}
searchFilter={searchFilter}
/>
</StyledMenuWrapper>
<RecoilScope>
<StyledMenuWrapper ref={refs.setFloating} style={floatingStyles}>
<MultipleEntitySelect
entities={{
entitiesToSelect,
filteredSelectedEntities,
selectedEntities,
loading: false, // TODO implement skeleton loading
}}
onItemCheckChange={handleCheckItemChange}
onSearchFilterChange={handleFilterChange}
searchFilter={searchFilter}
/>
</StyledMenuWrapper>
</RecoilScope>
)}
</StyledContainer>
);

View File

@ -0,0 +1,17 @@
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
export const INITIAL_HOTKEYS_SCOPES: string[] = [InternalHotkeysScope.App];
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
InternalHotkeysScope.CommandMenu,
InternalHotkeysScope.App,
];
export const DEFAULT_HOTKEYS_SCOPE_STACK_ITEM: HotkeysScopeStackItem = {
scope: InternalHotkeysScope.App,
customScopes: {
'command-menu': true,
goto: true,
},
};

View File

@ -0,0 +1,103 @@
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '@/hotkeys/states/internal/internalHotkeysEnabledScopesState';
export function useHotkeysScope() {
const { disableScope, enableScope } = useHotkeysContext();
const disableAllHotkeysScopes = useRecoilCallback(
({ set, snapshot }) => {
return async () => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
for (const enabledScope of enabledScopes) {
disableScope(enabledScope);
}
set(internalHotkeysEnabledScopesState, []);
};
},
[disableScope],
);
const enableHotkeysScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToEnable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
if (!enabledScopes.includes(scopeToEnable)) {
enableScope(scopeToEnable);
set(internalHotkeysEnabledScopesState, [
...enabledScopes,
scopeToEnable,
]);
}
};
},
[enableScope],
);
const disableHotkeysScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToDisable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopeToRemoveIndex = enabledScopes.findIndex(
(scope) => scope === scopeToDisable,
);
if (scopeToRemoveIndex > -1) {
disableScope(scopeToDisable);
enabledScopes.splice(scopeToRemoveIndex);
set(internalHotkeysEnabledScopesState, enabledScopes);
}
};
},
[disableScope],
);
const setHotkeysScopes = useRecoilCallback(
({ set, snapshot }) => {
return async (scopesToSet: string[]) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopesToDisable = enabledScopes.filter(
(enabledScope) => !scopesToSet.includes(enabledScope),
);
const scopesToEnable = scopesToSet.filter(
(scopeToSet) => !enabledScopes.includes(scopeToSet),
);
for (const scopeToDisable of scopesToDisable) {
disableScope(scopeToDisable);
}
for (const scopeToEnable of scopesToEnable) {
enableScope(scopeToEnable);
}
set(internalHotkeysEnabledScopesState, scopesToSet);
};
},
[disableScope, enableScope],
);
return {
disableAllHotkeysScopes,
enableHotkeysScope,
disableHotkeysScope,
setHotkeysScopes,
};
}

View File

@ -0,0 +1,37 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { customHotkeysScopesState } from '@/hotkeys/states/internal/customHotkeysScopesState';
import { hotkeysScopeStackState } from '@/hotkeys/states/internal/hotkeysScopeStackState';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useHotkeysScope } from './useHotkeysScope';
export function useHotkeysScopeStackAutoSync() {
const { setHotkeysScopes } = useHotkeysScope();
const hotkeysScopeStack = useRecoilValue(hotkeysScopeStackState);
const customHotkeysScopes = useRecoilValue(customHotkeysScopesState);
useEffect(() => {
if (hotkeysScopeStack.length === 0) {
return;
}
const scopesToSet: string[] = [];
const currentHotkeysScope = hotkeysScopeStack[hotkeysScopeStack.length - 1];
if (currentHotkeysScope.customScopes?.['command-menu']) {
scopesToSet.push(InternalHotkeysScope.CommandMenu);
}
if (currentHotkeysScope?.customScopes?.goto) {
scopesToSet.push(InternalHotkeysScope.Goto);
}
scopesToSet.push(currentHotkeysScope.scope);
setHotkeysScopes(scopesToSet);
}, [setHotkeysScopes, customHotkeysScopes, hotkeysScopeStack]);
}

View File

@ -0,0 +1,48 @@
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
export function useAddToHotkeysScopeStack() {
return useRecoilCallback(
({ snapshot, set }) =>
async ({
scope,
customScopes = {
'command-menu': true,
goto: false,
},
ancestorScope,
}: HotkeysScopeStackItem) => {
const hotkeysScopeStack = await snapshot.getPromise(
hotkeysScopeStackState,
);
const currentHotkeysScope =
hotkeysScopeStack.length > 0
? hotkeysScopeStack[hotkeysScopeStack.length - 1]
: null;
const previousHotkeysScope =
hotkeysScopeStack.length > 1
? hotkeysScopeStack[hotkeysScopeStack.length - 2]
: null;
if (
scope === currentHotkeysScope?.scope ||
scope === previousHotkeysScope?.scope
) {
return;
}
set(
hotkeysScopeStackState,
produce(hotkeysScopeStack, (draft) => {
draft.push({ scope, customScopes, ancestorScope });
}),
);
},
[],
);
}

View File

@ -0,0 +1,16 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
export function useCurrentHotkeysScope() {
const hotkeysScopeStack = useRecoilValue(hotkeysScopeStackState);
return useMemo(() => {
if (hotkeysScopeStack.length === 0) {
return null;
} else {
return hotkeysScopeStack[hotkeysScopeStack.length - 1];
}
}, [hotkeysScopeStack]);
}

View File

@ -1,11 +1,19 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useNavigate } from 'react-router-dom';
import { useSequenceHotkeys } from './useSequenceHotkeys';
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
export function useGoToHotkeys(key: string, location: string) {
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
export function useGoToHotkeys(key: Keys, location: string) {
const navigate = useNavigate();
useSequenceHotkeys('g', key, () => {
navigate(location);
});
useSequenceHotkeys(
'g',
key,
() => {
navigate(location);
},
InternalHotkeysScope.Goto,
);
}

View File

@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
import { useRemoveFromHotkeysScopeStack } from './useRemoveFromHotkeysScopeStack';
export function useHotkeysScopeOnBooleanState(
hotkeysScopeStackItem: HotkeysScopeStackItem,
booleanState: boolean,
) {
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
const removeFromHoteysScopeStack = useRemoveFromHotkeysScopeStack();
useEffect(() => {
if (booleanState) {
addToHotkeysScopeStack(hotkeysScopeStackItem);
} else {
removeFromHoteysScopeStack(hotkeysScopeStackItem.scope);
}
}, [
hotkeysScopeStackItem,
removeFromHoteysScopeStack,
addToHotkeysScopeStack,
booleanState,
]);
}

View File

@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
export function useHotkeysScopeOnMountOnly(
hotkeysScopeStackItem: HotkeysScopeStackItem,
enabled = true,
) {
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
const [hotkeysScopeStack] = useRecoilState(hotkeysScopeStackState);
const hotkeysScopeAlreadyInStack = hotkeysScopeStack.some(
(hotkeysScopeStackItemToFind) =>
hotkeysScopeStackItemToFind.scope === hotkeysScopeStackItem.scope,
);
useEffect(() => {
if (!hotkeysScopeAlreadyInStack && enabled) {
addToHotkeysScopeStack(hotkeysScopeStackItem);
}
}, [
enabled,
addToHotkeysScopeStack,
hotkeysScopeStackItem,
hotkeysScopeAlreadyInStack,
]);
}

View File

@ -0,0 +1,54 @@
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '../constants';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
export function useRemoveFromHotkeysScopeStack() {
return useRecoilCallback(
({ snapshot, set }) =>
async (hotkeysScopeToRemove: string) => {
const hotkeysScopeStack = await snapshot.getPromise(
hotkeysScopeStackState,
);
if (hotkeysScopeStack.length < 1) {
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
return;
}
const currentHotkeysScope =
hotkeysScopeStack[hotkeysScopeStack.length - 1];
if (hotkeysScopeStack.length === 1) {
if (currentHotkeysScope?.scope !== InternalHotkeysScope.App) {
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
}
return;
}
const previousHotkeysScope =
hotkeysScopeStack[hotkeysScopeStack.length - 2];
if (
previousHotkeysScope.scope === hotkeysScopeToRemove ||
currentHotkeysScope.scope !== hotkeysScopeToRemove
) {
return;
}
set(
hotkeysScopeStackState,
produce(hotkeysScopeStack, (draft) => {
return draft.filter(
(hotkeysScope) => hotkeysScope.scope !== hotkeysScopeToRemove,
);
}),
);
},
[],
);
}

View File

@ -0,0 +1,42 @@
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '../constants';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
export function useRemoveHighestHotkeysScopeStackItem() {
return useRecoilCallback(
({ snapshot, set }) =>
async () => {
const hotkeysScopeStack = await snapshot.getPromise(
hotkeysScopeStackState,
);
if (hotkeysScopeStack.length < 1) {
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
return;
}
const currentHotkeysScope =
hotkeysScopeStack[hotkeysScopeStack.length - 1];
if (hotkeysScopeStack.length === 1) {
if (currentHotkeysScope?.scope !== InternalHotkeysScope.App) {
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
}
return;
}
set(
hotkeysScopeStackState,
produce(hotkeysScopeStack, (draft) => {
draft.pop();
}),
);
},
[],
);
}

View File

@ -0,0 +1,18 @@
import { useResetRecoilState } from 'recoil';
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
export function useResetHotkeysScopeStack() {
const resetHotkeysScopeStack = useResetRecoilState(hotkeysScopeStackState);
const addHotkeysScopedStack = useAddToHotkeysScopeStack();
return function reset(toFirstScope?: string) {
resetHotkeysScopeStack();
if (toFirstScope) {
addHotkeysScopedStack({ scope: toFirstScope });
}
};
}

View File

@ -2,16 +2,24 @@ import { useHotkeys } from 'react-hotkeys-hook';
import {
Hotkey,
HotkeyCallback,
Keys,
Options,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/pendingHotkeysState';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
export function useDirectHotkeys(
keys: string,
export function useScopedHotkeys(
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: OptionsOrDependencyArray,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
@ -26,5 +34,10 @@ export function useDirectHotkeys(
setPendingHotkey(null);
}
useHotkeys(keys, callbackIfDirectKey, dependencies);
return useHotkeys(
keys,
callbackIfDirectKey,
{ ...options, scopes: [scope] },
dependencies,
);
}

View File

@ -1,12 +1,19 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { Options, useHotkeys } from 'react-hotkeys-hook';
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/pendingHotkeysState';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
export function useSequenceHotkeys(
firstKey: string,
secondKey: string,
firstKey: Keys,
secondKey: Keys,
callback: () => void,
scope: string,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
@ -15,6 +22,7 @@ export function useSequenceHotkeys(
() => {
setPendingHotkey(firstKey);
},
{ ...options, scopes: [scope] },
[pendingHotkey],
);
@ -27,6 +35,7 @@ export function useSequenceHotkeys(
setPendingHotkey(null);
callback();
},
{ ...options, scopes: [scope] },
[pendingHotkey, setPendingHotkey],
);
}

View File

@ -1,31 +0,0 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import { OptionsOrDependencyArray } from 'react-hotkeys-hook/dist/types';
export function useUpDownHotkeys(
upArrowCallBack: HotkeyCallback,
downArrownCallback: HotkeyCallback,
dependencies?: OptionsOrDependencyArray,
) {
useHotkeys(
'up',
upArrowCallBack,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
dependencies,
);
useHotkeys(
'down',
downArrownCallback,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
dependencies,
);
}

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const captureHotkeyTypeInFocusState = atom<boolean>({
key: 'captureHotkeyTypeInFocusState',
default: false,
});

View File

@ -0,0 +1,16 @@
import { atom } from 'recoil';
import { InternalHotkeysScope } from '../../types/internal/InternalHotkeysScope';
export type CustomHotkeysScopes = {
[InternalHotkeysScope.Goto]: boolean;
[InternalHotkeysScope.CommandMenu]: boolean;
};
export const customHotkeysScopesState = atom<CustomHotkeysScopes>({
key: 'customHotkeysScopesState',
default: {
'command-menu': true,
goto: false,
},
});

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '@/hotkeys/constants';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
export const hotkeysScopeStackState = atom<HotkeysScopeStackItem[]>({
key: 'hotkeysScopeStackState',
default: [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM],
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const internalHotkeysEnabledScopesState = atom<string[]>({
key: 'internalHotkeysEnabledScopesState',
default: [],
});

View File

@ -0,0 +1,7 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { atom } from 'recoil';
export const pendingHotkeyState = atom<Keys | null>({
key: 'pendingHotkeyState',
default: null,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const pendingHotkeyState = atom<string | null>({
key: 'pendingHotkeyState',
default: null,
});

View File

@ -0,0 +1,3 @@
export enum HotkeysScope {
CompanyPage = 'company-page',
}

View File

@ -0,0 +1,7 @@
import { CustomHotkeysScopes } from '@/hotkeys/states/internal/customHotkeysScopesState';
export type HotkeysScopeStackItem = {
scope: string;
customScopes?: CustomHotkeysScopes;
ancestorScope?: string | null;
};

View File

@ -0,0 +1,14 @@
export enum InternalHotkeysScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
Table = 'table',
TableSoftFocus = 'table-soft-focus',
CellEditMode = 'cell-edit-mode',
RightDrawer = 'right-drawer',
TableHeaderDropdownButton = 'table-header-dropdown-button',
CreateProfile = 'create-profile',
RelationPicker = 'relation-picker',
CellDoubleTextInput = 'cell-double-text-input',
Modal = 'modal',
}

View File

@ -1,4 +1,5 @@
import CompanyChip from '@/companies/components/CompanyChip';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
@ -19,6 +20,9 @@ export function PeopleCompanyCell({ people }: OwnProps) {
return (
<EditableCell
editHotkeysScope={
!isCreating ? { scope: InternalHotkeysScope.RelationPicker } : undefined
}
editModeContent={
isCreating ? (
<PeopleCompanyCreateCell people={people} />

View File

@ -1,3 +1,7 @@
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
@ -57,6 +61,13 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
setIsCreating(true);
}
useScopedHotkeys(
Key.Escape,
() => closeEditableCell(),
InternalHotkeysScope.RelationPicker,
[closeEditableCell],
);
return (
<SingleEntitySelect
onCreate={handleCreate}

View File

@ -1,6 +1,7 @@
import { useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
@ -40,15 +41,13 @@ export function SingleEntitySelectBase<
containerRef,
});
useHotkeys(
// TODO: move to better place for scopping
useScopedHotkeys(
'enter',
() => {
onEntitySelected(entitiesInDropdown[hoveredIndex]);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.RelationPicker,
[entitiesInDropdown, hoveredIndex, onEntitySelected],
);

View File

@ -1,6 +1,8 @@
import scrollIntoView from 'scroll-into-view';
import { Key } from 'ts-key-enum';
import { useUpDownHotkeys } from '@/hotkeys/hooks/useUpDownHotkeys';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
@ -19,7 +21,8 @@ export function useEntitySelectScroll<
relationPickerHoverIndexScopedState,
);
useUpDownHotkeys(
useScopedHotkeys(
Key.ArrowUp,
() => {
setHoveredIndex((prevSelectedIndex) =>
Math.max(prevSelectedIndex - 1, 0),
@ -41,6 +44,12 @@ export function useEntitySelectScroll<
});
}
},
InternalHotkeysScope.RelationPicker,
[setHoveredIndex, entities],
);
useScopedHotkeys(
Key.ArrowDown,
() => {
setHoveredIndex((prevSelectedIndex) =>
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
@ -62,6 +71,7 @@ export function useEntitySelectScroll<
});
}
},
InternalHotkeysScope.RelationPicker,
[setHoveredIndex, entities],
);

View File

@ -1,12 +1,16 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { isEditModeScopedState } from './states/isEditModeScopedState';
import { useSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
@ -27,6 +31,7 @@ type OwnProps = {
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
editHotkeysScope?: HotkeysScopeStackItem;
};
export function EditableCell({
@ -34,39 +39,51 @@ export function EditableCell({
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
editHotkeysScope,
}: OwnProps) {
const [isEditMode] = useRecoilScopedState(isEditModeScopedState);
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const setSoftFocusOnCurrentCell = useSoftFocusOnCurrentCell();
const { closeEditableCell, openEditableCell } = useEditableCell();
const { openEditableCell } = useEditableCell();
const isSoftFocusActive = useRecoilValue(isSoftFocusActiveState);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
// TODO: we might have silent problematic behavior because of the setTimeout in openEditableCell, investigate
// Maybe we could build a switchEditableCell to handle the case where we go from one cell to another.
// See https://github.com/twentyhq/twenty/issues/446
function handleOnClick() {
openEditableCell();
setSoftFocusOnCurrentCell();
}
if (isCurrentCellInEditMode) {
return;
}
function handleOnOutsideClick() {
closeEditableCell();
if (isSoftFocusActive) {
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
}
setSoftFocusOnCurrentCell();
}
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
<CellBaseContainer onClick={handleOnClick}>
{isEditMode ? (
{isCurrentCellInEditMode ? (
<EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</EditableCellEditMode>
) : hasSoftFocus ? (
<EditableCellSoftFocusMode>
<EditableCellSoftFocusMode editHotkeysScope={editHotkeysScope}>
{nonEditModeContent}
</EditableCellSoftFocusMode>
) : (

View File

@ -1,7 +1,8 @@
import { ReactElement, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled from '@emotion/styled';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { overlayBackground } from '@/ui/themes/effects';
@ -38,7 +39,6 @@ export function EditableCellEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
onOutsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
@ -46,61 +46,45 @@ export function EditableCellEditMode({
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useListenClickOutsideArrayOfRef([wrapperRef], () => {
onOutsideClick?.();
closeEditableCell();
});
useHotkeys(
useScopedHotkeys(
'enter',
() => {
closeEditableCell();
moveDown();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'esc',
() => {
closeEditableCell();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'tab',
() => {
closeEditableCell();
moveRight();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);
useHotkeys(
useScopedHotkeys(
'shift+tab',
() => {
closeEditableCell();
moveLeft();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);

View File

@ -1,8 +1,9 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
import { useEditableCell } from './hooks/useCloseEditableCell';
@ -10,26 +11,26 @@ import { EditableCellDisplayMode } from './EditableCellDisplayMode';
export function EditableCellSoftFocusMode({
children,
}: React.PropsWithChildren<unknown>) {
editHotkeysScope,
}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScopeStackItem }>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
const [captureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
useHotkeys(
useScopedHotkeys(
'enter',
() => {
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.TableSoftFocus,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'*',
(keyboardEvent) => {
const isWritingText =
@ -41,14 +42,16 @@ export function EditableCellSoftFocusMode({
return;
}
if (captureHotkeyTypeInFocus) {
return;
}
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
InternalHotkeysScope.TableSoftFocus,
[openEditableCell, addToHotkeysScopeStack, editHotkeysScope],
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: false,
},
);

View File

@ -1,24 +1,24 @@
import { useRecoilCallback } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useRemoveHighestHotkeysScopeStackItem } from '@/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem';
import { useCloseCurrentCellInEditMode } from '@/ui/tables/hooks/useClearCellInEditMode';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
import { isEditModeScopedState } from '../states/isEditModeScopedState';
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
export function useEditableCell() {
const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
const closeEditableCell = useRecoilCallback(
({ set }) =>
async () => {
setIsEditMode(false);
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
await new Promise((resolve) => setTimeout(resolve, 20));
const removeHighestHotkeysScopedStackItem =
useRemoveHighestHotkeysScopeStackItem();
set(isSomeInputInEditModeState, false);
},
[setIsEditMode],
);
function closeEditableCell() {
closeCurrentCellInEditMode();
removeHighestHotkeysScopedStackItem();
}
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
@ -29,11 +29,12 @@ export function useEditableCell() {
if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true);
set(isSoftFocusActiveState, false);
setIsEditMode(true);
setCurrentCellInEditMode();
}
},
[setIsEditMode],
[setCurrentCellInEditMode],
);
return {

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { useMoveEditModeToCellPosition } from '@/ui/tables/hooks/useMoveEditModeToCellPosition';
import { isCellInEditModeFamilyState } from '@/ui/tables/states/isCellInEditModeFamilyState';
import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useCurrentCellEditMode() {
const moveEditModeToCellPosition = useMoveEditModeToCellPosition();
const currentCellPosition = useCurrentCellPosition();
const [isCurrentCellInEditMode] = useRecoilState(
isCellInEditModeFamilyState(currentCellPosition),
);
const setCurrentCellInEditMode = useCallback(() => {
moveEditModeToCellPosition(currentCellPosition);
}, [currentCellPosition, moveEditModeToCellPosition]);
return { isCurrentCellInEditMode, setCurrentCellInEditMode };
}

View File

@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useCurrentCellPosition() {
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentCellPosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
return currentCellPosition;
}

View File

@ -1,35 +1,14 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { isSoftFocusOnCellFamilyState } from '@/ui/tables/states/isSoftFocusOnCellFamilyState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { TablePosition } from '@/ui/tables/types/TablePosition';
import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useIsSoftFocusOnCurrentCell() {
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
const currentCellPosition = useCurrentCellPosition();
const isSoftFocusOnCell = useRecoilValue(
isSoftFocusOnCellFamilyState(currentTablePosition),
isSoftFocusOnCellFamilyState(currentCellPosition),
);
return isSoftFocusOnCell;

View File

@ -1,14 +1,18 @@
import { useCallback, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useSetSoftFocusPosition } from '@/ui/tables/hooks/useSetSoftFocusPosition';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { TablePosition } from '@/ui/tables/types/TablePosition';
import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useSetSoftFocusOnCurrentCell() {
export function useSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
@ -20,7 +24,7 @@ export function useSetSoftFocusOnCurrentCell() {
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
const currentTablePosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
@ -28,7 +32,18 @@ export function useSetSoftFocusOnCurrentCell() {
[currentColumnNumber, currentRowNumber],
);
const [, setIsSoftFocusActive] = useRecoilState(isSoftFocusActiveState);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
return useCallback(() => {
setSoftFocusPosition(currentTablePosition);
}, [setSoftFocusPosition, currentTablePosition]);
setIsSoftFocusActive(true);
addToHotkeysScopeStack({ scope: InternalHotkeysScope.TableSoftFocus });
}, [
setSoftFocusPosition,
currentTablePosition,
setIsSoftFocusActive,
addToHotkeysScopeStack,
]);
}

View File

@ -1,6 +0,0 @@
import { atomFamily } from 'recoil';
export const isEditModeScopedState = atomFamily<boolean, string>({
key: 'isEditModeScopedState',
default: false,
});

View File

@ -1,10 +1,11 @@
import { ChangeEvent, ReactElement, useRef } from 'react';
import styled from '@emotion/styled';
import { ReactElement } from 'react';
import { textInputStyle } from '@/ui/themes/effects';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { EditableCell } from '../EditableCell';
import { EditableDoubleTextEditMode } from './EditableDoubleTextEditMode';
type OwnProps = {
firstValue: string;
secondValue: string;
@ -14,57 +15,25 @@ type OwnProps = {
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
& > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
}
`;
const StyledEditInplaceInput = styled.input`
height: 18px;
margin: 0;
width: 45%;
${textInputStyle}
`;
export function EditableDoubleText({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
nonEditModeContent,
onChange,
nonEditModeContent,
}: OwnProps) {
const firstValueInputRef = useRef<HTMLInputElement>(null);
return (
<EditableCell
editHotkeysScope={{ scope: InternalHotkeysScope.CellDoubleTextInput }}
editModeContent={
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
placeholder={secondValuePlaceholder}
ref={firstValueInputRef}
value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
<EditableDoubleTextEditMode
firstValue={firstValue}
secondValue={secondValue}
firstValuePlaceholder={firstValuePlaceholder}
secondValuePlaceholder={secondValuePlaceholder}
onChange={onChange}
/>
}
nonEditModeContent={nonEditModeContent}
></EditableCell>

View File

@ -0,0 +1,129 @@
import { ChangeEvent, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { textInputStyle } from '@/ui/themes/effects';
import { useEditableCell } from '../hooks/useCloseEditableCell';
type OwnProps = {
firstValue: string;
secondValue: string;
firstValuePlaceholder: string;
secondValuePlaceholder: string;
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
& > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
}
`;
const StyledEditInplaceInput = styled.input`
height: 18px;
margin: 0;
width: 45%;
${textInputStyle}
`;
export function EditableDoubleTextEditMode({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
onChange,
}: OwnProps) {
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
const firstValueInputRef = useRef<HTMLInputElement>(null);
const secondValueInputRef = useRef<HTMLInputElement>(null);
const { closeEditableCell } = useEditableCell();
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
function closeCell() {
setFocusPosition('left');
closeEditableCell();
}
useScopedHotkeys(
Key.Enter,
() => {
closeCell();
moveDown();
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell],
);
useScopedHotkeys(
Key.Escape,
() => {
closeCell();
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell],
);
useScopedHotkeys(
'tab',
async (keyboardEvent, hotkeyEvent) => {
if (focusPosition === 'left') {
setFocusPosition('right');
secondValueInputRef.current?.focus();
} else {
closeCell();
moveRight();
}
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell, moveRight, focusPosition],
);
useScopedHotkeys(
'shift+tab',
() => {
if (focusPosition === 'right') {
setFocusPosition('left');
firstValueInputRef.current?.focus();
} else {
closeCell();
moveLeft();
}
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell, moveRight, focusPosition],
);
return (
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
placeholder={secondValuePlaceholder}
ref={secondValueInputRef}
value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
);
}

View File

@ -13,6 +13,8 @@ import {
SortType,
} from '@/filters-and-sorts/interfaces/sorts/interface';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useLeaveTableFocus } from '@/ui/tables/hooks/useLeaveTableFocus';
import { RowContext } from '@/ui/tables/states/RowContext';
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
@ -103,6 +105,8 @@ export function EntityTable<TData extends { id: string }, SortField>({
availableSorts,
onSortsUpdate,
}: OwnProps<TData, SortField>) {
const tableBodyRef = React.useRef<HTMLDivElement>(null);
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
currentRowSelectionState,
);
@ -119,6 +123,12 @@ export function EntityTable<TData extends { id: string }, SortField>({
getRowId: (row) => row.id,
});
const leaveTableFocus = useLeaveTableFocus();
useListenClickOutsideArrayOfRef([tableBodyRef], () => {
leaveTableFocus();
});
return (
<StyledTableWithHeader>
<TableHeader
@ -127,7 +137,7 @@ export function EntityTable<TData extends { id: string }, SortField>({
availableSorts={availableSorts}
onSortsUpdate={onSortsUpdate}
/>
<StyledTableScrollableContainer>
<StyledTableScrollableContainer ref={tableBodyRef}>
<StyledTable>
<thead>
{table.getHeaderGroups().map((headerGroup) => (

View File

@ -1,8 +1,9 @@
import { ReactNode, useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { IconChevronDown } from '@/ui/icons/index';
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
@ -13,7 +14,7 @@ type OwnProps = {
isActive: boolean;
children?: ReactNode;
isUnfolded?: boolean;
setIsUnfolded?: React.Dispatch<React.SetStateAction<boolean>>;
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
resetState?: () => void;
};
@ -158,22 +159,23 @@ function DropdownButton({
isActive,
children,
isUnfolded = false,
setIsUnfolded,
resetState,
onIsUnfoldedChange,
}: OwnProps) {
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
useScopedHotkeys(
[Key.Enter, Key.Escape],
() => {
onIsUnfoldedChange?.(false);
},
InternalHotkeysScope.TableHeaderDropdownButton,
[onIsUnfoldedChange],
);
const onButtonClick = () => {
setIsUnfolded && setIsUnfolded(!isUnfolded);
setCaptureHotkeyTypeInFocus((isPreviousUnfolded) => !isPreviousUnfolded);
onIsUnfoldedChange?.(!isUnfolded);
};
const onOutsideClick = () => {
setCaptureHotkeyTypeInFocus(false);
setIsUnfolded && setIsUnfolded(false);
resetState && resetState();
onIsUnfoldedChange?.(false);
};
const dropdownRef = useRef(null);

View File

@ -5,6 +5,8 @@ import { filterDropdownSearchInputScopedState } from '@/filters-and-sorts/states
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { tableFilterDefinitionUsedInDropdownScopedState } from '@/filters-and-sorts/states/tableFilterDefinitionUsedInDropdownScopedState';
import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
@ -21,6 +23,11 @@ import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
export function FilterDropdownButton() {
const [isUnfolded, setIsUnfolded] = useState(false);
useHotkeysScopeOnBooleanState(
{ scope: InternalHotkeysScope.TableHeaderDropdownButton },
isUnfolded,
);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
@ -64,13 +71,21 @@ export function FilterDropdownButton() {
const isFilterSelected = (activeTableFilters?.length ?? 0) > 0;
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setIsUnfolded(true);
} else {
setIsUnfolded(false);
resetState();
}
}
return (
<DropdownButton
label="Filter"
isActive={isFilterSelected}
isUnfolded={isUnfolded}
setIsUnfolded={setIsUnfolded}
resetState={resetState}
onIsUnfoldedChange={handleIsUnfoldedChange}
>
{!tableFilterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect />

View File

@ -1,11 +1,9 @@
import { useCallback, useState } from 'react';
import { useRecoilState } from 'recoil';
import {
SelectedSortType,
SortType,
} from '@/filters-and-sorts/interfaces/sorts/interface';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import DropdownButton from './DropdownButton';
@ -23,9 +21,6 @@ export function SortDropdownButton<SortField>({
onSortSelect,
}: OwnProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
@ -41,17 +36,24 @@ export function SortDropdownButton<SortField>({
const resetState = useCallback(() => {
setIsOptionUnfolded(false);
setCaptureHotkeyTypeInFocus(false);
setSelectedSortDirection('asc');
}, [setCaptureHotkeyTypeInFocus]);
}, []);
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setIsUnfolded(true);
} else {
setIsUnfolded(false);
resetState();
}
}
return (
<DropdownButton
label="Sort"
isActive={isSortSelected}
isUnfolded={isUnfolded}
setIsUnfolded={setIsUnfolded}
resetState={resetState}
onIsUnfoldedChange={handleIsUnfoldedChange}
>
{isOptionUnfolded
? options.map((option, index) => (
@ -59,7 +61,6 @@ export function SortDropdownButton<SortField>({
key={index}
onClick={() => {
setSelectedSortDirection(option);
setCaptureHotkeyTypeInFocus(false);
setIsOptionUnfolded(false);
}}
>
@ -80,7 +81,6 @@ export function SortDropdownButton<SortField>({
key={index + 1}
onClick={() => {
setIsUnfolded(false);
setCaptureHotkeyTypeInFocus(false);
onSortItemSelect(sort);
}}
>

View File

@ -1,8 +1,8 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isDefined } from '@/utils/type-guards/isDefined';
import { Panel } from '../../Panel';
@ -18,14 +18,14 @@ const StyledRightDrawer = styled.div`
`;
export function RightDrawer() {
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
useEffect(() => {
setCaptureHotkeyTypeInFocus(isRightDrawerOpen);
}, [isRightDrawerOpen, setCaptureHotkeyTypeInFocus]);
useHotkeysScopeOnBooleanState(
{ scope: InternalHotkeysScope.RightDrawer },
isRightDrawerOpen,
);
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
return <></>;
}

View File

@ -1,7 +1,8 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { IconPlus } from '@/ui/icons/index';
import NavCollapseButton from '../navbar/NavCollapseButton';
@ -50,7 +51,7 @@ type OwnProps = {
};
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
useDirectHotkeys('c', () => onAddButtonClick && onAddButtonClick());
useScopedHotkeys('c', () => onAddButtonClick?.(), InternalHotkeysScope.Table);
return (
<>

View File

@ -0,0 +1,21 @@
import { useRecoilCallback } from 'recoil';
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
export function useCloseCurrentCellInEditMode() {
return useRecoilCallback(({ set, snapshot }) => {
return async () => {
const currentCellInEditModePosition = await snapshot.getPromise(
currentCellInEditModePositionState,
);
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
await new Promise((resolve) => setTimeout(resolve, 20));
set(isSomeInputInEditModeState, false);
};
}, []);
}

View File

@ -0,0 +1,19 @@
import { useRecoilCallback } from 'recoil';
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
export function useDisableSoftFocus() {
return useRecoilCallback(({ set, snapshot }) => {
return () => {
const currentPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
set(isSoftFocusActiveState, false);
set(isSoftFocusOnCellFamilyState(currentPosition), false);
};
}, []);
}

View File

@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { useResetTableRowSelection } from './useResetTableRowSelection';
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
export type TableDimensions = {
numberOfRows: number;
@ -30,13 +28,4 @@ export function useInitializeEntityTable({
numberOfRows,
});
}, [numberOfRows, numberOfColumns, setTableDimensions]);
const setSoftFocusPosition = useSetSoftFocusPosition();
useEffect(() => {
setSoftFocusPosition({
row: 0,
column: TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN,
});
}, [setSoftFocusPosition]);
}

View File

@ -0,0 +1,24 @@
import { useCurrentHotkeysScope } from '@/hotkeys/hooks/useCurrentHotkeysScope';
import { useResetHotkeysScopeStack } from '@/hotkeys/hooks/useResetHotkeysScopeStack';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
import { useDisableSoftFocus } from './useDisableSoftFocus';
export function useLeaveTableFocus() {
const resetHotkeysScopeStack = useResetHotkeysScopeStack();
const currentHotkeysScope = useCurrentHotkeysScope();
const disableSoftFocus = useDisableSoftFocus();
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
return async function leaveTableFocus() {
if (currentHotkeysScope?.scope === InternalHotkeysScope.Table) {
return;
}
closeCurrentCellInEditMode();
disableSoftFocus();
resetHotkeysScopeStack(InternalHotkeysScope.Table);
};
}

View File

@ -1,72 +1,74 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useRemoveFromHotkeysScopeStack } from '@/hotkeys/hooks/useRemoveFromHotkeysScopeStack';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
import { useDisableSoftFocus } from './useDisableSoftFocus';
import { useMoveSoftFocus } from './useMoveSoftFocus';
export function useMapKeyboardToSoftFocus() {
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
const removeFromHotkeysScopedStack = useRemoveFromHotkeysScopeStack();
const disableSoftFocus = useDisableSoftFocus();
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
useHotkeys(
'up, shift+enter',
useScopedHotkeys(
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
() => {
if (!isSomeInputInEditMode) {
moveUp();
}
},
InternalHotkeysScope.TableSoftFocus,
[moveUp, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
'down',
useScopedHotkeys(
Key.ArrowDown,
() => {
if (!isSomeInputInEditMode) {
moveDown();
}
},
InternalHotkeysScope.TableSoftFocus,
[moveDown, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
['left', 'shift+tab'],
useScopedHotkeys(
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
() => {
if (!isSomeInputInEditMode) {
moveLeft();
}
},
InternalHotkeysScope.TableSoftFocus,
[moveLeft, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
['right', 'tab'],
useScopedHotkeys(
[Key.ArrowRight, Key.Tab],
() => {
if (!isSomeInputInEditMode) {
moveRight();
}
},
InternalHotkeysScope.TableSoftFocus,
[moveRight, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
);
useScopedHotkeys(
[Key.Escape],
() => {
removeFromHotkeysScopedStack(InternalHotkeysScope.TableSoftFocus);
disableSoftFocus();
},
InternalHotkeysScope.TableSoftFocus,
[removeFromHotkeysScopedStack, disableSoftFocus],
);
}

View File

@ -0,0 +1,21 @@
import { useRecoilCallback } from 'recoil';
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
import { CellPosition } from '../types/CellPosition';
export function useMoveEditModeToCellPosition() {
return useRecoilCallback(({ set, snapshot }) => {
return (newPosition: CellPosition) => {
const currentCellInEditModePosition = snapshot
.getLoadable(currentCellInEditModePositionState)
.valueOrThrow();
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
set(currentCellInEditModePositionState, newPosition);
set(isCellInEditModeFamilyState(newPosition), true);
};
}, []);
}

View File

@ -1,16 +1,19 @@
import { useRecoilCallback } from 'recoil';
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
import { TablePosition } from '../types/TablePosition';
import { CellPosition } from '../types/CellPosition';
export function useSetSoftFocusPosition() {
return useRecoilCallback(({ set, snapshot }) => {
return (newPosition: TablePosition) => {
return (newPosition: CellPosition) => {
const currentPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
set(isSoftFocusActiveState, true);
set(isSoftFocusOnCellFamilyState(currentPosition), false);
set(softFocusPositionState, newPosition);

View File

@ -0,0 +1,11 @@
import { atom } from 'recoil';
import { CellPosition } from '../types/CellPosition';
export const currentCellInEditModePositionState = atom<CellPosition>({
key: 'currentCellInEditModePositionState',
default: {
row: 0,
column: 1,
},
});

View File

@ -0,0 +1,8 @@
import { atomFamily } from 'recoil';
import { CellPosition } from '../types/CellPosition';
export const isCellInEditModeFamilyState = atomFamily<boolean, CellPosition>({
key: 'isCellInEditModeFamilyState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isSoftFocusActiveState = atom<boolean>({
key: 'isSoftFocusActiveState',
default: false,
});

View File

@ -1,8 +1,8 @@
import { atomFamily } from 'recoil';
import { TablePosition } from '../types/TablePosition';
import { CellPosition } from '../types/CellPosition';
export const isSoftFocusOnCellFamilyState = atomFamily<boolean, TablePosition>({
export const isSoftFocusOnCellFamilyState = atomFamily<boolean, CellPosition>({
key: 'isSoftFocusOnCellFamilyState',
default: false,
});

View File

@ -1,8 +1,8 @@
import { atom } from 'recoil';
import { TablePosition } from '../types/TablePosition';
import { CellPosition } from '../types/CellPosition';
export const softFocusPositionState = atom<TablePosition>({
export const softFocusPositionState = atom<CellPosition>({
key: 'softFocusPositionState',
default: {
row: 0,

View File

@ -1,4 +1,4 @@
export type TablePosition = {
export type CellPosition = {
row: number;
column: number;
};

View File

@ -1,6 +1,6 @@
import { TablePosition } from '../TablePosition';
import { CellPosition } from '../CellPosition';
export function isTablePosition(value: any): value is TablePosition {
export function isTablePosition(value: any): value is CellPosition {
return (
value && typeof value.row === 'number' && typeof value.column === 'number'
);

View File

@ -1,4 +1,4 @@
export interface PositionType {
export type PositionType = {
x: number | null;
y: number | null;
}
};

View File

@ -1,16 +1,17 @@
import { useCallback, useEffect, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { SubTitle } from '@/auth/components/ui/SubTitle';
import { Title } from '@/auth/components/ui/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
@ -48,9 +49,6 @@ export function CreateProfile() {
const onboardingStatus = useOnboardingStatus();
const [currentUser] = useRecoilState(currentUserState);
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const [updateUser] = useUpdateUserMutation();
@ -85,29 +83,18 @@ export function CreateProfile() {
throw errors;
}
setCaptureHotkeyTypeInFocus(false);
navigate('/');
} catch (error) {
console.error(error);
}
}, [
currentUser?.id,
firstName,
lastName,
navigate,
setCaptureHotkeyTypeInFocus,
updateUser,
]);
}, [currentUser?.id, firstName, lastName, navigate, updateUser]);
useHotkeys(
'enter',
useScopedHotkeys(
Key.Enter,
() => {
handleCreate();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.Modal,
[handleCreate],
);
@ -117,10 +104,6 @@ export function CreateProfile() {
}
}, [onboardingStatus, navigate]);
useEffect(() => {
setCaptureHotkeyTypeInFocus(true);
}, [setCaptureHotkeyTypeInFocus]);
return (
<>
<Title>Create profile</Title>

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
@ -8,6 +7,8 @@ import { SubTitle } from '@/auth/components/ui/SubTitle';
import { Title } from '@/auth/components/ui/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
@ -68,15 +69,12 @@ export function CreateWorkspace() {
}
}, [navigate, updateWorkspace, workspaceName]);
useHotkeys(
useScopedHotkeys(
'enter',
() => {
handleCreate();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.Modal,
[handleCreate],
);

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@ -13,7 +12,8 @@ import { Title } from '@/auth/components/ui/Title';
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { AnimatedEaseIn } from '@/ui/components/motion/AnimatedEaseIn';
@ -31,9 +31,6 @@ const StyledFooterNote = styled(FooterNote)`
`;
export function Index() {
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const navigate = useNavigate();
const theme = useTheme();
const [, setMockMode] = useRecoilState(isMockModeState);
@ -59,29 +56,19 @@ export function Index() {
navigate('/auth/password-login');
}, [navigate, visible]);
useHotkeys(
useScopedHotkeys(
'enter',
() => {
onPasswordLoginClick();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.Modal,
[onPasswordLoginClick],
);
useEffect(() => {
setMockMode(true);
setCaptureHotkeyTypeInFocus(true);
setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : '');
}, [
navigate,
setMockMode,
setCaptureHotkeyTypeInFocus,
setAuthFlowUserEmail,
demoMode,
]);
}, [navigate, setMockMode, setAuthFlowUserEmail, demoMode]);
return (
<>

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
@ -12,6 +11,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
@ -74,15 +75,12 @@ export function PasswordLogin() {
}
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
useHotkeys(
useScopedHotkeys(
'enter',
() => {
handleLogin();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
InternalHotkeysScope.Modal,
[handleLogin],
);

View File

@ -1,9 +1,13 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { GET_COMPANIES } from '@/companies/services';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
import { IconBuildingSkyscraper } from '@/ui/icons/index';
@ -24,6 +28,18 @@ const StyledTableContainer = styled.div`
`;
export function Companies() {
const [isMockMode] = useRecoilState(isMockModeState);
const hotkeysEnabled = !isMockMode;
useHotkeysScopeOnMountOnly(
{
scope: InternalHotkeysScope.Table,
customScopes: { 'command-menu': true, goto: true },
},
hotkeysEnabled,
);
const [insertCompany] = useInsertCompanyMutation();
async function handleAddButtonClick() {
@ -45,20 +61,22 @@ export function Companies() {
const theme = useTheme();
return (
<WithTopBarContainer
title="Companies"
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
onAddButtonClick={handleAddButtonClick}
>
<RecoilScope SpecificContext={TableContext}>
<StyledTableContainer>
<CompanyTable />
</StyledTableContainer>
<EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadCompany />
<TableActionBarButtonDeleteCompanies />
</EntityTableActionBar>
</RecoilScope>
</WithTopBarContainer>
<>
<WithTopBarContainer
title="Companies"
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
onAddButtonClick={handleAddButtonClick}
>
<RecoilScope SpecificContext={TableContext}>
<StyledTableContainer>
<CompanyTable />
</StyledTableContainer>
<EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadCompany />
<TableActionBarButtonDeleteCompanies />
</EntityTableActionBar>
</RecoilScope>
</WithTopBarContainer>
</>
);
}

View File

@ -3,6 +3,8 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { v4 as uuidv4 } from 'uuid';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { GET_PEOPLE } from '@/people/services';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
@ -22,6 +24,11 @@ const StyledPeopleContainer = styled.div`
`;
export function People() {
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.Table,
customScopes: { 'command-menu': true, goto: true },
});
const [insertPersonMutation] = useInsertPersonMutation();
async function handleAddButtonClick() {

View File

@ -0,0 +1,7 @@
import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
export function AnalyticsHook() {
useTrackPageView();
return <></>;
}

View File

@ -0,0 +1,10 @@
import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
export function GotoHotkeysHooks() {
useGoToHotkeys('p', '/people');
useGoToHotkeys('c', '/companies');
useGoToHotkeys('o', '/opportunities');
useGoToHotkeys('s', '/settings/profile');
return <></>;
}

View File

@ -0,0 +1,7 @@
import { useHotkeysScopeStackAutoSync } from '@/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync';
export function HotkeysScopeStackAutoSyncHook() {
useHotkeysScopeStackAutoSync();
return <></>;
}

View File

@ -16413,6 +16413,11 @@ ts-jest@^29.1.0:
semver "^7.5.3"
yargs-parser "^21.0.1"
ts-key-enum@^2.0.12:
version "2.0.12"
resolved "https://registry.yarnpkg.com/ts-key-enum/-/ts-key-enum-2.0.12.tgz#4f7f35eb041fa5847f8f9ed8c38beaaa047a33ba"
integrity sha512-Ety4IvKMaeG34AyXMp5r11XiVZNDRL+XWxXbVVJjLvq2vxKRttEANBE7Za1bxCAZRdH2/sZT6jFyyTWxXz28hw==
ts-log@^2.2.3:
version "2.2.5"
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623"