Record Table Row Navigation (#11879)
# Record Table Row Navigation This PR improves the table accessibility by adding a row navigation and new shortcuts to the table. Closes #896. # Introduce focused active row states on the table This PR implements the focused and active row states feature for the record table, allowing users to navigate through the table with keyboard arrows and providing visual feedback for selection. ## Implementation details: - Added new component states to track focused and active row positions and states. - Implemented dedicated hooks for row state management - Updated UI styling for active and focused rows: - Applied blue border (Adaptive Colors Blue 3) - Added highlight background (Accent Quaternary) - Added styling for focused rows to clearly indicate selection state - Added row state cleanup: - `RecordTableDeactivateRecordTableRowEffect` component to reset states - Added row state reset logic upon navigation ## Bug fixes - Fixed record table unselection in the page change effect - Fixed a hack introduced by https://github.com/twentyhq/twenty/pull/8489 which duplicated the last table column # Shortcuts ## Arrow keys and J+K navigation https://github.com/user-attachments/assets/6b46f6b5-cd98-4053-aaef-f8bf2b9584b5 ## Record selection with X https://github.com/user-attachments/assets/44ab7397-e00c-4dfe-8dd1-b3ffc53b3e5f ## Enter allows for cell navigation, Escape goes back to row navigation https://github.com/user-attachments/assets/890d7e25-2d81-47e3-972f-043a1879b8cc ## Command + Enter opens the record https://github.com/user-attachments/assets/cf8cdbd5-7cf0-4d78-909f-dc6be88b9e25
This commit is contained in:
@ -21,11 +21,10 @@ import { useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { IconBrowserMaximize } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
@ -15,21 +15,26 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnalyticsType } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
||||
import { AnalyticsType } from '~/generated/graphql';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
|
||||
// TODO: break down into smaller functions and / or hooks
|
||||
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
||||
export const PageChangeEffect = () => {
|
||||
@ -54,7 +59,19 @@ export const PageChangeEffect = () => {
|
||||
const objectNamePlural =
|
||||
useParams().objectNamePlural ?? CoreObjectNamePlural.Person;
|
||||
|
||||
const resetTableSelections = useResetTableRowSelection(objectNamePlural);
|
||||
const contextStoreCurrentViewId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectNamePlural,
|
||||
contextStoreCurrentViewId || '',
|
||||
);
|
||||
|
||||
const resetTableSelections = useResetTableRowSelection(recordIndexId);
|
||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
|
||||
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
|
||||
|
||||
const { executeTasksOnAnyLocationChange } =
|
||||
useExecuteTasksOnAnyLocationChange();
|
||||
@ -84,8 +101,16 @@ export const PageChangeEffect = () => {
|
||||
|
||||
if (isLeavingRecordIndexPage) {
|
||||
resetTableSelections();
|
||||
unfocusRecordTableRow();
|
||||
deactivateRecordTableRow();
|
||||
}
|
||||
}, [isMatchingLocation, previousLocation, resetTableSelections]);
|
||||
}, [
|
||||
isMatchingLocation,
|
||||
previousLocation,
|
||||
resetTableSelections,
|
||||
unfocusRecordTableRow,
|
||||
deactivateRecordTableRow,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
switch (true) {
|
||||
|
||||
@ -68,6 +68,7 @@ export const RecordChip = ({
|
||||
|
||||
const isSidePanelViewOpenRecordInType =
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL;
|
||||
|
||||
const onClick = isSidePanelViewOpenRecordInType
|
||||
? () =>
|
||||
openRecordInCommandMenu({
|
||||
|
||||
@ -6,12 +6,14 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
|
||||
import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper';
|
||||
import { RecordTableContent } from '@/object-record/record-table/components/RecordTableContent';
|
||||
import { RecordTableEmpty } from '@/object-record/record-table/components/RecordTableEmpty';
|
||||
import { RecordTableScrollToFocusedElementEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedElementEffect';
|
||||
import { RecordTableScrollToFocusedCellEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedCellEffect';
|
||||
import { RecordTableScrollToFocusedRowEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedRowEffect';
|
||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
@ -46,8 +48,13 @@ export const RecordTable = () => {
|
||||
const recordTableIsEmpty =
|
||||
!isRecordTableInitialLoading && allRecordIds.length === 0;
|
||||
|
||||
const isRecordTableFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableFocusActiveComponentState,
|
||||
const isRecordTableCellFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const isRecordTableRowFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
@ -71,7 +78,9 @@ export const RecordTable = () => {
|
||||
tableBodyRef={tableBodyRef}
|
||||
/>
|
||||
|
||||
{isRecordTableFocusActive && <RecordTableScrollToFocusedElementEffect />}
|
||||
{isRecordTableCellFocusActive && <RecordTableScrollToFocusedCellEffect />}
|
||||
|
||||
{isRecordTableRowFocusActive && <RecordTableScrollToFocusedRowEffect />}
|
||||
|
||||
{recordTableIsEmpty && !hasRecordGroups ? (
|
||||
<RecordTableEmpty
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { RecordTableDeactivateRecordTableRowEffect } from '@/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect';
|
||||
import { RecordTableBodyEscapeHotkeyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect';
|
||||
import { RecordTableBodyFocusClickOutsideEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect';
|
||||
import { RecordTableBodyFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect';
|
||||
import { RecordTableBodyRowFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect';
|
||||
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
|
||||
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
|
||||
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
|
||||
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export interface RecordTableBodyEffectsWrapperProps {
|
||||
@ -16,12 +17,8 @@ export const RecordTableBodyEffectsWrapper = ({
|
||||
hasRecordGroups,
|
||||
tableBodyRef,
|
||||
}: RecordTableBodyEffectsWrapperProps) => {
|
||||
const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
|
||||
isAtLeastOneTableRowSelectedSelector,
|
||||
);
|
||||
|
||||
const isRecordTableFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableFocusActiveComponentState,
|
||||
const isRecordTableRowFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
@ -31,9 +28,11 @@ export const RecordTableBodyEffectsWrapper = ({
|
||||
) : (
|
||||
<RecordTableNoRecordGroupBodyEffect />
|
||||
)}
|
||||
{isAtLeastOneRecordSelected && <RecordTableBodyEscapeHotkeyEffect />}
|
||||
{isRecordTableFocusActive && <RecordTableBodyFocusKeyboardEffect />}
|
||||
<RecordTableBodyEscapeHotkeyEffect />
|
||||
<RecordTableBodyFocusKeyboardEffect />
|
||||
{isRecordTableRowFocusActive && <RecordTableBodyRowFocusKeyboardEffect />}
|
||||
<RecordTableBodyFocusClickOutsideEffect tableBodyRef={tableBodyRef} />
|
||||
<RecordTableDeactivateRecordTableRowEffect />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
|
||||
|
||||
export const RecordTableDeactivateRecordTableRowEffect = () => {
|
||||
const { deactivateRecordTableRow } = useActiveRecordTableRow();
|
||||
|
||||
useListenRightDrawerClose(() => {
|
||||
deactivateRecordTableRow();
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
|
||||
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
|
||||
import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell';
|
||||
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
|
||||
import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell';
|
||||
import {
|
||||
@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
|
||||
openTableCell(args);
|
||||
};
|
||||
|
||||
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
|
||||
const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId);
|
||||
|
||||
const handleMoveFocus = (direction: MoveFocusDirection) => {
|
||||
moveFocus(direction);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
|
||||
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
|
||||
import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove';
|
||||
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
|
||||
import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell';
|
||||
import {
|
||||
@ -29,10 +29,10 @@ export const RecordTableRecordGroupBodyContextProvider = ({
|
||||
openTableCell(args);
|
||||
};
|
||||
|
||||
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
|
||||
const { move } = useRecordTableMove(recordTableId);
|
||||
|
||||
const handleMoveFocus = (direction: MoveFocusDirection) => {
|
||||
moveFocus(direction);
|
||||
move(direction);
|
||||
};
|
||||
|
||||
const { closeTableCellInGroup } = useCloseRecordTableCellInGroup();
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableScrollToFocusedElementEffect = () => {
|
||||
export const RecordTableScrollToFocusedCellEffect = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const focusPosition = useRecoilComponentValueV2(
|
||||
recordTableFocusPositionComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
// Handle cell focus
|
||||
useEffect(() => {
|
||||
if (!focusPosition) {
|
||||
return;
|
||||
@ -36,11 +41,15 @@ export const RecordTableScrollToFocusedElementEffect = () => {
|
||||
}
|
||||
}
|
||||
|
||||
focusElement.style.scrollMarginTop = '32px';
|
||||
focusElement.style.scrollMarginBottom = '32px';
|
||||
|
||||
focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
return () => {
|
||||
if (isDefined(focusElement)) {
|
||||
focusElement.style.scrollMarginLeft = '';
|
||||
focusElement.style.scrollMarginBottom = '';
|
||||
}
|
||||
};
|
||||
}, [focusPosition]);
|
||||
@ -0,0 +1,63 @@
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableScrollToFocusedRowEffect = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const focusedRowIndex = useRecoilComponentValueV2(
|
||||
focusedRecordTableRowIndexComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const isRowFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const allRecordIds = useRecoilComponentValueV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isRowFocusActive ||
|
||||
!isDefined(focusedRowIndex) ||
|
||||
!allRecordIds?.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recordId = allRecordIds[focusedRowIndex];
|
||||
|
||||
if (!recordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusElement = document.getElementById(
|
||||
`record-table-cell-0-${focusedRowIndex}`,
|
||||
);
|
||||
|
||||
if (!focusElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusElement.style.scrollMarginBottom = '32px';
|
||||
focusElement.style.scrollMarginTop = '32px';
|
||||
|
||||
focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
return () => {
|
||||
if (isDefined(focusElement)) {
|
||||
focusElement.style.scrollMarginBottom = '';
|
||||
}
|
||||
};
|
||||
}, [focusedRowIndex, isRowFocusActive, allRecordIds]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,7 +1,11 @@
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
@ -24,11 +28,27 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
|
||||
recordTableIdFromContext,
|
||||
);
|
||||
|
||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(
|
||||
recordTableIdFromContext,
|
||||
);
|
||||
|
||||
const { deactivateRecordTableRow } = useActiveRecordTableRow(
|
||||
recordTableIdFromContext,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return () => {
|
||||
resetTableRowSelection();
|
||||
|
||||
setIsFocusActiveForCurrentPosition(false);
|
||||
|
||||
unfocusRecordTableRow();
|
||||
|
||||
deactivateRecordTableRow();
|
||||
|
||||
setRecordTableHoverPosition(null);
|
||||
|
||||
setHotkeyScope(TableHotkeyScope.Table);
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { activeRecordTableRowIndexComponentState } from '@/object-record/record-table/states/activeRecordTableRowIndexComponentState';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useActiveRecordTableRow = (recordTableId?: string) => {
|
||||
const isRowActiveState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const activeRowIndexState = useRecoilComponentCallbackStateV2(
|
||||
activeRecordTableRowIndexComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const deactivateRecordTableRow = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const activeRowIndex = snapshot
|
||||
.getLoadable(activeRowIndexState)
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(activeRowIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(activeRowIndexState, null);
|
||||
|
||||
set(isRowActiveState(activeRowIndex), false);
|
||||
},
|
||||
[activeRowIndexState, isRowActiveState],
|
||||
);
|
||||
|
||||
const activateRecordTableRow = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(rowIndex: number) => {
|
||||
const activeRowIndex = snapshot
|
||||
.getLoadable(activeRowIndexState)
|
||||
.getValue();
|
||||
|
||||
if (activeRowIndex === rowIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(activeRowIndex)) {
|
||||
set(isRowActiveState(activeRowIndex), false);
|
||||
}
|
||||
|
||||
set(activeRowIndexState, rowIndex);
|
||||
|
||||
set(isRowActiveState(rowIndex), true);
|
||||
},
|
||||
[activeRowIndexState, isRowActiveState],
|
||||
);
|
||||
|
||||
return {
|
||||
activateRecordTableRow,
|
||||
deactivateRecordTableRow,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,110 @@
|
||||
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useFocusedRecordTableRow = (recordTableId?: string) => {
|
||||
const isRowFocusedState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const focusedRowIndexState = useRecoilComponentCallbackStateV2(
|
||||
focusedRecordTableRowIndexComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const isRowFocusActiveState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const focusedCellPositionState = useRecoilComponentCallbackStateV2(
|
||||
recordTableFocusPositionComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const isRecordTableCellFocusActiveState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const unfocusRecordTableRow = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const focusedRowIndex = snapshot
|
||||
.getLoadable(focusedRowIndexState)
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(focusedRowIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(focusedRowIndexState, null);
|
||||
set(isRowFocusedState(focusedRowIndex), false);
|
||||
set(isRowFocusActiveState, false);
|
||||
},
|
||||
[focusedRowIndexState, isRowFocusedState, isRowFocusActiveState],
|
||||
);
|
||||
|
||||
const focusRecordTableRow = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(rowIndex: number) => {
|
||||
const focusedRowIndex = snapshot
|
||||
.getLoadable(focusedRowIndexState)
|
||||
.getValue();
|
||||
|
||||
if (isDefined(focusedRowIndex) && focusedRowIndex !== rowIndex) {
|
||||
set(isRowFocusedState(focusedRowIndex), false);
|
||||
}
|
||||
|
||||
set(focusedRowIndexState, rowIndex);
|
||||
set(isRowFocusedState(rowIndex), true);
|
||||
set(isRowFocusActiveState, true);
|
||||
},
|
||||
[focusedRowIndexState, isRowFocusedState, isRowFocusActiveState],
|
||||
);
|
||||
|
||||
const restoreRecordTableRowFocusFromCellPosition = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const focusedRowIndex = snapshot
|
||||
.getLoadable(focusedRowIndexState)
|
||||
.getValue();
|
||||
|
||||
const focusedCellPosition = snapshot
|
||||
.getLoadable(focusedCellPositionState)
|
||||
.getValue();
|
||||
|
||||
const isRecordTableCellFocusActive = snapshot
|
||||
.getLoadable(isRecordTableCellFocusActiveState)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
!isDefined(focusedRowIndex) ||
|
||||
!isDefined(focusedCellPosition) ||
|
||||
!isRecordTableCellFocusActive
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusRecordTableRow(focusedCellPosition.row);
|
||||
},
|
||||
[
|
||||
focusedRowIndexState,
|
||||
focusedCellPositionState,
|
||||
isRecordTableCellFocusActiveState,
|
||||
focusRecordTableRow,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
focusRecordTableRow,
|
||||
unfocusRecordTableRow,
|
||||
restoreRecordTableRowFocusFromCellPosition,
|
||||
};
|
||||
};
|
||||
@ -3,9 +3,7 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useSetHasUserSelectedAllRows } from '@/object-record/record-table/hooks/internal/useSetAllRowSelectedState';
|
||||
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
@ -13,15 +11,16 @@ import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
||||
import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
|
||||
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
|
||||
|
||||
import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove';
|
||||
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
||||
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
@ -31,7 +30,6 @@ import { useSelectAllRows } from './internal/useSelectAllRows';
|
||||
import { useSetRecordTableData } from './internal/useSetRecordTableData';
|
||||
import { useSetRecordTableFocusPosition } from './internal/useSetRecordTableFocusPosition';
|
||||
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
|
||||
|
||||
type useRecordTableProps = {
|
||||
recordTableId?: string;
|
||||
};
|
||||
@ -141,62 +139,65 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
|
||||
const setFocusPosition = useSetRecordTableFocusPosition(recordTableId);
|
||||
|
||||
const { setIsFocusActiveForCurrentPosition } =
|
||||
useSetIsRecordTableFocusActive(recordTableId);
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
|
||||
const { moveDown, moveLeft, moveRight, moveUp } =
|
||||
useRecordTableMoveFocus(recordTableId);
|
||||
const { move } = useRecordTableMove(recordTableId);
|
||||
|
||||
const useMapKeyboardToFocus = () => {
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
|
||||
() => {
|
||||
moveUp();
|
||||
move('up');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveUp],
|
||||
[move],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowDown,
|
||||
() => {
|
||||
moveDown();
|
||||
move('down');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveDown],
|
||||
[move],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, 'k'],
|
||||
() => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||
move('up');
|
||||
},
|
||||
TableHotkeyScope.Table,
|
||||
[move],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowDown, 'j'],
|
||||
() => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||
move('down');
|
||||
},
|
||||
TableHotkeyScope.Table,
|
||||
[move],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
|
||||
() => {
|
||||
moveLeft();
|
||||
move('left');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveLeft],
|
||||
[move],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowRight, Key.Tab],
|
||||
() => {
|
||||
moveRight();
|
||||
move('right');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveRight],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
setHotkeyScope(TableHotkeyScope.Table, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
setIsFocusActiveForCurrentPosition(false);
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[setIsFocusActiveForCurrentPosition],
|
||||
[move],
|
||||
);
|
||||
};
|
||||
|
||||
@ -211,10 +212,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
setRowSelected,
|
||||
resetTableRowSelection,
|
||||
upsertRecordTableItem,
|
||||
moveDown,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
move,
|
||||
useMapKeyboardToFocus,
|
||||
selectAllRows,
|
||||
setOnColumnsChange,
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell';
|
||||
import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRecordTableMove = (recordTableId?: string) => {
|
||||
const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId);
|
||||
|
||||
const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId);
|
||||
|
||||
const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const move = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(direction: MoveFocusDirection) => {
|
||||
const isRecordTableFocusActive = getSnapshotValue(
|
||||
snapshot,
|
||||
isRecordTableFocusActiveState,
|
||||
);
|
||||
|
||||
if (isRecordTableFocusActive) {
|
||||
moveFocus(direction);
|
||||
} else {
|
||||
moveFocusedRow(direction);
|
||||
}
|
||||
},
|
||||
[isRecordTableFocusActiveState, moveFocusedRow, moveFocus],
|
||||
);
|
||||
|
||||
return {
|
||||
move,
|
||||
};
|
||||
};
|
||||
@ -9,7 +9,7 @@ import { recordTableFocusPositionComponentState } from '@/object-record/record-t
|
||||
import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
|
||||
export const useRecordTableMoveFocus = (recordTableId?: string) => {
|
||||
export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
|
||||
const setFocusPosition = useSetRecordTableFocusPosition(recordTableId);
|
||||
|
||||
const focusPositionState = useRecoilComponentCallbackStateV2(
|
||||
@ -0,0 +1,93 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
|
||||
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordTableMoveFocusedRow = (recordTableId?: string) => {
|
||||
const { focusRecordTableRow } = useFocusedRecordTableRow(recordTableId);
|
||||
|
||||
const focusedRowIndexState = useRecoilComponentCallbackStateV2(
|
||||
focusedRecordTableRowIndexComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackStateV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const moveFocusedRowUp = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const focusedRowIndex = getSnapshotValue(
|
||||
snapshot,
|
||||
focusedRowIndexState,
|
||||
);
|
||||
|
||||
if (!isDefined(focusedRowIndex)) {
|
||||
focusRecordTableRow(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let newRowIndex = focusedRowIndex - 1;
|
||||
|
||||
if (newRowIndex < 0) {
|
||||
newRowIndex = 0;
|
||||
}
|
||||
|
||||
focusRecordTableRow(newRowIndex);
|
||||
},
|
||||
[focusedRowIndexState, focusRecordTableRow],
|
||||
);
|
||||
|
||||
const moveFocusedRowDown = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const allRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
);
|
||||
const focusedRowIndex = getSnapshotValue(
|
||||
snapshot,
|
||||
focusedRowIndexState,
|
||||
);
|
||||
|
||||
if (!isDefined(focusedRowIndex)) {
|
||||
focusRecordTableRow(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let newRowIndex = focusedRowIndex + 1;
|
||||
|
||||
if (newRowIndex >= allRecordIds.length) {
|
||||
newRowIndex = allRecordIds.length - 1;
|
||||
}
|
||||
|
||||
focusRecordTableRow(newRowIndex);
|
||||
},
|
||||
[
|
||||
recordIndexAllRecordIdsSelector,
|
||||
focusedRowIndexState,
|
||||
focusRecordTableRow,
|
||||
],
|
||||
);
|
||||
|
||||
const moveFocusedRow = (direction: MoveFocusDirection) => {
|
||||
if (direction === 'up') {
|
||||
moveFocusedRowUp();
|
||||
} else if (direction === 'down') {
|
||||
moveFocusedRowDown();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
moveFocusedRowUp,
|
||||
moveFocusedRowDown,
|
||||
moveFocusedRow,
|
||||
};
|
||||
};
|
||||
@ -6,20 +6,20 @@ const StyledTbody = styled.tbody`
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 6;
|
||||
transition: 0.3s ease;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
td:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 11px;
|
||||
z-index: 6;
|
||||
transition: 0.3s ease;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
tr:not(:last-child) td:nth-of-type(3) {
|
||||
// Last row is aggregate footer
|
||||
position: sticky;
|
||||
left: 43px;
|
||||
z-index: 6;
|
||||
transition: 0.3s ease;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:not(.disable-shadow)::after {
|
||||
content: '';
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
export const RecordTableBodyEscapeHotkeyEffect = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
@ -12,12 +14,22 @@ export const RecordTableBodyEscapeHotkeyEffect = () => {
|
||||
recordTableId,
|
||||
});
|
||||
|
||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordTableId);
|
||||
|
||||
const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
|
||||
isAtLeastOneTableRowSelectedSelector,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
resetTableRowSelection();
|
||||
unfocusRecordTableRow();
|
||||
if (isAtLeastOneRecordSelected) {
|
||||
resetTableRowSelection();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.Table,
|
||||
[isAtLeastOneRecordSelected, resetTableRowSelection, unfocusRecordTableRow],
|
||||
);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const RecordTableBodyFocusKeyboardEffect = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
@ -8,7 +16,41 @@ export const RecordTableBodyFocusKeyboardEffect = () => {
|
||||
recordTableId,
|
||||
});
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const { restoreRecordTableRowFocusFromCellPosition } =
|
||||
useFocusedRecordTableRow(recordTableId);
|
||||
|
||||
const { setIsFocusActiveForCurrentPosition } =
|
||||
useSetIsRecordTableFocusActive(recordTableId);
|
||||
|
||||
const isRecordTableFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
);
|
||||
|
||||
useMapKeyboardToFocus();
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
if (isRecordTableFocusActive) {
|
||||
restoreRecordTableRowFocusFromCellPosition();
|
||||
setIsFocusActiveForCurrentPosition(false);
|
||||
} else {
|
||||
setHotkeyScope(TableHotkeyScope.Table, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[
|
||||
setIsFocusActiveForCurrentPosition,
|
||||
restoreRecordTableRowFocusFromCellPosition,
|
||||
setHotkeyScope,
|
||||
isRecordTableFocusActive,
|
||||
],
|
||||
);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const RecordTableBodyRowFocusKeyboardEffect = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, 'k'],
|
||||
() => {
|
||||
moveFocusedRow('up');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveFocusedRow],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowDown, 'j'],
|
||||
() => {
|
||||
moveFocusedRow('down');
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
[moveFocusedRow],
|
||||
);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -17,6 +17,10 @@ const StyledContainer = styled.div`
|
||||
min-width: ${TABLE_CELL_CHECKBOX_MIN_WIDTH};
|
||||
`;
|
||||
|
||||
const StyledRecordTableTd = styled(RecordTableTd)`
|
||||
border-left: 1px solid transparent;
|
||||
`;
|
||||
|
||||
export const RecordTableCellCheckbox = () => {
|
||||
const { isSelected } = useRecordTableRowContextOrThrow();
|
||||
|
||||
@ -27,10 +31,10 @@ export const RecordTableCellCheckbox = () => {
|
||||
}, [isSelected, setCurrentRowSelected]);
|
||||
|
||||
return (
|
||||
<RecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||
<StyledRecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||
<StyledContainer onClick={handleClick}>
|
||||
<Checkbox hoverable checked={isSelected} />
|
||||
</StyledContainer>
|
||||
</RecordTableTd>
|
||||
</StyledRecordTableTd>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,20 +6,25 @@ import styled from '@emotion/styled';
|
||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellDisplayMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode';
|
||||
import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton';
|
||||
import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode';
|
||||
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useContext } from 'react';
|
||||
import { BORDER_COMMON } from 'twenty-ui/theme';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledRecordTableCellHoveredPortalContent = styled.div<{
|
||||
isReadOnly: boolean;
|
||||
isRowActive: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
background-color: ${({ theme, isRowActive }) =>
|
||||
isRowActive ? theme.accent.quaternary : theme.background.primary};
|
||||
border-radius: ${({ isReadOnly }) =>
|
||||
!isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
|
||||
box-sizing: border-box;
|
||||
@ -28,10 +33,12 @@ const StyledRecordTableCellHoveredPortalContent = styled.div<{
|
||||
|
||||
height: 32px;
|
||||
|
||||
outline: ${({ theme, isReadOnly }) =>
|
||||
isReadOnly
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: `1px solid ${theme.font.color.extraLight}`};
|
||||
outline: ${({ theme, isReadOnly, isRowActive }) =>
|
||||
isRowActive
|
||||
? 'none'
|
||||
: isReadOnly
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: `1px solid ${theme.font.color.extraLight}`};
|
||||
|
||||
position: relative;
|
||||
user-select: none;
|
||||
@ -53,8 +60,18 @@ const RecordTableCellHoveredPortalContent = () => {
|
||||
const showButton =
|
||||
!isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn);
|
||||
|
||||
const { rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const isRowActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
rowIndex,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRecordTableCellHoveredPortalContent isReadOnly={isReadOnly}>
|
||||
<StyledRecordTableCellHoveredPortalContent
|
||||
isReadOnly={isReadOnly}
|
||||
isRowActive={isRowActive}
|
||||
>
|
||||
{isFieldInputOnly ? (
|
||||
<RecordTableCellEditMode>
|
||||
<RecordTableCellFieldInput />
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableCellEditModePortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal';
|
||||
import { RecordTableCellHoveredPortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal';
|
||||
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const RecordTableCellPortals = () => {
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const isRecordTableFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableFocusActiveComponentState,
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
|
||||
@ -4,10 +4,5 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co
|
||||
export const RecordTableLastEmptyCell = () => {
|
||||
const { isSelected } = useRecordTableRowContextOrThrow();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordTableTd isSelected={isSelected} hasRightBorder={false} />
|
||||
<RecordTableTd isSelected={isSelected} hasRightBorder={false} />
|
||||
</>
|
||||
);
|
||||
return <RecordTableTd isSelected={isSelected} hasRightBorder={false} />;
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ const StyledTd = styled.td<{
|
||||
hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'};
|
||||
|
||||
padding: 0;
|
||||
transition: 0.3s ease;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
text-align: left;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
|
||||
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
||||
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
|
||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
|
||||
@ -19,7 +19,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
isRecordTableFocusActiveComponentState.atomFamily({
|
||||
isRecordTableCellFocusActiveComponentState.atomFamily({
|
||||
instanceId: 'test-table-id',
|
||||
}),
|
||||
false,
|
||||
@ -50,7 +50,7 @@ const renderHooks = () => {
|
||||
const { setIsFocusActive, setIsFocusActiveForCurrentPosition } =
|
||||
useSetIsRecordTableFocusActive('test-table-id');
|
||||
const isRecordTableFocusActive = useRecoilValue(
|
||||
isRecordTableFocusActiveComponentState.atomFamily({
|
||||
isRecordTableCellFocusActiveComponentState.atomFamily({
|
||||
instanceId: 'test-table-id',
|
||||
}),
|
||||
);
|
||||
@ -99,7 +99,7 @@ describe('useSetIsRecordTableFocusActive', () => {
|
||||
expect(result.current.focusPosition).toEqual(cellPosition);
|
||||
});
|
||||
|
||||
it('should remove focus-active class when focus is deactivated and update isRecordTableFocusActiveComponentState', () => {
|
||||
it('should remove focus-active class when focus is deactivated and update isRecordTableCellFocusActiveComponentState', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const cellPosition: TableCellPosition = { column: 1, row: 0 };
|
||||
|
||||
@ -9,7 +9,6 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
@ -36,8 +35,6 @@ export const useOpenRecordTableCellFromCell = () => {
|
||||
|
||||
const { cellPosition } = useContext(RecordTableCellContext);
|
||||
|
||||
const setFocusPosition = useSetRecordTableFocusPosition();
|
||||
|
||||
const openTableCell = (
|
||||
initialValue?: string,
|
||||
isActionButtonClick = false,
|
||||
@ -54,8 +51,6 @@ export const useOpenRecordTableCellFromCell = () => {
|
||||
isActionButtonClick,
|
||||
isNavigating,
|
||||
});
|
||||
|
||||
setFocusPosition(cellPosition);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -25,6 +25,10 @@ import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropd
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
|
||||
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
@ -82,6 +86,18 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
|
||||
const { openFieldInput } = useOpenFieldInputEditMode();
|
||||
|
||||
const { activateRecordTableRow, deactivateRecordTableRow } =
|
||||
useActiveRecordTableRow(tableScopeId);
|
||||
|
||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(tableScopeId);
|
||||
|
||||
const setIsRowFocusActive = useSetRecoilComponentStateV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const setFocusPosition = useSetRecordTableFocusPosition();
|
||||
|
||||
const openTableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
@ -134,6 +150,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
activateRecordTableRow(cellPosition.row);
|
||||
unfocusRecordTableRow();
|
||||
}
|
||||
|
||||
return;
|
||||
@ -147,6 +166,12 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
deactivateRecordTableRow();
|
||||
|
||||
setFocusPosition(cellPosition);
|
||||
|
||||
setIsRowFocusActive(false);
|
||||
|
||||
setDragSelectionStartEnabled(false);
|
||||
|
||||
openFieldInput({
|
||||
@ -179,6 +204,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
},
|
||||
[
|
||||
clickOutsideListenerIsActivatedState,
|
||||
deactivateRecordTableRow,
|
||||
setFocusPosition,
|
||||
setIsRowFocusActive,
|
||||
setDragSelectionStartEnabled,
|
||||
openFieldInput,
|
||||
setCurrentTableCellInEditModePosition,
|
||||
@ -189,6 +217,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
navigate,
|
||||
indexIdentifierUrl,
|
||||
openRecordInCommandMenu,
|
||||
activateRecordTableRow,
|
||||
unfocusRecordTableRow,
|
||||
setViewableRecordId,
|
||||
setViewableRecordNameSingular,
|
||||
],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
|
||||
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
|
||||
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
|
||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
@ -6,7 +6,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useSetIsRecordTableFocusActive = (recordTableId?: string) => {
|
||||
const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2(
|
||||
isRecordTableFocusActiveComponentState,
|
||||
isRecordTableCellFocusActiveComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
@ -17,23 +19,27 @@ import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPoin
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 104;
|
||||
|
||||
const StyledColumnHeaderCell = styled.th<{
|
||||
columnWidth: number;
|
||||
isResizing?: boolean;
|
||||
isFirstRowActiveOrFocused: boolean;
|
||||
}>`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
|
||||
isFirstRowActiveOrFocused
|
||||
? 'none'
|
||||
: `1px solid ${theme.border.color.light}`};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
transition: 0.3s ease;
|
||||
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
@ -215,6 +221,18 @@ export const RecordTableHeaderCell = ({
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
|
||||
|
||||
return (
|
||||
<StyledColumnHeaderCell
|
||||
key={column.fieldMetadataId}
|
||||
@ -227,6 +245,7 @@ export const RecordTableHeaderCell = ({
|
||||
)}
|
||||
onMouseEnter={() => setIconVisibility(true)}
|
||||
onMouseLeave={() => setIconVisibility(false)}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
>
|
||||
<StyledColumnHeadContainer>
|
||||
<RecordTableColumnHeadWithDropdown column={column} />
|
||||
|
||||
@ -4,7 +4,10 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Checkbox } from 'twenty-ui/input';
|
||||
|
||||
@ -16,9 +19,14 @@ const StyledContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
`;
|
||||
|
||||
const StyledColumnHeaderCell = styled.th`
|
||||
const StyledColumnHeaderCell = styled.th<{
|
||||
isFirstRowActiveOrFocused: boolean;
|
||||
}>`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
|
||||
isFirstRowActiveOrFocused
|
||||
? 'none'
|
||||
: `1px solid ${theme.border.color.light}`};
|
||||
border-right: transparent;
|
||||
width: 30px;
|
||||
`;
|
||||
@ -58,8 +66,22 @@ export const RecordTableHeaderCheckboxColumn = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
|
||||
|
||||
return (
|
||||
<StyledColumnHeaderCell>
|
||||
<StyledColumnHeaderCell
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
>
|
||||
<StyledContainer>
|
||||
<Checkbox
|
||||
hoverable
|
||||
|
||||
@ -2,49 +2,50 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId';
|
||||
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
|
||||
const StyledPlusIconHeaderCell = styled.th<{
|
||||
isTableWiderThanScreen: boolean;
|
||||
isFirstRowActiveOrFocused: boolean;
|
||||
}>`
|
||||
${({ theme }) => {
|
||||
return `
|
||||
&:hover {
|
||||
background: ${theme.background.transparent.light};
|
||||
};
|
||||
`;
|
||||
}};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
|
||||
isFirstRowActiveOrFocused
|
||||
? 'none'
|
||||
: `1px solid ${theme.border.color.light}`};
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-left: none !important;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
border-right: none !important;
|
||||
width: 32px;
|
||||
cursor: default;
|
||||
|
||||
${({ isTableWiderThanScreen, theme }) =>
|
||||
isTableWiderThanScreen
|
||||
? `
|
||||
background-color: ${theme.background.primary};
|
||||
width: 32px;
|
||||
`
|
||||
: ''};
|
||||
: 'width: 100%'};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledEmptyHeaderCell = styled.th`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPlusIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDropdownContainer = styled.div`
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
|
||||
@ -59,9 +60,24 @@ export const RecordTableHeaderLastColumn = () => {
|
||||
(scrollWrapperHTMLElement?.clientWidth ?? 0) <
|
||||
(scrollWrapperHTMLElement?.scrollWidth ?? 0);
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledPlusIconHeaderCell isTableWiderThanScreen={isTableWiderThanScreen}>
|
||||
<StyledPlusIconHeaderCell
|
||||
isTableWiderThanScreen={isTableWiderThanScreen}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
>
|
||||
<StyledDropdownContainer>
|
||||
<Dropdown
|
||||
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
|
||||
clickableComponent={
|
||||
@ -75,8 +91,7 @@ export const RecordTableHeaderLastColumn = () => {
|
||||
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
|
||||
}}
|
||||
/>
|
||||
</StyledPlusIconHeaderCell>
|
||||
<StyledEmptyHeaderCell />
|
||||
</>
|
||||
</StyledDropdownContainer>
|
||||
</StyledPlusIconHeaderCell>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,8 +5,13 @@ import { RecordTableCellGrip } from '@/object-record/record-table/record-table-c
|
||||
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
|
||||
import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells';
|
||||
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
|
||||
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
||||
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
type RecordTableRowProps = {
|
||||
recordId: string;
|
||||
@ -23,6 +28,13 @@ export const RecordTableRow = ({
|
||||
const listenedFields = getDefaultRecordFieldsToListen({
|
||||
objectNameSingular,
|
||||
});
|
||||
const isFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
rowIndexForFocus,
|
||||
);
|
||||
const isRowFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordTableDraggableTr
|
||||
@ -30,6 +42,7 @@ export const RecordTableRow = ({
|
||||
draggableIndex={rowIndexForDrag}
|
||||
focusIndex={rowIndexForFocus}
|
||||
>
|
||||
{isRowFocusActive && isFocused && <RecordTableRowHotkeyEffect />}
|
||||
<RecordTableCellGrip />
|
||||
<RecordTableCellCheckbox />
|
||||
<RecordTableCells />
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const RecordTableRowHotkeyEffect = () => {
|
||||
const { isSelected, recordId, objectNameSingular, rowIndex } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
|
||||
const { setCurrentRowSelected } = useSetCurrentRowSelected();
|
||||
|
||||
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
||||
|
||||
const { activateRecordTableRow } = useActiveRecordTableRow();
|
||||
|
||||
const setIsRowFocusActive = useSetRecoilComponentStateV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
);
|
||||
|
||||
const setFocusPosition = useSetRecordTableFocusPosition();
|
||||
|
||||
useScopedHotkeys(
|
||||
'x',
|
||||
() => {
|
||||
setCurrentRowSelected(!isSelected);
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
|
||||
() => {
|
||||
openRecordInCommandMenu({
|
||||
recordId: recordId,
|
||||
objectNameSingular: objectNameSingular,
|
||||
isNewRecord: false,
|
||||
});
|
||||
|
||||
activateRecordTableRow(rowIndex);
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
setIsRowFocusActive(false);
|
||||
setFocusPosition({
|
||||
row: rowIndex,
|
||||
column: 0,
|
||||
});
|
||||
},
|
||||
TableHotkeyScope.TableFocus,
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -3,17 +3,70 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactNode, forwardRef } from 'react';
|
||||
|
||||
const StyledTr = styled.tr<{ isDragging: boolean }>`
|
||||
position: relative;
|
||||
const StyledTr = styled.tr<{
|
||||
isDragging: boolean;
|
||||
}>`
|
||||
border: ${({ isDragging, theme }) =>
|
||||
isDragging
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: '1px solid transparent'};
|
||||
position: relative;
|
||||
transition: border-left-color 0.2s ease-in-out;
|
||||
|
||||
&[data-next-row-active-or-focused='true'] {
|
||||
td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-focused='true'] {
|
||||
td {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-color: ${({ theme }) => theme.border.color.medium};
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
&:last-of-type {
|
||||
border-right: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: 0 ${({ theme }) => theme.border.radius.sm}
|
||||
${({ theme }) => theme.border.radius.sm} 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active='true'] {
|
||||
td {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
background-color: ${({ theme }) => theme.accent.quaternary};
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
border-left: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
&:last-of-type {
|
||||
border-right: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-radius: 0 ${({ theme }) => theme.border.radius.sm}
|
||||
${({ theme }) => theme.border.radius.sm} 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type RecordTableTrProps = {
|
||||
@ -21,7 +74,10 @@ type RecordTableTrProps = {
|
||||
recordId: string;
|
||||
focusIndex: number;
|
||||
isDragging?: boolean;
|
||||
} & React.ComponentProps<typeof StyledTr>;
|
||||
} & Omit<
|
||||
React.ComponentProps<typeof StyledTr>,
|
||||
'isActive' | 'isNextRowActiveOrFocused' | 'isFocused'
|
||||
>;
|
||||
|
||||
export const RecordTableTr = forwardRef<
|
||||
HTMLTableRowElement,
|
||||
@ -38,6 +94,33 @@ export const RecordTableTr = forwardRef<
|
||||
recordId,
|
||||
);
|
||||
|
||||
const isActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
focusIndex,
|
||||
);
|
||||
|
||||
const isNextRowActive = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
focusIndex + 1,
|
||||
);
|
||||
|
||||
const isFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
focusIndex,
|
||||
);
|
||||
|
||||
const isRowFocusActive = useRecoilComponentValueV2(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
);
|
||||
|
||||
const isNextRowFocused = useRecoilComponentFamilyValueV2(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
focusIndex + 1,
|
||||
);
|
||||
|
||||
const isNextRowActiveOrFocused =
|
||||
(isRowFocusActive && isNextRowFocused) || isNextRowActive;
|
||||
|
||||
return (
|
||||
<RecordTableRowContextProvider
|
||||
value={{
|
||||
@ -56,6 +139,13 @@ export const RecordTableTr = forwardRef<
|
||||
data-virtualized-id={recordId}
|
||||
isDragging={isDragging}
|
||||
ref={ref}
|
||||
data-active={isActive ? 'true' : 'false'}
|
||||
data-focused={
|
||||
isRowFocusActive && isFocused && !isActive ? 'true' : 'false'
|
||||
}
|
||||
data-next-row-active-or-focused={
|
||||
isNextRowActiveOrFocused ? 'true' : 'false'
|
||||
}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const activeRecordTableRowIndexComponentState = createComponentStateV2<
|
||||
number | null
|
||||
>({
|
||||
key: 'activeRecordTableRowIndexComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const focusedRecordTableRowIndexComponentState = createComponentStateV2<
|
||||
number | null
|
||||
>({
|
||||
key: 'focusedRecordTableRowIndexComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const isRecordTableCellFocusActiveComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'isRecordTableCellFocusActiveComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
|
||||
export const isRecordTableRowActiveComponentFamilyState =
|
||||
createComponentFamilyStateV2<boolean, number>({
|
||||
key: 'isRecordTableRowActiveComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -1,9 +1,9 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const isRecordTableFocusActiveComponentState =
|
||||
export const isRecordTableRowFocusActiveComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'isRecordTableFocusActiveComponentState',
|
||||
key: 'isRecordTableRowFocusActiveComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
|
||||
export const isRecordTableRowFocusedComponentFamilyState =
|
||||
createComponentFamilyStateV2<boolean, number>({
|
||||
key: 'isRecordTableRowFocusedComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
Reference in New Issue
Block a user