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:
@ -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"
|
||||
},
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
13
front/src/AppInternalHooks.tsx
Normal file
13
front/src/AppInternalHooks.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
17
front/src/modules/hotkeys/constants/index.ts
Normal file
17
front/src/modules/hotkeys/constants/index.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
103
front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
Normal file
103
front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
48
front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
Normal file
48
front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
Normal 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 });
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
16
front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
Normal file
16
front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
Normal 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]);
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -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();
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
18
front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
Normal file
18
front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
@ -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],
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const captureHotkeyTypeInFocusState = atom<boolean>({
|
||||
key: 'captureHotkeyTypeInFocusState',
|
||||
default: false,
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
@ -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],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const internalHotkeysEnabledScopesState = atom<string[]>({
|
||||
key: 'internalHotkeysEnabledScopesState',
|
||||
default: [],
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const pendingHotkeyState = atom<string | null>({
|
||||
key: 'pendingHotkeyState',
|
||||
default: null,
|
||||
});
|
||||
3
front/src/modules/hotkeys/types/HotkeysScope.ts
Normal file
3
front/src/modules/hotkeys/types/HotkeysScope.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum HotkeysScope {
|
||||
CompanyPage = 'company-page',
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { CustomHotkeysScopes } from '@/hotkeys/states/internal/customHotkeysScopesState';
|
||||
|
||||
export type HotkeysScopeStackItem = {
|
||||
scope: string;
|
||||
customScopes?: CustomHotkeysScopes;
|
||||
ancestorScope?: string | null;
|
||||
};
|
||||
@ -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',
|
||||
}
|
||||
@ -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} />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
) : (
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isEditModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isEditModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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) => (
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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 <></>;
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
21
front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts
Normal file
21
front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts
Normal 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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
19
front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts
Normal file
19
front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts
Normal 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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
24
front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts
Normal file
24
front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export const isCellInEditModeFamilyState = atomFamily<boolean, CellPosition>({
|
||||
key: 'isCellInEditModeFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isSoftFocusActiveState = atom<boolean>({
|
||||
key: 'isSoftFocusActiveState',
|
||||
default: false,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export type TablePosition = {
|
||||
export type CellPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
@ -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'
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export interface PositionType {
|
||||
export type PositionType = {
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
}
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
7
front/src/sync-hooks/AnalyticsHook.tsx
Normal file
7
front/src/sync-hooks/AnalyticsHook.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
|
||||
|
||||
export function AnalyticsHook() {
|
||||
useTrackPageView();
|
||||
|
||||
return <></>;
|
||||
}
|
||||
10
front/src/sync-hooks/GotoHotkeysHooks.tsx
Normal file
10
front/src/sync-hooks/GotoHotkeysHooks.tsx
Normal 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 <></>;
|
||||
}
|
||||
7
front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx
Normal file
7
front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { useHotkeysScopeStackAutoSync } from '@/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync';
|
||||
|
||||
export function HotkeysScopeStackAutoSyncHook() {
|
||||
useHotkeysScopeStackAutoSync();
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user