Fix/table rerenders (#609)

* Fixed top bar rerenders

* Fixed rerender on editable cell

* Fix lint

* asd

* Fix

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-12 05:51:24 +02:00
committed by GitHub
parent b5de2abd48
commit 5e0e449e4c
17 changed files with 211 additions and 126 deletions

View File

@ -27,15 +27,15 @@ root.render(
<ApolloProvider> <ApolloProvider>
<AppThemeProvider> <AppThemeProvider>
<StrictMode> <StrictMode>
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}> <BrowserRouter>
<BrowserRouter> <UserProvider>
<UserProvider> <ClientConfigProvider>
<ClientConfigProvider> <HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}>
<App /> <App />
</ClientConfigProvider> </HotkeysProvider>
</UserProvider> </ClientConfigProvider>
</BrowserRouter> </UserProvider>
</HotkeysProvider> </BrowserRouter>
</StrictMode> </StrictMode>
</AppThemeProvider> </AppThemeProvider>
</ApolloProvider> </ApolloProvider>

View File

@ -15,5 +15,11 @@ export function useGoToHotkeys(key: Keys, location: string) {
navigate(location); navigate(location);
}, },
InternalHotkeysScope.Goto, InternalHotkeysScope.Goto,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[navigate],
); );
} }

View File

@ -14,6 +14,7 @@ export function useSequenceHotkeys(
enableOnFormTags: true, enableOnFormTags: true,
preventDefault: true, preventDefault: true,
}, },
deps: any[] = [],
) { ) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
@ -23,7 +24,7 @@ export function useSequenceHotkeys(
setPendingHotkey(firstKey); setPendingHotkey(firstKey);
}, },
{ ...options, scopes: [scope] }, { ...options, scopes: [scope] },
[pendingHotkey], [setPendingHotkey],
); );
useHotkeys( useHotkeys(
@ -36,6 +37,6 @@ export function useSequenceHotkeys(
callback(); callback();
}, },
{ ...options, scopes: [scope] }, { ...options, scopes: [scope] },
[pendingHotkey, setPendingHotkey], [pendingHotkey, setPendingHotkey, ...deps],
); );
} }

View File

@ -6,6 +6,16 @@ import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
import { currentHotkeysScopeState } from '../states/internal/currentHotkeysScopeState'; import { currentHotkeysScopeState } from '../states/internal/currentHotkeysScopeState';
import { CustomHotkeysScopes } from '../types/internal/CustomHotkeysScope'; import { CustomHotkeysScopes } from '../types/internal/CustomHotkeysScope';
function isCustomScopesEqual(
customScopesA: CustomHotkeysScopes | undefined,
customScopesB: CustomHotkeysScopes | undefined,
) {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
customScopesA?.goto === customScopesB?.goto
);
}
export function useSetHotkeysScope() { export function useSetHotkeysScope() {
return useRecoilCallback( return useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
@ -14,16 +24,6 @@ export function useSetHotkeysScope() {
currentHotkeysScopeState, currentHotkeysScopeState,
); );
function isCustomScopesEqual(
customScopesA: CustomHotkeysScopes | undefined,
customScopesB: CustomHotkeysScopes | undefined,
) {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
customScopesA?.goto === customScopesB?.goto
);
}
if (currentHotkeysScope.scope === hotkeysScopeToSet) { if (currentHotkeysScope.scope === hotkeysScopeToSet) {
if (!isDefined(customScopes)) { if (!isDefined(customScopes)) {
if ( if (

View File

@ -2,12 +2,9 @@ import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { HotkeysScope } from '@/hotkeys/types/internal/HotkeysScope'; import { HotkeysScope } from '@/hotkeys/types/internal/HotkeysScope';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode'; import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
import { useEditableCell } from './hooks/useEditableCell';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell'; import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode'; import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode'; import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode'; import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
@ -40,33 +37,10 @@ export function EditableCell({
}: OwnProps) { }: OwnProps) {
const { isCurrentCellInEditMode } = useCurrentCellEditMode(); const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const { openEditableCell } = useEditableCell();
const hasSoftFocus = useIsSoftFocusOnCurrentCell(); const hasSoftFocus = useIsSoftFocusOnCurrentCell();
// TODO: we might have silent problematic behavior because of the setTimeout in openEditableCell, investigate
// Maybe we could build a switchEditableCell to handle the case where we go from one cell to another.
// See https://github.com/twentyhq/twenty/issues/446
function handleOnClick() {
if (isCurrentCellInEditMode) {
return;
}
if (hasSoftFocus) {
openEditableCell(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
} else {
setSoftFocusOnCurrentCell();
}
}
return ( return (
<CellBaseContainer onClick={handleOnClick}> <CellBaseContainer>
{isCurrentCellInEditMode ? ( {isCurrentCellInEditMode ? (
<EditableCellEditMode <EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}

View File

@ -1,9 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell'; import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
type Props = { type Props = {
softFocus: boolean; softFocus?: boolean;
}; };
export const EditableCellNormalModeOuterContainer = styled.div<Props>` export const EditableCellNormalModeOuterContainer = styled.div<Props>`
@ -35,10 +35,14 @@ export const EditableCellNormalModeInnerContainer = styled.div`
export function EditableCellDisplayMode({ export function EditableCellDisplayMode({
children, children,
}: React.PropsWithChildren<unknown>) { }: React.PropsWithChildren<unknown>) {
const hasSoftFocus = useIsSoftFocusOnCurrentCell(); const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
function handleOnClick() {
setSoftFocusOnCurrentCell();
}
return ( return (
<EditableCellNormalModeOuterContainer softFocus={hasSoftFocus}> <EditableCellNormalModeOuterContainer onClick={handleOnClick}>
<EditableCellNormalModeInnerContainer> <EditableCellNormalModeInnerContainer>
{children} {children}
</EditableCellNormalModeInnerContainer> </EditableCellNormalModeInnerContainer>

View File

@ -6,25 +6,32 @@ import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysSc
import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey'; import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
import { useEditableCell } from './hooks/useEditableCell'; import { useEditableCell } from './hooks/useEditableCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode'; import {
EditableCellNormalModeInnerContainer,
EditableCellNormalModeOuterContainer,
} from './EditableCellDisplayMode';
export function EditableCellSoftFocusMode({ export function EditableCellSoftFocusMode({
children, children,
editHotkeysScope, editHotkeysScope,
}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScope }>) { }: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScope }>) {
const { closeEditableCell, openEditableCell } = useEditableCell(); const { openEditableCell } = useEditableCell();
function openEditMode() {
openEditableCell(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
}
useScopedHotkeys( useScopedHotkeys(
'enter', 'enter',
() => { () => {
openEditableCell( openEditMode();
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
}, },
InternalHotkeysScope.TableSoftFocus, InternalHotkeysScope.TableSoftFocus,
[closeEditableCell, editHotkeysScope], [openEditMode],
); );
useScopedHotkeys( useScopedHotkeys(
@ -39,18 +46,27 @@ export function EditableCellSoftFocusMode({
return; return;
} }
openEditableCell( openEditMode();
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
}, },
InternalHotkeysScope.TableSoftFocus, InternalHotkeysScope.TableSoftFocus,
[openEditableCell, editHotkeysScope], [openEditMode],
{ {
preventDefault: false, preventDefault: false,
}, },
); );
return <EditableCellDisplayMode>{children}</EditableCellDisplayMode>; function handleClick() {
openEditMode();
}
return (
<EditableCellNormalModeOuterContainer
onClick={handleClick}
softFocus={true}
>
<EditableCellNormalModeInnerContainer>
{children}
</EditableCellNormalModeInnerContainer>
</EditableCellNormalModeOuterContainer>
);
} }

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope'; import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
@ -14,6 +14,7 @@ import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useSetSoftFocusOnCurrentCell() { export function useSetSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition(); const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState( const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState, currentRowNumberScopedState,
RowContext, RowContext,
@ -32,18 +33,17 @@ export function useSetSoftFocusOnCurrentCell() {
[currentColumnNumber, currentRowNumber], [currentColumnNumber, currentRowNumber],
); );
const [, setIsSoftFocusActive] = useRecoilState(isSoftFocusActiveState);
const setHotkeysScope = useSetHotkeysScope(); const setHotkeysScope = useSetHotkeysScope();
return useCallback(() => { return useRecoilCallback(
setSoftFocusPosition(currentTablePosition); ({ set }) =>
setIsSoftFocusActive(true); () => {
setHotkeysScope(InternalHotkeysScope.TableSoftFocus); setSoftFocusPosition(currentTablePosition);
}, [
setSoftFocusPosition, set(isSoftFocusActiveState, true);
currentTablePosition,
setIsSoftFocusActive, setHotkeysScope(InternalHotkeysScope.TableSoftFocus);
setHotkeysScope, },
]); [setHotkeysScope, currentTablePosition, setSoftFocusPosition],
);
} }

View File

@ -0,0 +1,7 @@
import styled from '@emotion/styled';
export const FlexExpandingContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
`;

View File

@ -1,9 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ContentContainer } from './ContentContainer'; import { RightDrawerContainer } from './RightDrawerContainer';
type OwnProps = { type OwnProps = {
children: JSX.Element; children: JSX.Element | JSX.Element[];
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -15,7 +15,7 @@ const StyledContainer = styled.div`
export function NoTopBarContainer({ children }: OwnProps) { export function NoTopBarContainer({ children }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<ContentContainer topMargin={16}>{children}</ContentContainer> <RightDrawerContainer topMargin={16}>{children}</RightDrawerContainer>
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -0,0 +1,43 @@
import styled from '@emotion/styled';
import { Panel } from '../Panel';
import { RightDrawer } from '../right-drawer/components/RightDrawer';
type OwnProps = {
children: JSX.Element | JSX.Element[];
topMargin?: number;
};
const StyledMainContainer = styled.div<{ topMargin: number }>`
background: ${({ theme }) => theme.background.noisy};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
height: calc(100% - ${(props) => props.topMargin}px);
padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(3)};
width: calc(100% - ${({ theme }) => theme.spacing(3)});
`;
type LeftContainerProps = {
isRightDrawerOpen?: boolean;
};
const StyledLeftContainer = styled.div<LeftContainerProps>`
display: flex;
position: relative;
width: 100%;
`;
export function RightDrawerContainer({ children, topMargin }: OwnProps) {
return (
<StyledMainContainer topMargin={topMargin ?? 0}>
<StyledLeftContainer>
<Panel>{children}</Panel>
</StyledLeftContainer>
<RightDrawer />
</StyledMainContainer>
);
}

View File

@ -0,0 +1,15 @@
import styled from '@emotion/styled';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
export function VerticalFullWidthContainer({
children,
}: {
children: JSX.Element[];
}) {
return <StyledContainer>{children}</StyledContainer>;
}

View File

@ -1,12 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { TopBarHotkeys } from '../top-bar/TableTopBarHotkeys';
import { TOP_BAR_MIN_HEIGHT, TopBar } from '../top-bar/TopBar'; import { TOP_BAR_MIN_HEIGHT, TopBar } from '../top-bar/TopBar';
import { ContentContainer } from './ContentContainer'; import { RightDrawerContainer } from './RightDrawerContainer';
type OwnProps = { type OwnProps = {
children: JSX.Element; children: JSX.Element | JSX.Element[];
title: string; title: string;
icon: ReactNode; icon: ReactNode;
onAddButtonClick?: () => void; onAddButtonClick?: () => void;
@ -26,10 +27,11 @@ export function WithTopBarContainer({
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<TopBarHotkeys onAddButtonClick={onAddButtonClick} />
<TopBar title={title} icon={icon} onAddButtonClick={onAddButtonClick} /> <TopBar title={title} icon={icon} onAddButtonClick={onAddButtonClick} />
<ContentContainer topMargin={TOP_BAR_MIN_HEIGHT + 16 + 16}> <RightDrawerContainer topMargin={TOP_BAR_MIN_HEIGHT + 16 + 16}>
{children} {children}
</ContentContainer> </RightDrawerContainer>
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -0,0 +1,17 @@
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
type OwnProps = {
onAddButtonClick?: () => void;
};
export function TopBarHotkeys({ onAddButtonClick }: OwnProps) {
useScopedHotkeys(
'c',
() => onAddButtonClick?.(),
InternalHotkeysScope.Table,
[onAddButtonClick],
);
return <></>;
}

View File

@ -1,8 +1,6 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
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';
@ -51,8 +49,6 @@ type OwnProps = {
}; };
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) { export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
useScopedHotkeys('c', () => onAddButtonClick?.(), InternalHotkeysScope.Table);
return ( return (
<> <>
<TopBarContainer> <TopBarContainer>

View File

@ -1,4 +1,4 @@
import { useRecoilValue } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope'; import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope';
import { currentHotkeysScopeState } from '@/hotkeys/states/internal/currentHotkeysScopeState'; import { currentHotkeysScopeState } from '@/hotkeys/states/internal/currentHotkeysScopeState';
@ -11,29 +11,39 @@ import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
import { useDisableSoftFocus } from './useDisableSoftFocus'; import { useDisableSoftFocus } from './useDisableSoftFocus';
export function useLeaveTableFocus() { export function useLeaveTableFocus() {
const currentHotkeysScope = useRecoilValue(currentHotkeysScopeState);
const disableSoftFocus = useDisableSoftFocus(); const disableSoftFocus = useDisableSoftFocus();
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode(); const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
const setHotkeysScope = useSetHotkeysScope(); const setHotkeysScope = useSetHotkeysScope();
const isSoftFocusActive = useRecoilValue(isSoftFocusActiveState); return useRecoilCallback(
const isSomeInputInEditMode = useRecoilValue(isSomeInputInEditModeState); ({ snapshot }) =>
() => {
const isSoftFocusActive = snapshot
.getLoadable(isSoftFocusActiveState)
.valueOrThrow();
return async function leaveTableFocus() { const isSomeInputInEditMode = snapshot
// TODO: replace with scope ancestor ? .getLoadable(isSomeInputInEditModeState)
if (!isSoftFocusActive && !isSomeInputInEditMode) { .valueOrThrow();
return;
}
if (currentHotkeysScope?.scope === InternalHotkeysScope.Table) { const currentHotkeysScope = snapshot
return; .getLoadable(currentHotkeysScopeState)
} .valueOrThrow();
closeCurrentCellInEditMode(); if (!isSoftFocusActive && !isSomeInputInEditMode) {
disableSoftFocus(); return;
}
setHotkeysScope(InternalHotkeysScope.Table, { goto: true }); if (currentHotkeysScope?.scope === InternalHotkeysScope.Table) {
}; return;
}
closeCurrentCellInEditMode();
disableSoftFocus();
setHotkeysScope(InternalHotkeysScope.Table, { goto: true });
},
[setHotkeysScope, closeCurrentCellInEditMode, disableSoftFocus],
);
} }

View File

@ -1,12 +1,12 @@
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 { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
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';
import { IconUser } from '@/ui/icons/index'; import { IconBuildingSkyscraper } from '@/ui/icons/index';
import { FlexExpandingContainer } from '@/ui/layout/containers/FlexExpandingContainer';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
import { TableContext } from '@/ui/tables/states/TableContext'; import { TableContext } from '@/ui/tables/states/TableContext';
import { useInsertPersonMutation } from '~/generated/graphql'; import { useInsertPersonMutation } from '~/generated/graphql';
@ -15,12 +15,6 @@ import { TableActionBarButtonCreateCommentThreadPeople } from './table/TableActi
import { TableActionBarButtonDeletePeople } from './table/TableActionBarButtonDeletePeople'; import { TableActionBarButtonDeletePeople } from './table/TableActionBarButtonDeletePeople';
import { PeopleTable } from './PeopleTable'; import { PeopleTable } from './PeopleTable';
const StyledPeopleContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
`;
export function People() { export function People() {
const [insertPersonMutation] = useInsertPersonMutation(); const [insertPersonMutation] = useInsertPersonMutation();
@ -42,20 +36,20 @@ export function People() {
const theme = useTheme(); const theme = useTheme();
return ( return (
<WithTopBarContainer <RecoilScope SpecificContext={TableContext}>
title="People" <WithTopBarContainer
icon={<IconUser size={theme.icon.size.md} />} title="Companies"
onAddButtonClick={handleAddButtonClick} icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
> onAddButtonClick={handleAddButtonClick}
<RecoilScope SpecificContext={TableContext}> >
<StyledPeopleContainer> <FlexExpandingContainer>
<PeopleTable /> <PeopleTable />
</StyledPeopleContainer> </FlexExpandingContainer>
<EntityTableActionBar> <EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadPeople /> <TableActionBarButtonCreateCommentThreadPeople />
<TableActionBarButtonDeletePeople /> <TableActionBarButtonDeletePeople />
</EntityTableActionBar> </EntityTableActionBar>
</RecoilScope> </WithTopBarContainer>
</WithTopBarContainer> </RecoilScope>
); );
} }