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",
|
"react-tooltip": "^5.13.1",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"scroll-into-view": "^1.16.2",
|
"scroll-into-view": "^1.16.2",
|
||||||
|
"ts-key-enum": "^2.0.12",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||||
|
|
||||||
import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
|
|
||||||
import { RequireOnboarded } from '@/auth/components/RequireOnboarded';
|
import { RequireOnboarded } from '@/auth/components/RequireOnboarded';
|
||||||
import { RequireOnboarding } from '@/auth/components/RequireOnboarding';
|
import { RequireOnboarding } from '@/auth/components/RequireOnboarding';
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
import { AuthModal } from '@/auth/components/ui/Modal';
|
||||||
import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
|
|
||||||
import { AuthLayout } from '@/ui/layout/AuthLayout';
|
import { AuthLayout } from '@/ui/layout/AuthLayout';
|
||||||
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||||
@ -18,6 +16,8 @@ import { Opportunities } from '~/pages/opportunities/Opportunities';
|
|||||||
import { People } from '~/pages/people/People';
|
import { People } from '~/pages/people/People';
|
||||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||||
|
|
||||||
|
import { AppInternalHooks } from './AppInternalHooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthRoutes is used to allow transitions between auth pages with framer-motion.
|
* AuthRoutes is used to allow transitions between auth pages with framer-motion.
|
||||||
*/
|
*/
|
||||||
@ -42,48 +42,44 @@ function AuthRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useGoToHotkeys('p', '/people');
|
|
||||||
useGoToHotkeys('c', '/companies');
|
|
||||||
useGoToHotkeys('o', '/opportunities');
|
|
||||||
useGoToHotkeys('s', '/settings/profile');
|
|
||||||
|
|
||||||
useTrackPageView();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<>
|
||||||
<Routes>
|
<AppInternalHooks />
|
||||||
<Route
|
<DefaultLayout>
|
||||||
path="auth/*"
|
<Routes>
|
||||||
element={
|
<Route
|
||||||
<RequireOnboarding>
|
path="auth/*"
|
||||||
<AuthLayout>
|
element={
|
||||||
<AuthRoutes />
|
<RequireOnboarding>
|
||||||
</AuthLayout>
|
<AuthLayout>
|
||||||
</RequireOnboarding>
|
<AuthRoutes />
|
||||||
}
|
</AuthLayout>
|
||||||
/>
|
</RequireOnboarding>
|
||||||
<Route
|
}
|
||||||
path="*"
|
/>
|
||||||
element={
|
<Route
|
||||||
<RequireOnboarded>
|
path="*"
|
||||||
<Routes>
|
element={
|
||||||
<Route path="" element={<Navigate to="/people" replace />} />
|
<RequireOnboarded>
|
||||||
<Route path="people" element={<People />} />
|
<Routes>
|
||||||
<Route path="companies" element={<Companies />} />
|
<Route path="" element={<Navigate to="/people" replace />} />
|
||||||
<Route path="opportunities" element={<Opportunities />} />
|
<Route path="people" element={<People />} />
|
||||||
<Route
|
<Route path="companies" element={<Companies />} />
|
||||||
path="settings/*"
|
<Route path="opportunities" element={<Opportunities />} />
|
||||||
element={
|
<Route
|
||||||
<Routes>
|
path="settings/*"
|
||||||
<Route path="profile" element={<SettingsProfile />} />
|
element={
|
||||||
</Routes>
|
<Routes>
|
||||||
}
|
<Route path="profile" element={<SettingsProfile />} />
|
||||||
/>
|
</Routes>
|
||||||
</Routes>
|
}
|
||||||
</RequireOnboarded>
|
/>
|
||||||
}
|
</Routes>
|
||||||
/>
|
</RequireOnboarded>
|
||||||
</Routes>
|
}
|
||||||
</DefaultLayout>
|
/>
|
||||||
|
</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 ReactDOM from 'react-dom/client';
|
||||||
|
import { HotkeysProvider } from 'react-hotkeys-hook';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { INITIAL_HOTKEYS_SCOPES } from '@/hotkeys/constants';
|
||||||
import { ThemeType } from '@/ui/themes/themes';
|
import { ThemeType } from '@/ui/themes/themes';
|
||||||
|
|
||||||
import '@emotion/react';
|
import '@emotion/react';
|
||||||
@ -27,9 +29,11 @@ root.render(
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ClientConfigProvider>
|
<ClientConfigProvider>
|
||||||
<BrowserRouter>
|
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</HotkeysProvider>
|
||||||
</ClientConfigProvider>
|
</ClientConfigProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@ -2,9 +2,6 @@ import { useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { keyframes } from '@emotion/react';
|
import { keyframes } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
|
|
||||||
|
|
||||||
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
||||||
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
||||||
@ -38,9 +35,6 @@ export function RequireOnboarded({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,12 +47,6 @@ export function RequireOnboarded({
|
|||||||
}
|
}
|
||||||
}, [onboardingStatus, navigate]);
|
}, [onboardingStatus, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onboardingStatus === OnboardingStatus.Completed) {
|
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
|
||||||
}
|
|
||||||
}, [setCaptureHotkeyTypeInFocus, onboardingStatus]);
|
|
||||||
|
|
||||||
if (onboardingStatus !== OnboardingStatus.Completed) {
|
if (onboardingStatus !== OnboardingStatus.Completed) {
|
||||||
return (
|
return (
|
||||||
<EmptyContainer>
|
<EmptyContainer>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
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';
|
import { Modal as UIModal } from '@/ui/components/modal/Modal';
|
||||||
|
|
||||||
type Props = React.ComponentProps<'div'>;
|
type Props = React.ComponentProps<'div'>;
|
||||||
@ -17,6 +19,11 @@ const StyledContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function AuthModal({ children, ...restProps }: Props) {
|
export function AuthModal({ children, ...restProps }: Props) {
|
||||||
|
useHotkeysScopeOnMountOnly({
|
||||||
|
scope: InternalHotkeysScope.Modal,
|
||||||
|
customScopes: { 'command-menu': false, goto: false },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UIModal isOpen={true}>
|
<UIModal isOpen={true}>
|
||||||
<StyledContainer {...restProps}>{children}</StyledContainer>
|
<StyledContainer {...restProps}>{children}</StyledContainer>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
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';
|
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpened';
|
||||||
|
|
||||||
@ -18,14 +19,20 @@ import {
|
|||||||
export function CommandMenu() {
|
export function CommandMenu() {
|
||||||
const [open, setOpen] = useRecoilState(isCommandMenuOpenedState);
|
const [open, setOpen] = useRecoilState(isCommandMenuOpenedState);
|
||||||
|
|
||||||
useDirectHotkeys(
|
useScopedHotkeys(
|
||||||
'ctrl+k,meta+k',
|
'ctrl+k,meta+k',
|
||||||
() => {
|
() => {
|
||||||
setOpen((prevOpen) => !prevOpen);
|
setOpen((prevOpen) => !prevOpen);
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.CommandMenu,
|
||||||
[setOpen],
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeysScopeOnBooleanState(
|
||||||
|
{ scope: InternalHotkeysScope.CommandMenu },
|
||||||
|
open,
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Allow performing actions on page through CommandBar
|
TODO: Allow performing actions on page through CommandBar
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
@ -13,7 +12,11 @@ import { IconArrowUpRight } from '@tabler/icons-react';
|
|||||||
|
|
||||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||||
import CompanyChip from '@/companies/components/CompanyChip';
|
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 { PersonChip } from '@/people/components/PersonChip';
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
|
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
|
||||||
@ -95,6 +98,11 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
useHotkeysScopeOnBooleanState(
|
||||||
|
{ scope: InternalHotkeysScope.RelationPicker },
|
||||||
|
isMenuOpen,
|
||||||
|
);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const peopleIds =
|
const peopleIds =
|
||||||
@ -156,15 +164,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
|||||||
setSearchFilter('');
|
setSearchFilter('');
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
['esc', 'enter'],
|
['esc', 'enter'],
|
||||||
() => {
|
() => {
|
||||||
exitEditMode();
|
exitEditMode();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.RelationPicker,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[exitEditMode],
|
[exitEditMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -225,19 +230,21 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
|||||||
)}
|
)}
|
||||||
</StyledRelationContainer>
|
</StyledRelationContainer>
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<StyledMenuWrapper ref={refs.setFloating} style={floatingStyles}>
|
<RecoilScope>
|
||||||
<MultipleEntitySelect
|
<StyledMenuWrapper ref={refs.setFloating} style={floatingStyles}>
|
||||||
entities={{
|
<MultipleEntitySelect
|
||||||
entitiesToSelect,
|
entities={{
|
||||||
filteredSelectedEntities,
|
entitiesToSelect,
|
||||||
selectedEntities,
|
filteredSelectedEntities,
|
||||||
loading: false, // TODO implement skeleton loading
|
selectedEntities,
|
||||||
}}
|
loading: false, // TODO implement skeleton loading
|
||||||
onItemCheckChange={handleCheckItemChange}
|
}}
|
||||||
onSearchFilterChange={handleFilterChange}
|
onItemCheckChange={handleCheckItemChange}
|
||||||
searchFilter={searchFilter}
|
onSearchFilterChange={handleFilterChange}
|
||||||
/>
|
searchFilter={searchFilter}
|
||||||
</StyledMenuWrapper>
|
/>
|
||||||
|
</StyledMenuWrapper>
|
||||||
|
</RecoilScope>
|
||||||
)}
|
)}
|
||||||
</StyledContainer>
|
</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 { 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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useSequenceHotkeys('g', key, () => {
|
useSequenceHotkeys(
|
||||||
navigate(location);
|
'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 {
|
import {
|
||||||
Hotkey,
|
Hotkey,
|
||||||
HotkeyCallback,
|
HotkeyCallback,
|
||||||
|
Keys,
|
||||||
|
Options,
|
||||||
OptionsOrDependencyArray,
|
OptionsOrDependencyArray,
|
||||||
} from 'react-hotkeys-hook/dist/types';
|
} from 'react-hotkeys-hook/dist/types';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { pendingHotkeyState } from '../states/pendingHotkeysState';
|
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||||
|
|
||||||
export function useDirectHotkeys(
|
export function useScopedHotkeys(
|
||||||
keys: string,
|
keys: Keys,
|
||||||
callback: HotkeyCallback,
|
callback: HotkeyCallback,
|
||||||
|
scope: string,
|
||||||
dependencies?: OptionsOrDependencyArray,
|
dependencies?: OptionsOrDependencyArray,
|
||||||
|
options: Options = {
|
||||||
|
enableOnContentEditable: true,
|
||||||
|
enableOnFormTags: true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||||
|
|
||||||
@ -26,5 +34,10 @@ export function useDirectHotkeys(
|
|||||||
setPendingHotkey(null);
|
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 { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { pendingHotkeyState } from '../states/pendingHotkeysState';
|
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||||
|
|
||||||
export function useSequenceHotkeys(
|
export function useSequenceHotkeys(
|
||||||
firstKey: string,
|
firstKey: Keys,
|
||||||
secondKey: string,
|
secondKey: Keys,
|
||||||
callback: () => void,
|
callback: () => void,
|
||||||
|
scope: string,
|
||||||
|
options: Options = {
|
||||||
|
enableOnContentEditable: true,
|
||||||
|
enableOnFormTags: true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||||
|
|
||||||
@ -15,6 +22,7 @@ export function useSequenceHotkeys(
|
|||||||
() => {
|
() => {
|
||||||
setPendingHotkey(firstKey);
|
setPendingHotkey(firstKey);
|
||||||
},
|
},
|
||||||
|
{ ...options, scopes: [scope] },
|
||||||
[pendingHotkey],
|
[pendingHotkey],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -27,6 +35,7 @@ export function useSequenceHotkeys(
|
|||||||
setPendingHotkey(null);
|
setPendingHotkey(null);
|
||||||
callback();
|
callback();
|
||||||
},
|
},
|
||||||
|
{ ...options, scopes: [scope] },
|
||||||
[pendingHotkey, setPendingHotkey],
|
[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 CompanyChip from '@/companies/components/CompanyChip';
|
||||||
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
|
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
|
||||||
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
||||||
@ -19,6 +20,9 @@ export function PeopleCompanyCell({ people }: OwnProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCell
|
<EditableCell
|
||||||
|
editHotkeysScope={
|
||||||
|
!isCreating ? { scope: InternalHotkeysScope.RelationPicker } : undefined
|
||||||
|
}
|
||||||
editModeContent={
|
editModeContent={
|
||||||
isCreating ? (
|
isCreating ? (
|
||||||
<PeopleCompanyCreateCell people={people} />
|
<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 { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||||
@ -57,6 +61,13 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
|
|||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.Escape,
|
||||||
|
() => closeEditableCell(),
|
||||||
|
InternalHotkeysScope.RelationPicker,
|
||||||
|
[closeEditableCell],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleEntitySelect
|
<SingleEntitySelect
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useRef } from 'react';
|
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 { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
|
||||||
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
|
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
|
||||||
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
|
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
|
||||||
@ -40,15 +41,13 @@ export function SingleEntitySelectBase<
|
|||||||
containerRef,
|
containerRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeys(
|
// TODO: move to better place for scopping
|
||||||
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
onEntitySelected(entitiesInDropdown[hoveredIndex]);
|
onEntitySelected(entitiesInDropdown[hoveredIndex]);
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.RelationPicker,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[entitiesInDropdown, hoveredIndex, onEntitySelected],
|
[entitiesInDropdown, hoveredIndex, onEntitySelected],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import scrollIntoView from 'scroll-into-view';
|
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 { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
|
import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
|
||||||
@ -19,7 +21,8 @@ export function useEntitySelectScroll<
|
|||||||
relationPickerHoverIndexScopedState,
|
relationPickerHoverIndexScopedState,
|
||||||
);
|
);
|
||||||
|
|
||||||
useUpDownHotkeys(
|
useScopedHotkeys(
|
||||||
|
Key.ArrowUp,
|
||||||
() => {
|
() => {
|
||||||
setHoveredIndex((prevSelectedIndex) =>
|
setHoveredIndex((prevSelectedIndex) =>
|
||||||
Math.max(prevSelectedIndex - 1, 0),
|
Math.max(prevSelectedIndex - 1, 0),
|
||||||
@ -41,6 +44,12 @@ export function useEntitySelectScroll<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.RelationPicker,
|
||||||
|
[setHoveredIndex, entities],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.ArrowDown,
|
||||||
() => {
|
() => {
|
||||||
setHoveredIndex((prevSelectedIndex) =>
|
setHoveredIndex((prevSelectedIndex) =>
|
||||||
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
|
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
|
||||||
@ -62,6 +71,7 @@ export function useEntitySelectScroll<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.RelationPicker,
|
||||||
[setHoveredIndex, entities],
|
[setHoveredIndex, entities],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import styled from '@emotion/styled';
|
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 { useEditableCell } from './hooks/useCloseEditableCell';
|
||||||
|
import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
|
||||||
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
|
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
|
||||||
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
|
import { useSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
|
||||||
import { isEditModeScopedState } from './states/isEditModeScopedState';
|
|
||||||
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
||||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||||
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
|
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
|
||||||
@ -27,6 +31,7 @@ type OwnProps = {
|
|||||||
nonEditModeContent: ReactElement;
|
nonEditModeContent: ReactElement;
|
||||||
editModeHorizontalAlign?: 'left' | 'right';
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
editModeVerticalPosition?: 'over' | 'below';
|
editModeVerticalPosition?: 'over' | 'below';
|
||||||
|
editHotkeysScope?: HotkeysScopeStackItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EditableCell({
|
export function EditableCell({
|
||||||
@ -34,39 +39,51 @@ export function EditableCell({
|
|||||||
editModeVerticalPosition = 'over',
|
editModeVerticalPosition = 'over',
|
||||||
editModeContent,
|
editModeContent,
|
||||||
nonEditModeContent,
|
nonEditModeContent,
|
||||||
|
editHotkeysScope,
|
||||||
}: OwnProps) {
|
}: 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
|
// 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.
|
// 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
|
// See https://github.com/twentyhq/twenty/issues/446
|
||||||
function handleOnClick() {
|
function handleOnClick() {
|
||||||
openEditableCell();
|
if (isCurrentCellInEditMode) {
|
||||||
setSoftFocusOnCurrentCell();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnOutsideClick() {
|
if (isSoftFocusActive) {
|
||||||
closeEditableCell();
|
openEditableCell();
|
||||||
|
addToHotkeysScopeStack(
|
||||||
|
editHotkeysScope ?? {
|
||||||
|
scope: InternalHotkeysScope.CellEditMode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSoftFocusOnCurrentCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
|
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellBaseContainer onClick={handleOnClick}>
|
<CellBaseContainer onClick={handleOnClick}>
|
||||||
{isEditMode ? (
|
{isCurrentCellInEditMode ? (
|
||||||
<EditableCellEditMode
|
<EditableCellEditMode
|
||||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
editModeVerticalPosition={editModeVerticalPosition}
|
editModeVerticalPosition={editModeVerticalPosition}
|
||||||
onOutsideClick={handleOnOutsideClick}
|
|
||||||
>
|
>
|
||||||
{editModeContent}
|
{editModeContent}
|
||||||
</EditableCellEditMode>
|
</EditableCellEditMode>
|
||||||
) : hasSoftFocus ? (
|
) : hasSoftFocus ? (
|
||||||
<EditableCellSoftFocusMode>
|
<EditableCellSoftFocusMode editHotkeysScope={editHotkeysScope}>
|
||||||
{nonEditModeContent}
|
{nonEditModeContent}
|
||||||
</EditableCellSoftFocusMode>
|
</EditableCellSoftFocusMode>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { ReactElement, useRef } from 'react';
|
import { ReactElement, useRef } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import styled from '@emotion/styled';
|
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 { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||||
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
|
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
|
||||||
import { overlayBackground } from '@/ui/themes/effects';
|
import { overlayBackground } from '@/ui/themes/effects';
|
||||||
@ -38,7 +39,6 @@ export function EditableCellEditMode({
|
|||||||
editModeHorizontalAlign,
|
editModeHorizontalAlign,
|
||||||
editModeVerticalPosition,
|
editModeVerticalPosition,
|
||||||
children,
|
children,
|
||||||
onOutsideClick,
|
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const wrapperRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
|
|
||||||
@ -46,61 +46,45 @@ export function EditableCellEditMode({
|
|||||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||||
|
|
||||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||||
onOutsideClick?.();
|
closeEditableCell();
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
closeEditableCell();
|
closeEditableCell();
|
||||||
moveDown();
|
moveDown();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.CellEditMode,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[closeEditableCell],
|
[closeEditableCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
'esc',
|
||||||
() => {
|
() => {
|
||||||
closeEditableCell();
|
closeEditableCell();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.CellEditMode,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[closeEditableCell],
|
[closeEditableCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'tab',
|
'tab',
|
||||||
() => {
|
() => {
|
||||||
closeEditableCell();
|
closeEditableCell();
|
||||||
moveRight();
|
moveRight();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.CellEditMode,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[closeEditableCell, moveRight],
|
[closeEditableCell, moveRight],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'shift+tab',
|
'shift+tab',
|
||||||
() => {
|
() => {
|
||||||
closeEditableCell();
|
closeEditableCell();
|
||||||
moveLeft();
|
moveLeft();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.CellEditMode,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[closeEditableCell, moveRight],
|
[closeEditableCell, moveRight],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
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 { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
|
||||||
|
|
||||||
import { useEditableCell } from './hooks/useCloseEditableCell';
|
import { useEditableCell } from './hooks/useCloseEditableCell';
|
||||||
@ -10,26 +11,26 @@ import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
|||||||
|
|
||||||
export function EditableCellSoftFocusMode({
|
export function EditableCellSoftFocusMode({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<unknown>) {
|
editHotkeysScope,
|
||||||
|
}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScopeStackItem }>) {
|
||||||
const { closeEditableCell, openEditableCell } = useEditableCell();
|
const { closeEditableCell, openEditableCell } = useEditableCell();
|
||||||
const [captureHotkeyTypeInFocus] = useRecoilState(
|
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
openEditableCell();
|
openEditableCell();
|
||||||
|
addToHotkeysScopeStack(
|
||||||
|
editHotkeysScope ?? {
|
||||||
|
scope: InternalHotkeysScope.CellEditMode,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[closeEditableCell],
|
[closeEditableCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'*',
|
'*',
|
||||||
(keyboardEvent) => {
|
(keyboardEvent) => {
|
||||||
const isWritingText =
|
const isWritingText =
|
||||||
@ -41,14 +42,16 @@ export function EditableCellSoftFocusMode({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (captureHotkeyTypeInFocus) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openEditableCell();
|
openEditableCell();
|
||||||
|
addToHotkeysScopeStack(
|
||||||
|
editHotkeysScope ?? {
|
||||||
|
scope: InternalHotkeysScope.CellEditMode,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
|
[openEditableCell, addToHotkeysScopeStack, editHotkeysScope],
|
||||||
{
|
{
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
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 { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
|
||||||
|
|
||||||
import { isEditModeScopedState } from '../states/isEditModeScopedState';
|
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||||
|
|
||||||
export function useEditableCell() {
|
export function useEditableCell() {
|
||||||
const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
|
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||||
|
|
||||||
const closeEditableCell = useRecoilCallback(
|
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
|
||||||
({ set }) =>
|
|
||||||
async () => {
|
|
||||||
setIsEditMode(false);
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
const removeHighestHotkeysScopedStackItem =
|
||||||
|
useRemoveHighestHotkeysScopeStackItem();
|
||||||
|
|
||||||
set(isSomeInputInEditModeState, false);
|
function closeEditableCell() {
|
||||||
},
|
closeCurrentCellInEditMode();
|
||||||
[setIsEditMode],
|
removeHighestHotkeysScopedStackItem();
|
||||||
);
|
}
|
||||||
|
|
||||||
const openEditableCell = useRecoilCallback(
|
const openEditableCell = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
@ -29,11 +29,12 @@ export function useEditableCell() {
|
|||||||
|
|
||||||
if (!isSomeInputInEditMode) {
|
if (!isSomeInputInEditMode) {
|
||||||
set(isSomeInputInEditModeState, true);
|
set(isSomeInputInEditModeState, true);
|
||||||
|
set(isSoftFocusActiveState, false);
|
||||||
|
|
||||||
setIsEditMode(true);
|
setCurrentCellInEditMode();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setIsEditMode],
|
[setCurrentCellInEditMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
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 { 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 { 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() {
|
export function useIsSoftFocusOnCurrentCell() {
|
||||||
const [currentRowNumber] = useRecoilScopedState(
|
const currentCellPosition = useCurrentCellPosition();
|
||||||
currentRowNumberScopedState,
|
|
||||||
RowContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [currentColumnNumber] = useRecoilScopedState(
|
|
||||||
currentColumnNumberScopedState,
|
|
||||||
CellContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentTablePosition: TablePosition = useMemo(
|
|
||||||
() => ({
|
|
||||||
column: currentColumnNumber,
|
|
||||||
row: currentRowNumber,
|
|
||||||
}),
|
|
||||||
[currentColumnNumber, currentRowNumber],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSoftFocusOnCell = useRecoilValue(
|
const isSoftFocusOnCell = useRecoilValue(
|
||||||
isSoftFocusOnCellFamilyState(currentTablePosition),
|
isSoftFocusOnCellFamilyState(currentCellPosition),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isSoftFocusOnCell;
|
return isSoftFocusOnCell;
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
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 { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { useSetSoftFocusPosition } from '@/ui/tables/hooks/useSetSoftFocusPosition';
|
import { useSetSoftFocusPosition } from '@/ui/tables/hooks/useSetSoftFocusPosition';
|
||||||
import { CellContext } from '@/ui/tables/states/CellContext';
|
import { CellContext } from '@/ui/tables/states/CellContext';
|
||||||
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
|
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
|
||||||
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
|
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
|
||||||
|
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
|
||||||
import { RowContext } from '@/ui/tables/states/RowContext';
|
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 setSoftFocusPosition = useSetSoftFocusPosition();
|
||||||
const [currentRowNumber] = useRecoilScopedState(
|
const [currentRowNumber] = useRecoilScopedState(
|
||||||
currentRowNumberScopedState,
|
currentRowNumberScopedState,
|
||||||
@ -20,7 +24,7 @@ export function useSetSoftFocusOnCurrentCell() {
|
|||||||
CellContext,
|
CellContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentTablePosition: TablePosition = useMemo(
|
const currentTablePosition: CellPosition = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
column: currentColumnNumber,
|
column: currentColumnNumber,
|
||||||
row: currentRowNumber,
|
row: currentRowNumber,
|
||||||
@ -28,7 +32,18 @@ export function useSetSoftFocusOnCurrentCell() {
|
|||||||
[currentColumnNumber, currentRowNumber],
|
[currentColumnNumber, currentRowNumber],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [, setIsSoftFocusActive] = useRecoilState(isSoftFocusActiveState);
|
||||||
|
|
||||||
|
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
setSoftFocusPosition(currentTablePosition);
|
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 { ReactElement } from 'react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { textInputStyle } from '@/ui/themes/effects';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
|
|
||||||
import { EditableCell } from '../EditableCell';
|
import { EditableCell } from '../EditableCell';
|
||||||
|
|
||||||
|
import { EditableDoubleTextEditMode } from './EditableDoubleTextEditMode';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
firstValue: string;
|
firstValue: string;
|
||||||
secondValue: string;
|
secondValue: string;
|
||||||
@ -14,57 +15,25 @@ type OwnProps = {
|
|||||||
onChange: (firstValue: string, secondValue: string) => void;
|
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({
|
export function EditableDoubleText({
|
||||||
firstValue,
|
firstValue,
|
||||||
secondValue,
|
secondValue,
|
||||||
firstValuePlaceholder,
|
firstValuePlaceholder,
|
||||||
secondValuePlaceholder,
|
secondValuePlaceholder,
|
||||||
nonEditModeContent,
|
|
||||||
onChange,
|
onChange,
|
||||||
|
nonEditModeContent,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const firstValueInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCell
|
<EditableCell
|
||||||
|
editHotkeysScope={{ scope: InternalHotkeysScope.CellDoubleTextInput }}
|
||||||
editModeContent={
|
editModeContent={
|
||||||
<StyledContainer>
|
<EditableDoubleTextEditMode
|
||||||
<StyledEditInplaceInput
|
firstValue={firstValue}
|
||||||
autoFocus
|
secondValue={secondValue}
|
||||||
placeholder={firstValuePlaceholder}
|
firstValuePlaceholder={firstValuePlaceholder}
|
||||||
ref={firstValueInputRef}
|
secondValuePlaceholder={secondValuePlaceholder}
|
||||||
value={firstValue}
|
onChange={onChange}
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
nonEditModeContent={nonEditModeContent}
|
nonEditModeContent={nonEditModeContent}
|
||||||
></EditableCell>
|
></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,
|
SortType,
|
||||||
} from '@/filters-and-sorts/interfaces/sorts/interface';
|
} from '@/filters-and-sorts/interfaces/sorts/interface';
|
||||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
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 { RowContext } from '@/ui/tables/states/RowContext';
|
||||||
|
|
||||||
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
|
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
|
||||||
@ -103,6 +105,8 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
|||||||
availableSorts,
|
availableSorts,
|
||||||
onSortsUpdate,
|
onSortsUpdate,
|
||||||
}: OwnProps<TData, SortField>) {
|
}: OwnProps<TData, SortField>) {
|
||||||
|
const tableBodyRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
|
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
|
||||||
currentRowSelectionState,
|
currentRowSelectionState,
|
||||||
);
|
);
|
||||||
@ -119,6 +123,12 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
|||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const leaveTableFocus = useLeaveTableFocus();
|
||||||
|
|
||||||
|
useListenClickOutsideArrayOfRef([tableBodyRef], () => {
|
||||||
|
leaveTableFocus();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTableWithHeader>
|
<StyledTableWithHeader>
|
||||||
<TableHeader
|
<TableHeader
|
||||||
@ -127,7 +137,7 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
|||||||
availableSorts={availableSorts}
|
availableSorts={availableSorts}
|
||||||
onSortsUpdate={onSortsUpdate}
|
onSortsUpdate={onSortsUpdate}
|
||||||
/>
|
/>
|
||||||
<StyledTableScrollableContainer>
|
<StyledTableScrollableContainer ref={tableBodyRef}>
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { ReactNode, useRef } from 'react';
|
import { ReactNode, useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
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 { IconChevronDown } from '@/ui/icons/index';
|
||||||
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
|
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ type OwnProps = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
isUnfolded?: boolean;
|
isUnfolded?: boolean;
|
||||||
setIsUnfolded?: React.Dispatch<React.SetStateAction<boolean>>;
|
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
|
||||||
resetState?: () => void;
|
resetState?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -158,22 +159,23 @@ function DropdownButton({
|
|||||||
isActive,
|
isActive,
|
||||||
children,
|
children,
|
||||||
isUnfolded = false,
|
isUnfolded = false,
|
||||||
setIsUnfolded,
|
onIsUnfoldedChange,
|
||||||
resetState,
|
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
useScopedHotkeys(
|
||||||
captureHotkeyTypeInFocusState,
|
[Key.Enter, Key.Escape],
|
||||||
|
() => {
|
||||||
|
onIsUnfoldedChange?.(false);
|
||||||
|
},
|
||||||
|
InternalHotkeysScope.TableHeaderDropdownButton,
|
||||||
|
[onIsUnfoldedChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onButtonClick = () => {
|
const onButtonClick = () => {
|
||||||
setIsUnfolded && setIsUnfolded(!isUnfolded);
|
onIsUnfoldedChange?.(!isUnfolded);
|
||||||
setCaptureHotkeyTypeInFocus((isPreviousUnfolded) => !isPreviousUnfolded);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOutsideClick = () => {
|
const onOutsideClick = () => {
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
onIsUnfoldedChange?.(false);
|
||||||
setIsUnfolded && setIsUnfolded(false);
|
|
||||||
resetState && resetState();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { filterDropdownSearchInputScopedState } from '@/filters-and-sorts/states
|
|||||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||||
import { selectedOperandInDropdownScopedState } from '@/filters-and-sorts/states/selectedOperandInDropdownScopedState';
|
import { selectedOperandInDropdownScopedState } from '@/filters-and-sorts/states/selectedOperandInDropdownScopedState';
|
||||||
import { tableFilterDefinitionUsedInDropdownScopedState } from '@/filters-and-sorts/states/tableFilterDefinitionUsedInDropdownScopedState';
|
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 { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { TableContext } from '@/ui/tables/states/TableContext';
|
import { TableContext } from '@/ui/tables/states/TableContext';
|
||||||
|
|
||||||
@ -21,6 +23,11 @@ import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
|
|||||||
export function FilterDropdownButton() {
|
export function FilterDropdownButton() {
|
||||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||||
|
|
||||||
|
useHotkeysScopeOnBooleanState(
|
||||||
|
{ scope: InternalHotkeysScope.TableHeaderDropdownButton },
|
||||||
|
isUnfolded,
|
||||||
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isFilterDropdownOperandSelectUnfolded,
|
isFilterDropdownOperandSelectUnfolded,
|
||||||
setIsFilterDropdownOperandSelectUnfolded,
|
setIsFilterDropdownOperandSelectUnfolded,
|
||||||
@ -64,13 +71,21 @@ export function FilterDropdownButton() {
|
|||||||
|
|
||||||
const isFilterSelected = (activeTableFilters?.length ?? 0) > 0;
|
const isFilterSelected = (activeTableFilters?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
||||||
|
if (newIsUnfolded) {
|
||||||
|
setIsUnfolded(true);
|
||||||
|
} else {
|
||||||
|
setIsUnfolded(false);
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
label="Filter"
|
label="Filter"
|
||||||
isActive={isFilterSelected}
|
isActive={isFilterSelected}
|
||||||
isUnfolded={isUnfolded}
|
isUnfolded={isUnfolded}
|
||||||
setIsUnfolded={setIsUnfolded}
|
onIsUnfoldedChange={handleIsUnfoldedChange}
|
||||||
resetState={resetState}
|
|
||||||
>
|
>
|
||||||
{!tableFilterDefinitionUsedInDropdown ? (
|
{!tableFilterDefinitionUsedInDropdown ? (
|
||||||
<FilterDropdownFilterSelect />
|
<FilterDropdownFilterSelect />
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SelectedSortType,
|
SelectedSortType,
|
||||||
SortType,
|
SortType,
|
||||||
} from '@/filters-and-sorts/interfaces/sorts/interface';
|
} from '@/filters-and-sorts/interfaces/sorts/interface';
|
||||||
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
|
|
||||||
|
|
||||||
import DropdownButton from './DropdownButton';
|
import DropdownButton from './DropdownButton';
|
||||||
|
|
||||||
@ -23,9 +21,6 @@ export function SortDropdownButton<SortField>({
|
|||||||
onSortSelect,
|
onSortSelect,
|
||||||
}: OwnProps<SortField>) {
|
}: OwnProps<SortField>) {
|
||||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
|
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
|
||||||
|
|
||||||
@ -41,17 +36,24 @@ export function SortDropdownButton<SortField>({
|
|||||||
|
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
setIsOptionUnfolded(false);
|
setIsOptionUnfolded(false);
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
|
||||||
setSelectedSortDirection('asc');
|
setSelectedSortDirection('asc');
|
||||||
}, [setCaptureHotkeyTypeInFocus]);
|
}, []);
|
||||||
|
|
||||||
|
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
||||||
|
if (newIsUnfolded) {
|
||||||
|
setIsUnfolded(true);
|
||||||
|
} else {
|
||||||
|
setIsUnfolded(false);
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
label="Sort"
|
label="Sort"
|
||||||
isActive={isSortSelected}
|
isActive={isSortSelected}
|
||||||
isUnfolded={isUnfolded}
|
isUnfolded={isUnfolded}
|
||||||
setIsUnfolded={setIsUnfolded}
|
onIsUnfoldedChange={handleIsUnfoldedChange}
|
||||||
resetState={resetState}
|
|
||||||
>
|
>
|
||||||
{isOptionUnfolded
|
{isOptionUnfolded
|
||||||
? options.map((option, index) => (
|
? options.map((option, index) => (
|
||||||
@ -59,7 +61,6 @@ export function SortDropdownButton<SortField>({
|
|||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSortDirection(option);
|
setSelectedSortDirection(option);
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
|
||||||
setIsOptionUnfolded(false);
|
setIsOptionUnfolded(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -80,7 +81,6 @@ export function SortDropdownButton<SortField>({
|
|||||||
key={index + 1}
|
key={index + 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsUnfolded(false);
|
setIsUnfolded(false);
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
|
||||||
onSortItemSelect(sort);
|
onSortItemSelect(sort);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState } from 'recoil';
|
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 { isDefined } from '@/utils/type-guards/isDefined';
|
||||||
|
|
||||||
import { Panel } from '../../Panel';
|
import { Panel } from '../../Panel';
|
||||||
@ -18,14 +18,14 @@ const StyledRightDrawer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function RightDrawer() {
|
export function RightDrawer() {
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
|
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
|
||||||
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||||
useEffect(() => {
|
|
||||||
setCaptureHotkeyTypeInFocus(isRightDrawerOpen);
|
useHotkeysScopeOnBooleanState(
|
||||||
}, [isRightDrawerOpen, setCaptureHotkeyTypeInFocus]);
|
{ scope: InternalHotkeysScope.RightDrawer },
|
||||||
|
isRightDrawerOpen,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
|
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import styled from '@emotion/styled';
|
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 { IconPlus } from '@/ui/icons/index';
|
||||||
|
|
||||||
import NavCollapseButton from '../navbar/NavCollapseButton';
|
import NavCollapseButton from '../navbar/NavCollapseButton';
|
||||||
@ -50,7 +51,7 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
|
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
|
||||||
useDirectHotkeys('c', () => onAddButtonClick && onAddButtonClick());
|
useScopedHotkeys('c', () => onAddButtonClick?.(), InternalHotkeysScope.Table);
|
||||||
|
|
||||||
return (
|
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 { useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants';
|
|
||||||
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
||||||
|
|
||||||
import { useResetTableRowSelection } from './useResetTableRowSelection';
|
import { useResetTableRowSelection } from './useResetTableRowSelection';
|
||||||
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
|
|
||||||
|
|
||||||
export type TableDimensions = {
|
export type TableDimensions = {
|
||||||
numberOfRows: number;
|
numberOfRows: number;
|
||||||
@ -30,13 +28,4 @@ export function useInitializeEntityTable({
|
|||||||
numberOfRows,
|
numberOfRows,
|
||||||
});
|
});
|
||||||
}, [numberOfRows, numberOfColumns, setTableDimensions]);
|
}, [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 { 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 { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
|
||||||
|
|
||||||
|
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||||
import { useMoveSoftFocus } from './useMoveSoftFocus';
|
import { useMoveSoftFocus } from './useMoveSoftFocus';
|
||||||
|
|
||||||
export function useMapKeyboardToSoftFocus() {
|
export function useMapKeyboardToSoftFocus() {
|
||||||
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
|
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
|
||||||
|
|
||||||
|
const removeFromHotkeysScopedStack = useRemoveFromHotkeysScopeStack();
|
||||||
|
const disableSoftFocus = useDisableSoftFocus();
|
||||||
|
|
||||||
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
|
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'up, shift+enter',
|
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
|
||||||
() => {
|
() => {
|
||||||
if (!isSomeInputInEditMode) {
|
if (!isSomeInputInEditMode) {
|
||||||
moveUp();
|
moveUp();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
[moveUp, isSomeInputInEditMode],
|
[moveUp, isSomeInputInEditMode],
|
||||||
{
|
|
||||||
preventDefault: true,
|
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'down',
|
Key.ArrowDown,
|
||||||
() => {
|
() => {
|
||||||
if (!isSomeInputInEditMode) {
|
if (!isSomeInputInEditMode) {
|
||||||
moveDown();
|
moveDown();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
[moveDown, isSomeInputInEditMode],
|
[moveDown, isSomeInputInEditMode],
|
||||||
{
|
|
||||||
preventDefault: true,
|
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
['left', 'shift+tab'],
|
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
|
||||||
() => {
|
() => {
|
||||||
if (!isSomeInputInEditMode) {
|
if (!isSomeInputInEditMode) {
|
||||||
moveLeft();
|
moveLeft();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
[moveLeft, isSomeInputInEditMode],
|
[moveLeft, isSomeInputInEditMode],
|
||||||
{
|
|
||||||
preventDefault: true,
|
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
['right', 'tab'],
|
[Key.ArrowRight, Key.Tab],
|
||||||
() => {
|
() => {
|
||||||
if (!isSomeInputInEditMode) {
|
if (!isSomeInputInEditMode) {
|
||||||
moveRight();
|
moveRight();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InternalHotkeysScope.TableSoftFocus,
|
||||||
[moveRight, isSomeInputInEditMode],
|
[moveRight, isSomeInputInEditMode],
|
||||||
{
|
);
|
||||||
preventDefault: true,
|
|
||||||
enableOnContentEditable: true,
|
useScopedHotkeys(
|
||||||
enableOnFormTags: true,
|
[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 { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||||
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
|
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
|
||||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||||
import { TablePosition } from '../types/TablePosition';
|
import { CellPosition } from '../types/CellPosition';
|
||||||
|
|
||||||
export function useSetSoftFocusPosition() {
|
export function useSetSoftFocusPosition() {
|
||||||
return useRecoilCallback(({ set, snapshot }) => {
|
return useRecoilCallback(({ set, snapshot }) => {
|
||||||
return (newPosition: TablePosition) => {
|
return (newPosition: CellPosition) => {
|
||||||
const currentPosition = snapshot
|
const currentPosition = snapshot
|
||||||
.getLoadable(softFocusPositionState)
|
.getLoadable(softFocusPositionState)
|
||||||
.valueOrThrow();
|
.valueOrThrow();
|
||||||
|
|
||||||
|
set(isSoftFocusActiveState, true);
|
||||||
|
|
||||||
set(isSoftFocusOnCellFamilyState(currentPosition), false);
|
set(isSoftFocusOnCellFamilyState(currentPosition), false);
|
||||||
|
|
||||||
set(softFocusPositionState, newPosition);
|
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 { 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',
|
key: 'isSoftFocusOnCellFamilyState',
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { atom } from 'recoil';
|
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',
|
key: 'softFocusPositionState',
|
||||||
default: {
|
default: {
|
||||||
row: 0,
|
row: 0,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export type TablePosition = {
|
export type CellPosition = {
|
||||||
row: number;
|
row: number;
|
||||||
column: 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 (
|
return (
|
||||||
value && typeof value.row === 'number' && typeof value.column === 'number'
|
value && typeof value.row === 'number' && typeof value.column === 'number'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export interface PositionType {
|
export type PositionType = {
|
||||||
x: number | null;
|
x: number | null;
|
||||||
y: number | null;
|
y: number | null;
|
||||||
}
|
};
|
||||||
@ -1,16 +1,17 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/ui/Title';
|
||||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
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 { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
@ -48,9 +49,6 @@ export function CreateProfile() {
|
|||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
|
|
||||||
const [currentUser] = useRecoilState(currentUserState);
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [updateUser] = useUpdateUserMutation();
|
const [updateUser] = useUpdateUserMutation();
|
||||||
|
|
||||||
@ -85,29 +83,18 @@ export function CreateProfile() {
|
|||||||
throw errors;
|
throw errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCaptureHotkeyTypeInFocus(false);
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}, [
|
}, [currentUser?.id, firstName, lastName, navigate, updateUser]);
|
||||||
currentUser?.id,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
navigate,
|
|
||||||
setCaptureHotkeyTypeInFocus,
|
|
||||||
updateUser,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
Key.Enter,
|
||||||
() => {
|
() => {
|
||||||
handleCreate();
|
handleCreate();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.Modal,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[handleCreate],
|
[handleCreate],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -117,10 +104,6 @@ export function CreateProfile() {
|
|||||||
}
|
}
|
||||||
}, [onboardingStatus, navigate]);
|
}, [onboardingStatus, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCaptureHotkeyTypeInFocus(true);
|
|
||||||
}, [setCaptureHotkeyTypeInFocus]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Create profile</Title>
|
<Title>Create profile</Title>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -8,6 +7,8 @@ import { SubTitle } from '@/auth/components/ui/SubTitle';
|
|||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/ui/Title';
|
||||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
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 { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
@ -68,15 +69,12 @@ export function CreateWorkspace() {
|
|||||||
}
|
}
|
||||||
}, [navigate, updateWorkspace, workspaceName]);
|
}, [navigate, updateWorkspace, workspaceName]);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
handleCreate();
|
handleCreate();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.Modal,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[handleCreate],
|
[handleCreate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -13,7 +12,8 @@ import { Title } from '@/auth/components/ui/Title';
|
|||||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
||||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
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 { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
import { AnimatedEaseIn } from '@/ui/components/motion/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/components/motion/AnimatedEaseIn';
|
||||||
@ -31,9 +31,6 @@ const StyledFooterNote = styled(FooterNote)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function Index() {
|
export function Index() {
|
||||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
|
||||||
captureHotkeyTypeInFocusState,
|
|
||||||
);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [, setMockMode] = useRecoilState(isMockModeState);
|
const [, setMockMode] = useRecoilState(isMockModeState);
|
||||||
@ -59,29 +56,19 @@ export function Index() {
|
|||||||
navigate('/auth/password-login');
|
navigate('/auth/password-login');
|
||||||
}, [navigate, visible]);
|
}, [navigate, visible]);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
onPasswordLoginClick();
|
onPasswordLoginClick();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.Modal,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[onPasswordLoginClick],
|
[onPasswordLoginClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMockMode(true);
|
setMockMode(true);
|
||||||
setCaptureHotkeyTypeInFocus(true);
|
|
||||||
setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : '');
|
setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : '');
|
||||||
}, [
|
}, [navigate, setMockMode, setAuthFlowUserEmail, demoMode]);
|
||||||
navigate,
|
|
||||||
setMockMode,
|
|
||||||
setCaptureHotkeyTypeInFocus,
|
|
||||||
setAuthFlowUserEmail,
|
|
||||||
demoMode,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@ -12,6 +11,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
|||||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
||||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
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 { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||||
@ -74,15 +75,12 @@ export function PasswordLogin() {
|
|||||||
}
|
}
|
||||||
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
|
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
|
||||||
|
|
||||||
useHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
handleLogin();
|
handleLogin();
|
||||||
},
|
},
|
||||||
{
|
InternalHotkeysScope.Modal,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
},
|
|
||||||
[handleLogin],
|
[handleLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
import { GET_COMPANIES } from '@/companies/services';
|
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 { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
|
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
|
||||||
import { IconBuildingSkyscraper } from '@/ui/icons/index';
|
import { IconBuildingSkyscraper } from '@/ui/icons/index';
|
||||||
@ -24,6 +28,18 @@ const StyledTableContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function Companies() {
|
export function Companies() {
|
||||||
|
const [isMockMode] = useRecoilState(isMockModeState);
|
||||||
|
|
||||||
|
const hotkeysEnabled = !isMockMode;
|
||||||
|
|
||||||
|
useHotkeysScopeOnMountOnly(
|
||||||
|
{
|
||||||
|
scope: InternalHotkeysScope.Table,
|
||||||
|
customScopes: { 'command-menu': true, goto: true },
|
||||||
|
},
|
||||||
|
hotkeysEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
const [insertCompany] = useInsertCompanyMutation();
|
const [insertCompany] = useInsertCompanyMutation();
|
||||||
|
|
||||||
async function handleAddButtonClick() {
|
async function handleAddButtonClick() {
|
||||||
@ -45,20 +61,22 @@ export function Companies() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTopBarContainer
|
<>
|
||||||
title="Companies"
|
<WithTopBarContainer
|
||||||
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
title="Companies"
|
||||||
onAddButtonClick={handleAddButtonClick}
|
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
||||||
>
|
onAddButtonClick={handleAddButtonClick}
|
||||||
<RecoilScope SpecificContext={TableContext}>
|
>
|
||||||
<StyledTableContainer>
|
<RecoilScope SpecificContext={TableContext}>
|
||||||
<CompanyTable />
|
<StyledTableContainer>
|
||||||
</StyledTableContainer>
|
<CompanyTable />
|
||||||
<EntityTableActionBar>
|
</StyledTableContainer>
|
||||||
<TableActionBarButtonCreateCommentThreadCompany />
|
<EntityTableActionBar>
|
||||||
<TableActionBarButtonDeleteCompanies />
|
<TableActionBarButtonCreateCommentThreadCompany />
|
||||||
</EntityTableActionBar>
|
<TableActionBarButtonDeleteCompanies />
|
||||||
</RecoilScope>
|
</EntityTableActionBar>
|
||||||
</WithTopBarContainer>
|
</RecoilScope>
|
||||||
|
</WithTopBarContainer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
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 { GET_PEOPLE } from '@/people/services';
|
||||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
|
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
|
||||||
@ -22,6 +24,11 @@ const StyledPeopleContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function People() {
|
export function People() {
|
||||||
|
useHotkeysScopeOnMountOnly({
|
||||||
|
scope: InternalHotkeysScope.Table,
|
||||||
|
customScopes: { 'command-menu': true, goto: true },
|
||||||
|
});
|
||||||
|
|
||||||
const [insertPersonMutation] = useInsertPersonMutation();
|
const [insertPersonMutation] = useInsertPersonMutation();
|
||||||
|
|
||||||
async function handleAddButtonClick() {
|
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"
|
semver "^7.5.3"
|
||||||
yargs-parser "^21.0.1"
|
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:
|
ts-log@^2.2.3:
|
||||||
version "2.2.5"
|
version "2.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623"
|
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623"
|
||||||
|
|||||||
Reference in New Issue
Block a user