Speed up RecordTableCell by 5x (#5023)

Improved table cell performances by putting all hooks from
RecordTableCell in RecordTableContext and RecordTable component, so that
each cell now only subscribes to a reference to those hooks' returned
function.

We couldn't do memoization here since the problem is not to memoize
between re-renders but to share the same function reference between
hundreds of different components, so a context it the fastest way for
this.

I had to refactor the hooks a little bit so that they take as arguments
what was previously taken from the cell's context.
This commit is contained in:
Lucas Bordeau
2024-04-18 10:44:00 +02:00
committed by GitHub
parent 3e60c0050c
commit 86afc34e61
19 changed files with 731 additions and 105 deletions

View File

@ -0,0 +1,57 @@
import { isUndefined } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { computeDraftValueFromFieldValue } from '@/object-record/record-field/utils/computeDraftValueFromFieldValue';
import { computeDraftValueFromString } from '@/object-record/record-field/utils/computeDraftValueFromString';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
export const useInitDraftValueV2 = <FieldValue>() => {
return useRecoilCallback(
({ set, snapshot }) =>
({
value,
entityId,
fieldDefinition,
}: {
value?: string;
entityId: string;
fieldDefinition: FieldDefinition<FieldMetadata>;
}) => {
const recordFieldInputScopeId = `${entityId}-${fieldDefinition?.metadata?.fieldName}-scope`;
const getDraftValueSelector = extractComponentSelector<
FieldInputDraftValue<FieldValue> | undefined
>(recordFieldInputDraftValueComponentSelector, recordFieldInputScopeId);
const recordFieldValue = snapshot
.getLoadable(
recordStoreFamilySelector<FieldValue>({
recordId: entityId,
fieldName: fieldDefinition.metadata.fieldName,
}),
)
.getValue();
if (isUndefined(value)) {
set(
getDraftValueSelector(),
computeDraftValueFromFieldValue<FieldValue>({
fieldValue: recordFieldValue,
fieldDefinition,
}),
);
} else {
set(
getDraftValueSelector(),
computeDraftValueFromString<FieldValue>({ value, fieldDefinition }),
);
}
},
[],
);
};

View File

@ -7,8 +7,20 @@ import { RecordTableBody } from '@/object-record/record-table/components/RecordT
import { RecordTableBodyEffect } from '@/object-record/record-table/components/RecordTableBodyEffect';
import { RecordTableHeader } from '@/object-record/record-table/components/RecordTableHeader';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
useOpenRecordTableCellV2,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu';
import { useUpsertRecordV2 } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecordV2';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { RGBA } from '@/ui/theme/constants/Rgba';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
@ -145,6 +157,59 @@ export const RecordTable = ({
objectNameSingular,
});
const { upsertRecord } = useUpsertRecordV2({
objectNameSingular,
});
const handleUpsertRecord = ({
persistField,
entityId,
fieldName,
}: {
persistField: () => void;
entityId: string;
fieldName: string;
}) => {
upsertRecord(persistField, entityId, fieldName, recordTableId);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
const handleCloseTableCell = () => {
closeTableCell();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerContextMenu } = useTriggerContextMenu({
recordTableId,
});
const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
triggerContextMenu(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
return (
<RecordTableScope
recordTableScopeId={scopeId}
@ -154,6 +219,13 @@ export const RecordTable = ({
<RecordTableContext.Provider
value={{
objectMetadataItem,
onUpsertRecord: handleUpsertRecord,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
onContextMenu: handleContextMenu,
onCellMouseEnter: handleContainerMouseEnter,
}}
>
<StyledTable

View File

@ -12,7 +12,7 @@ import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkey
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const RecordTableCellContainer = () => {
export const RecordTableCellFieldContextWrapper = () => {
const { objectMetadataItem } = useContext(RecordTableContext);
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { RecordTableCellContainer } from '@/object-record/record-table/components/RecordTableCellContainer';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/components/RecordTableCellFieldContextWrapper';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
@ -67,7 +67,7 @@ export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => {
}}
key={column.fieldMetadataId}
>
<RecordTableCellContainer />
<RecordTableCellFieldContextWrapper />
</RecordTableCellContext.Provider>
) : (
<td key={column.fieldMetadataId}></td>

View File

@ -1,9 +1,28 @@
import { createContext } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
type RecordTableContextProps = {
objectMetadataItem: ObjectMetadataItem;
onUpsertRecord: ({
persistField,
entityId,
fieldName,
}: {
persistField: () => void;
entityId: string;
fieldName: string;
}) => void;
onOpenTableCell: (args: OpenTableCellArgs) => void;
onMoveFocus: (direction: MoveFocusDirection) => void;
onCloseTableCell: () => void;
onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
onContextMenu: (event: React.MouseEvent, recordId: string) => void;
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
};
export const RecordTableContext = createContext<RecordTableContextProps>(

View File

@ -0,0 +1,68 @@
import { useRecoilCallback } from 'recoil';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export type HandleContainerMouseEnterArgs = {
isHovered: boolean;
setIsHovered: React.Dispatch<React.SetStateAction<boolean>>;
cellPosition: TableCellPosition;
};
export const useHandleContainerMouseEnter = ({
recordTableId,
}: {
recordTableId: string;
}) => {
const tableScopeId = getScopeIdFromComponentId(recordTableId);
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleContainerMouseEnter = useRecoilCallback(
({ snapshot, set }) =>
({
isHovered,
setIsHovered,
cellPosition,
}: HandleContainerMouseEnterArgs) => {
const currentTableCellInEditModePositionState = extractComponentState(
currentTableCellInEditModePositionComponentState,
tableScopeId,
);
const currentTableCellInEditModePosition = getSnapshotValue(
snapshot,
currentTableCellInEditModePositionState,
);
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
isTableCellInEditModeComponentFamilyState,
tableScopeId,
);
const isSomeCellInEditMode = getSnapshotValue(
snapshot,
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
);
if (!isHovered && !isSomeCellInEditMode) {
setIsHovered(true);
moveSoftFocusToCell(cellPosition);
set(isSoftFocusUsingMouseState, true);
}
},
[tableScopeId, moveSoftFocusToCell],
);
return {
handleContainerMouseEnter,
};
};

View File

@ -1,6 +1,7 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition';
@ -167,6 +168,23 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => {
],
);
const moveFocus = (direction: MoveFocusDirection) => {
switch (direction) {
case 'up':
moveUp();
break;
case 'down':
moveDown();
break;
case 'left':
moveLeft();
break;
case 'right':
moveRight();
break;
}
};
return {
scopeId,
moveDown,
@ -175,5 +193,6 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => {
moveUp,
setSoftFocusPosition,
selectedRowIdsSelector,
moveFocus,
};
};

View File

@ -4,11 +4,9 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer';
import { useCloseRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell';
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
export const RecordTableCell = ({
@ -16,55 +14,76 @@ export const RecordTableCell = ({
}: {
customHotkeyScope: HotkeyScope;
}) => {
const { closeTableCell } = useCloseRecordTableCell();
const { upsertRecord } = useUpsertRecord();
const { moveLeft, moveRight, moveDown } = useRecordTableMoveFocus();
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext);
const { entityId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly } = useContext(RecordTableRowContext);
const handleEnter: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
moveDown();
onCloseTableCell();
onMoveFocus('down');
};
const handleSubmit: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
onCloseTableCell();
};
const handleCancel = () => {
closeTableCell();
onCloseTableCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
onCloseTableCell();
};
const handleEscape: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
onCloseTableCell();
};
const handleTab: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
moveRight();
onCloseTableCell();
onMoveFocus('right');
};
const handleShiftTab: FieldInputEvent = (persistField) => {
upsertRecord(persistField);
onUpsertRecord({
persistField,
entityId,
fieldName: fieldDefinition.metadata.fieldName,
});
closeTableCell();
moveLeft();
onCloseTableCell();
onMoveFocus('left');
};
return (

View File

@ -1,28 +1,26 @@
import { ReactElement, useContext, useState } from 'react';
import React, { ReactElement, useContext, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useGetIsSomeCellInEditModeState } from '@/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode';
import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentTableCellEditMode } from '../hooks/useCurrentTableCellEditMode';
import { useIsSoftFocusOnCurrentTableCell } from '../hooks/useIsSoftFocusOnCurrentTableCell';
import { useMoveSoftFocusToCurrentCellOnHover } from '../hooks/useMoveSoftFocusToCurrentCellOnHover';
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
import { RecordTableCellButton } from './RecordTableCellButton';
import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
@ -64,65 +62,60 @@ export const RecordTableCellContainer = ({
nonEditModeContent,
editHotkeyScope,
}: RecordTableCellContainerProps) => {
const { columnIndex } = useContext(RecordTableCellContext);
const { isReadOnly, isSelected, recordId } = useContext(
RecordTableRowContext,
);
const { onMoveSoftFocusToCell, onContextMenu, onCellMouseEnter } =
useContext(RecordTableContext);
const cellPosition = useCurrentTableCellPosition();
const [isHovered, setIsHovered] = useState(false);
const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
const isSomeCellInEditModeState = useGetIsSomeCellInEditModeState();
const { openTableCell } = useOpenRecordTableCellFromCell();
const setIsSoftFocusUsingMouseState = useSetRecoilState(
isSoftFocusUsingMouseState,
const tableScopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
getScopeIdOrUndefinedFromComponentId(),
);
const moveSoftFocusToCurrentCellOnHover =
useMoveSoftFocusToCurrentCellOnHover();
const isTableCellInEditModeFamilyState = extractComponentFamilyState(
isTableCellInEditModeComponentFamilyState,
tableScopeId,
);
const hasSoftFocus = useIsSoftFocusOnCurrentTableCell();
const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell();
const isSoftFocusOnTableCellFamilyState = extractComponentFamilyState(
isSoftFocusOnTableCellComponentFamilyState,
tableScopeId,
);
const { openTableCell } = useOpenRecordTableCell();
const isCurrentTableCellInEditMode = useRecoilValue(
isTableCellInEditModeFamilyState(cellPosition),
);
const hasSoftFocus = useRecoilValue(
isSoftFocusOnTableCellFamilyState(cellPosition),
);
const isEmpty = useIsFieldEmpty();
const handleButtonClick = () => {
setSoftFocusOnCurrentTableCell();
onMoveSoftFocusToCell(cellPosition);
openTableCell();
};
const { isSelected, isReadOnly } = useContext(RecordTableRowContext);
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setCurrentRowSelected(true);
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
setContextMenuOpenState(true);
onContextMenu(event, recordId);
};
const handleContainerMouseEnter = useRecoilCallback(
({ snapshot }) =>
() => {
const isSomeCellInEditMode = getSnapshotValue(
snapshot,
isSomeCellInEditModeState(),
);
if (!isHovered && !isSomeCellInEditMode) {
setIsHovered(true);
moveSoftFocusToCurrentCellOnHover();
setIsSoftFocusUsingMouseState(true);
}
},
[
const handleContainerMouseEnter = () => {
onCellMouseEnter({
cellPosition,
isHovered,
isSomeCellInEditModeState,
moveSoftFocusToCurrentCellOnHover,
setIsSoftFocusUsingMouseState,
],
);
setIsHovered,
});
};
const handleContainerMouseLeave = () => {
setIsHovered(false);
@ -130,9 +123,6 @@ export const RecordTableCellContainer = ({
const editModeContentOnly = useIsFieldInputOnly();
const isEmpty = useIsFieldEmpty();
const { columnIndex } = useContext(RecordTableCellContext);
const isFirstColumn = columnIndex === 0;
const customButtonIcon = useGetButtonIcon();
const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon;
@ -148,7 +138,7 @@ export const RecordTableCellContainer = ({
return (
<StyledTd
isSelected={isSelected}
onContextMenu={(event) => handleContextMenu(event)}
onContextMenu={handleContextMenu}
isInEditMode={isCurrentTableCellInEditMode}
>
<CellHotkeyScopeContext.Provider
@ -180,9 +170,11 @@ export const RecordTableCellContainer = ({
Icon={buttonIcon}
/>
)}
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
{!isEmpty && (
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
)}
</>
)}
</StyledCellBaseContainer>

View File

@ -1,21 +1,23 @@
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
import { useContext } from 'react';
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
export const RecordTableCellDisplayMode = ({
children,
}: React.PropsWithChildren<unknown>) => {
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentTableCell();
const cellPosition = useCurrentTableCellPosition();
const { onMoveSoftFocusToCell } = useContext(RecordTableContext);
const { openTableCell } = useOpenRecordTableCellFromCell();
const isFieldInputOnly = useIsFieldInputOnly();
const { openTableCell } = useOpenRecordTableCell();
const handleClick = () => {
setSoftFocusOnCurrentCell();
onMoveSoftFocusToCell(cellPosition);
if (!isFieldInputOnly) {
openTableCell();

View File

@ -6,7 +6,7 @@ import { useClearField } from '@/object-record/record-field/hooks/useClearField'
import { useIsFieldClearable } from '@/object-record/record-field/hooks/useIsFieldClearable';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
@ -20,7 +20,7 @@ type RecordTableCellSoftFocusModeProps = PropsWithChildren<unknown>;
export const RecordTableCellSoftFocusMode = ({
children,
}: RecordTableCellSoftFocusModeProps) => {
const { openTableCell } = useOpenRecordTableCell();
const { openTableCell } = useOpenRecordTableCellFromCell();
const isFieldInputOnly = useIsFieldInputOnly();
@ -82,9 +82,7 @@ export const RecordTableCellSoftFocusMode = ({
keyboardEvent.stopPropagation();
keyboardEvent.stopImmediatePropagation();
openTableCell({
initialValue: keyboardEvent.key,
});
openTableCell(keyboardEvent.key);
}
},
TableHotkeyScope.TableSoftFocus,

View File

@ -0,0 +1,38 @@
import { useResetRecoilState } from 'recoil';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const useCloseRecordTableCellV2 = (recordTableScopeId: string) => {
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const { pendingRecordIdState } = useRecordTableStates(recordTableScopeId);
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableScopeId);
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCell = async () => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
};
return {
closeTableCell,
};
};

View File

@ -0,0 +1,59 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useSetSoftFocus } from '@/object-record/record-table/record-table-cell/hooks/useSetSoftFocus';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const useMoveSoftFocusToCellOnHoverV2 = (recordTableId: string) => {
const setSoftFocus = useSetSoftFocus(recordTableId);
const {
currentTableCellInEditModePositionState,
isTableCellInEditModeFamilyState,
} = useRecordTableStates(recordTableId);
const moveSoftFocusToCell = useRecoilCallback(
({ snapshot }) =>
(cellPosition: TableCellPosition) => {
const currentTableCellInEditModePosition = getSnapshotValue(
snapshot,
currentTableCellInEditModePositionState,
);
const isSomeCellInEditMode = snapshot
.getLoadable(
isTableCellInEditModeFamilyState(
currentTableCellInEditModePosition,
),
)
.getValue();
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (
currentHotkeyScope.scope !== TableHotkeyScope.TableSoftFocus &&
currentHotkeyScope.scope !== TableHotkeyScope.CellEditMode &&
currentHotkeyScope.scope !== TableHotkeyScope.Table
) {
return;
}
if (!isSomeCellInEditMode) {
setSoftFocus(cellPosition);
}
},
[
currentTableCellInEditModePositionState,
isTableCellInEditModeFamilyState,
setSoftFocus,
],
);
return { moveSoftFocusToCell };
};

View File

@ -0,0 +1,51 @@
import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export type OpenTableCellArgs = {
initialValue?: string;
cellPosition: TableCellPosition;
isReadOnly: boolean;
pathToShowPage: string;
customCellHotkeyScope: HotkeyScope | null;
fieldDefinition: FieldDefinition<FieldMetadata>;
entityId: string;
};
export const useOpenRecordTableCellFromCell = () => {
const { onOpenTableCell } = useContext(RecordTableContext);
const cellPosition = useCurrentTableCellPosition();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { entityId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly, pathToShowPage } = useContext(RecordTableRowContext);
const openTableCell = (initialValue?: string) => {
onOpenTableCell({
cellPosition,
customCellHotkeyScope,
entityId,
fieldDefinition,
isReadOnly,
pathToShowPage,
initialValue,
});
};
return {
openTableCell,
};
};

View File

@ -0,0 +1,125 @@
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useMoveEditModeToTableCellPosition } from '@/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export type OpenTableCellArgs = {
initialValue?: string;
cellPosition: TableCellPosition;
isReadOnly: boolean;
pathToShowPage: string;
customCellHotkeyScope: HotkeyScope | null;
fieldDefinition: FieldDefinition<FieldMetadata>;
entityId: string;
};
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId);
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const navigate = useNavigate();
const leaveTableFocus = useLeaveTableFocus(tableScopeId);
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const initDraftValue = useInitDraftValueV2();
const openTableCell = useRecoilCallback(
({ snapshot }) =>
({
initialValue,
cellPosition,
isReadOnly,
pathToShowPage,
customCellHotkeyScope,
fieldDefinition,
entityId,
}: OpenTableCellArgs) => {
if (isReadOnly) {
return;
}
const isFirstColumnCell = cellPosition.column === 0;
const fieldValue = getSnapshotValue(
snapshot,
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldDefinition.metadata.fieldName,
}),
);
const isEmpty = isFieldValueEmpty({
fieldDefinition,
fieldValue,
});
if (isFirstColumnCell && !isEmpty) {
leaveTableFocus();
navigate(pathToShowPage);
return;
}
setDragSelectionStartEnabled(false);
moveEditModeToTableCellPosition(cellPosition);
initDraftValue({
value: initialValue,
entityId,
fieldDefinition,
});
toggleClickOutsideListener(false);
if (isDefined(customCellHotkeyScope)) {
setHotkeyScope(
customCellHotkeyScope.scope,
customCellHotkeyScope.customScopes,
);
} else {
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
}
},
[
setDragSelectionStartEnabled,
toggleClickOutsideListener,
leaveTableFocus,
navigate,
setHotkeyScope,
initDraftValue,
moveEditModeToTableCellPosition,
],
);
return {
openTableCell,
};
};

View File

@ -7,10 +7,10 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const useSetSoftFocus = () => {
const setSoftFocusPosition = useSetSoftFocusPosition();
export const useSetSoftFocus = (recordTableId?: string) => {
const setSoftFocusPosition = useSetSoftFocusPosition(recordTableId);
const { isSoftFocusActiveState } = useRecordTableStates();
const { isSoftFocusActiveState } = useRecordTableStates(recordTableId);
const setHotkeyScope = useSetHotkeyScope();

View File

@ -0,0 +1,46 @@
import { useRecoilCallback } from 'recoil';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
export const useTriggerContextMenu = ({
recordTableId,
}: {
recordTableId: string;
}) => {
const triggerContextMenu = useRecoilCallback(
({ set, snapshot }) =>
(event: React.MouseEvent, recordId: string) => {
event.preventDefault();
const tableScopeId = getScopeIdFromComponentId(recordTableId);
set(contextMenuPositionState, {
x: event.clientX,
y: event.clientY,
});
set(contextMenuIsOpenState, true);
const isRowSelectedFamilyState = extractComponentFamilyState(
isRowSelectedComponentFamilyState,
tableScopeId,
);
const isRowSelected = getSnapshotValue(
snapshot,
isRowSelectedFamilyState(recordId),
);
if (isRowSelected !== true) {
set(isRowSelectedFamilyState(recordId), true);
}
},
[recordTableId],
);
return { triggerContextMenu };
};

View File

@ -0,0 +1,60 @@
import { useRecoilCallback } from 'recoil';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { isDefined } from '~/utils/isDefined';
export const useUpsertRecordV2 = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
});
const upsertRecord = useRecoilCallback(
({ snapshot }) =>
(
persistField: () => void,
entityId: string,
fieldName: string,
tableScopeId: string,
) => {
const recordTablePendingRecordIdState = extractComponentState(
recordTablePendingRecordIdComponentState,
tableScopeId,
);
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdState,
);
const fieldScopeId = `${entityId}-${fieldName}`;
const draftValueSelector = extractComponentSelector(
recordFieldInputDraftValueComponentSelector,
fieldScopeId,
);
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
createOneRecord({
id: recordTablePendingRecordId,
name: draftValue,
position: 'first',
});
} else if (!recordTablePendingRecordId) {
persistField();
}
},
[createOneRecord],
);
return { upsertRecord };
};

View File

@ -0,0 +1 @@
export type MoveFocusDirection = 'up' | 'down' | 'left' | 'right';