Make filters and sorts work on record page pagination (#12460)

Fixes #7929 

This PR implements a system to capture and preserve the filters and
sorts when navigating from an index view to a record show page. This
information is stored in a context store component state.

This allows users to navigate between records inside the record page
while maintaining context from the index view.
This commit is contained in:
Raphaël Bosi
2025-06-11 18:01:03 +02:00
committed by GitHub
parent 23cbeec227
commit 27d0a3766f
18 changed files with 296 additions and 195 deletions

View File

@ -2,6 +2,8 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId'; import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -13,10 +15,10 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useCallback } from 'react'; import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconBrowserMaximize } from 'twenty-ui/display'; import { IconBrowserMaximize } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input'; import { Button } from 'twenty-ui/input';
@ -60,6 +62,11 @@ export const RecordShowRightDrawerOpenRecordButton = ({
tabListComponentIdInRecordPage, tabListComponentIdInRecordPage,
); );
const parentViewState = useRecoilComponentCallbackStateV2(
contextStoreRecordShowParentViewComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const navigate = useNavigateApp(); const navigate = useNavigateApp();
const actionMenuId = useAvailableComponentInstanceIdOrThrow( const actionMenuId = useAvailableComponentInstanceIdOrThrow(
@ -68,43 +75,54 @@ export const RecordShowRightDrawerOpenRecordButton = ({
const { closeDropdown } = useDropdownV2(); const { closeDropdown } = useDropdownV2();
const handleOpenRecord = useCallback(() => { const handleOpenRecord = useRecoilCallback(
const tabIdToOpen = ({ snapshot, reset }) =>
activeTabIdInRightDrawer === 'home' () => {
? objectNameSingular === CoreObjectNameSingular.Note || const tabIdToOpen =
objectNameSingular === CoreObjectNameSingular.Task activeTabIdInRightDrawer === 'home'
? 'richText' ? objectNameSingular === CoreObjectNameSingular.Note ||
: 'timeline' objectNameSingular === CoreObjectNameSingular.Task
: activeTabIdInRightDrawer; ? 'richText'
: 'timeline'
: activeTabIdInRightDrawer;
setActiveTabIdInRecordPage(tabIdToOpen); setActiveTabIdInRecordPage(tabIdToOpen);
navigate(AppPath.RecordShowPage, { const parentView = snapshot.getLoadable(parentViewState).getValue();
if (parentView?.parentViewObjectNameSingular !== objectNameSingular) {
reset(parentViewState);
}
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
closeDropdown(
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId),
);
closeCommandMenu();
},
[
actionMenuId,
activeTabIdInRightDrawer,
closeCommandMenu,
closeDropdown,
navigate,
objectNameSingular, objectNameSingular,
objectRecordId: recordId, parentViewState,
}); recordId,
setActiveTabIdInRecordPage,
closeDropdown( ],
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId), );
);
closeCommandMenu();
}, [
actionMenuId,
activeTabIdInRightDrawer,
closeCommandMenu,
closeDropdown,
navigate,
objectNameSingular,
recordId,
setActiveTabIdInRecordPage,
]);
useScopedHotkeys( useScopedHotkeys(
['ctrl+Enter,meta+Enter'], ['ctrl+Enter,meta+Enter'],
handleOpenRecord, handleOpenRecord,
AppHotkeyScope.CommandMenuOpen, AppHotkeyScope.CommandMenuOpen,
[closeCommandMenu, navigate, objectNameSingular, recordId], [handleOpenRecord],
); );
if (!isDefined(record)) { if (!isDefined(record)) {

View File

@ -0,0 +1,22 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
type RecordShowParentViewComponentState = {
parentViewComponentId: string;
parentViewObjectNameSingular: string;
parentViewFilterGroups: RecordFilterGroup[];
parentViewFilters: RecordFilter[];
parentViewSorts: RecordSort[];
};
export const contextStoreRecordShowParentViewComponentState =
createComponentStateV2<RecordShowParentViewComponentState | undefined | null>(
{
key: 'contextStoreRecordShowParentViewComponentState',
defaultValue: undefined,
componentInstanceContext: ContextStoreComponentInstanceContext,
},
);

View File

@ -7,6 +7,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { import {
AvatarChip, AvatarChip,
AvatarChipVariant, AvatarChipVariant,
@ -27,6 +28,7 @@ export type RecordChipProps = {
size?: ChipSize; size?: ChipSize;
isLabelHidden?: boolean; isLabelHidden?: boolean;
triggerEvent?: TriggerEventType; triggerEvent?: TriggerEventType;
onClick?: (event: MouseEvent) => void;
}; };
export const RecordChip = ({ export const RecordChip = ({
@ -40,6 +42,7 @@ export const RecordChip = ({
forceDisableClick = false, forceDisableClick = false,
isLabelHidden = false, isLabelHidden = false,
triggerEvent = 'MOUSE_DOWN', triggerEvent = 'MOUSE_DOWN',
onClick,
}: RecordChipProps) => { }: RecordChipProps) => {
const { recordChipData } = useRecordChipData({ const { recordChipData } = useRecordChipData({
objectNameSingular, objectNameSingular,
@ -53,14 +56,16 @@ export const RecordChip = ({
const isSidePanelViewOpenRecordInType = const isSidePanelViewOpenRecordInType =
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL; recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL;
const handleCustomClick = isSidePanelViewOpenRecordInType const handleCustomClick = isDefined(onClick)
? (_event: MouseEvent<HTMLElement>) => { ? onClick
openRecordInCommandMenu({ : isSidePanelViewOpenRecordInType
recordId: record.id, ? (_event: MouseEvent<HTMLElement>) => {
objectNameSingular, openRecordInCommandMenu({
}); recordId: record.id,
} objectNameSingular,
: undefined; });
}
: undefined;
// TODO temporary until we create a record show page for Workspaces members // TODO temporary until we create a record show page for Workspaces members

View File

@ -10,13 +10,10 @@ import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/re
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope'; import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody'; import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader'; import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId'; import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { AppPath } from '@/types/AppPath';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
@ -24,14 +21,12 @@ import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { InView, useInView } from 'react-intersection-observer'; import { InView, useInView } from 'react-intersection-observer';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { AnimatedEaseInOut } from 'twenty-ui/utilities'; import { AnimatedEaseInOut } from 'twenty-ui/utilities';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledBoardCard = styled.div<{ const StyledBoardCard = styled.div<{
isDragging?: boolean; isDragging?: boolean;
@ -92,9 +87,6 @@ const StyledBoardCardWrapper = styled.div`
`; `;
export const RecordBoardCard = () => { export const RecordBoardCard = () => {
const navigate = useNavigateApp();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { recordId, rowIndex, columnIndex } = useContext( const { recordId, rowIndex, columnIndex } = useContext(
RecordBoardCardContext, RecordBoardCardContext,
); );
@ -131,8 +123,6 @@ export const RecordBoardCard = () => {
}, },
); );
const { objectNameSingular } = useRecordIndexContextOrThrow();
const recordBoardId = useAvailableScopeIdOrThrow( const recordBoardId = useAvailableScopeIdOrThrow(
RecordBoardScopeInternalContext, RecordBoardScopeInternalContext,
); );
@ -151,7 +141,7 @@ export const RecordBoardCard = () => {
const { openDropdown } = useDropdownV2(); const { openDropdown } = useDropdownV2();
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState); const { openRecordFromIndexView } = useOpenRecordFromIndexView();
const handleActionMenuDropdown = (event: React.MouseEvent) => { const handleActionMenuDropdown = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -166,17 +156,7 @@ export const RecordBoardCard = () => {
}; };
const handleCardClick = () => { const handleCardClick = () => {
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) { openRecordFromIndexView({ recordId });
openRecordInCommandMenu({
recordId,
objectNameSingular,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
}
}; };
const onMouseLeaveBoard = useDebouncedCallback(() => { const onMouseLeaveBoard = useDebouncedCallback(() => {

View File

@ -8,13 +8,11 @@ import { RecordBoardScopeInternalContext } from '@/object-record/record-board/sc
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Dispatch, SetStateAction, useContext } from 'react'; import { Dispatch, SetStateAction, useContext } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -45,8 +43,6 @@ export const RecordBoardCardHeader = ({
}: RecordBoardCardHeaderProps) => { }: RecordBoardCardHeaderProps) => {
const { recordId } = useContext(RecordBoardCardContext); const { recordId } = useContext(RecordBoardCardContext);
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const record = useRecoilValue(recordStoreFamilyState(recordId)); const record = useRecoilValue(recordStoreFamilyState(recordId));
const { objectMetadataItem } = useContext(RecordBoardContext); const { objectMetadataItem } = useContext(RecordBoardContext);
@ -68,7 +64,7 @@ export const RecordBoardCardHeader = ({
recordId, recordId,
); );
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState); const { openRecordFromIndexView } = useOpenRecordFromIndexView();
return ( return (
<RecordBoardCardHeaderContainer showCompactView={showCompactView}> <RecordBoardCardHeaderContainer showCompactView={showCompactView}>
@ -79,11 +75,9 @@ export const RecordBoardCardHeader = ({
record={record} record={record}
variant={AvatarChipVariant.Transparent} variant={AvatarChipVariant.Transparent}
maxWidth={150} maxWidth={150}
to={ onClick={() => {
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE openRecordFromIndexView({ recordId });
? indexIdentifierUrl(recordId) }}
: undefined
}
triggerEvent="CLICK" triggerEvent="CLICK"
/> />
)} )}

View File

@ -1,4 +1,4 @@
import { createContext } from 'react'; import { createContext, MouseEvent } from 'react';
import { TriggerEventType } from 'twenty-ui/utilities'; import { TriggerEventType } from 'twenty-ui/utilities';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
@ -35,6 +35,7 @@ export type GenericFieldContextType = {
isDisplayModeFixHeight?: boolean; isDisplayModeFixHeight?: boolean;
isReadOnly: boolean; isReadOnly: boolean;
disableChipClick?: boolean; disableChipClick?: boolean;
onRecordChipClick?: (event: MouseEvent) => void;
onOpenEditMode?: () => void; onOpenEditMode?: () => void;
onCloseEditMode?: () => void; onCloseEditMode?: () => void;
triggerEvent?: TriggerEventType; triggerEvent?: TriggerEventType;

View File

@ -12,6 +12,7 @@ export const ChipFieldDisplay = () => {
disableChipClick, disableChipClick,
maxWidth, maxWidth,
triggerEvent, triggerEvent,
onRecordChipClick,
} = useChipFieldDisplay(); } = useChipFieldDisplay();
if (!isDefined(recordValue)) { if (!isDefined(recordValue)) {
@ -28,6 +29,7 @@ export const ChipFieldDisplay = () => {
isLabelHidden={isLabelIdentifierCompact} isLabelHidden={isLabelIdentifierCompact}
forceDisableClick={disableChipClick} forceDisableClick={disableChipClick}
triggerEvent={triggerEvent} triggerEvent={triggerEvent}
onClick={onRecordChipClick}
/> />
); );
}; };

View File

@ -21,6 +21,7 @@ export const useChipFieldDisplay = () => {
disableChipClick, disableChipClick,
maxWidth, maxWidth,
triggerEvent, triggerEvent,
onRecordChipClick,
} = useContext(FieldContext); } = useContext(FieldContext);
const { chipGeneratorPerObjectPerField } = useContext( const { chipGeneratorPerObjectPerField } = useContext(
@ -54,5 +55,6 @@ export const useChipFieldDisplay = () => {
disableChipClick, disableChipClick,
maxWidth, maxWidth,
triggerEvent, triggerEvent,
onRecordChipClick,
}; };
}; };

View File

@ -0,0 +1,94 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { AppPath } from '@/types/AppPath';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useOpenRecordFromIndexView = () => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useRecordIndexContextOrThrow();
const navigate = useNavigateApp();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const currentRecordFilters = useRecoilComponentCallbackStateV2(
currentRecordFiltersComponentState,
recordIndexId,
);
const currentRecordSorts = useRecoilComponentCallbackStateV2(
currentRecordSortsComponentState,
recordIndexId,
);
const currentRecordFilterGroups = useRecoilComponentCallbackStateV2(
currentRecordFilterGroupsComponentState,
recordIndexId,
);
const openRecordFromIndexView = useRecoilCallback(
({ snapshot, set }) =>
({ recordId }: { recordId: string }) => {
const recordIndexOpenRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInState)
.getValue();
const parentViewFilters = snapshot
.getLoadable(currentRecordFilters)
.getValue();
const parentViewSorts = snapshot
.getLoadable(currentRecordSorts)
.getValue();
const parentViewFilterGroups = snapshot
.getLoadable(currentRecordFilterGroups)
.getValue();
set(
contextStoreRecordShowParentViewComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
{
parentViewComponentId: recordIndexId,
parentViewObjectNameSingular: objectNameSingular,
parentViewFilterGroups,
parentViewFilters,
parentViewSorts,
},
);
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular,
});
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
}
},
[
currentRecordFilters,
currentRecordSorts,
currentRecordFilterGroups,
recordIndexId,
objectNameSingular,
navigate,
openRecordInCommandMenu,
],
);
return { openRecordFromIndexView };
};

View File

@ -8,7 +8,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery'; import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView'; import { useQueryVariablesFromParentView } from '@/views/hooks/useQueryVariablesFromParentView';
import { capitalize, isDefined } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -36,10 +36,9 @@ export const useRecordShowPagePagination = (
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { filter, orderBy } = const { filter, orderBy } = useQueryVariablesFromParentView({
useQueryVariablesFromActiveFieldsOfViewOrDefaultView({ objectMetadataItem,
objectMetadataItem, });
});
const { loading: loadingCursor, pageInfo: currentRecordsPageInfo } = const { loading: loadingCursor, pageInfo: currentRecordsPageInfo } =
useFindManyRecords({ useFindManyRecords({

View File

@ -0,0 +1,5 @@
export const computeRecordShowComponentInstanceId = (
objectRecordId: string,
) => {
return `record-show-${objectRecordId}`;
};

View File

@ -2,6 +2,7 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@ -48,6 +49,8 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
const isLabelIdentifierCompact = const isLabelIdentifierCompact =
isMobile && !isRecordTableScrolledLeftComponent; isMobile && !isRecordTableScrolledLeftComponent;
const { openRecordFromIndexView } = useOpenRecordFromIndexView();
return ( return (
<FieldContext.Provider <FieldContext.Provider
value={{ value={{
@ -60,6 +63,9 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
displayedMaxRows: 1, displayedMaxRows: 1,
isReadOnly: isFieldReadOnly, isReadOnly: isFieldReadOnly,
maxWidth: columnDefinition.size, maxWidth: columnDefinition.size,
onRecordChipClick: () => {
openRecordFromIndexView({ recordId });
},
isForbidden: !hasObjectReadPermissions, isForbidden: !hasObjectReadPermissions,
}} }}
> >

View File

@ -14,9 +14,7 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
@ -25,6 +23,7 @@ import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropd
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition'; import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
@ -33,8 +32,8 @@ import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/po
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useNavigate } from 'react-router-dom';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = { export const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode, scope: TableHotkeyScope.CellEditMode,
}; };
@ -51,21 +50,20 @@ export type OpenTableCellArgs = {
isNavigating: boolean; isNavigating: boolean;
}; };
export const useOpenRecordTableCellV2 = (tableScopeId: string) => { export const useOpenRecordTableCellV2 = (recordTableId: string) => {
const clickOutsideListenerIsActivatedState = const clickOutsideListenerIsActivatedState =
useRecoilComponentCallbackStateV2( useRecoilComponentCallbackStateV2(
clickOutsideListenerIsActivatedComponentState, clickOutsideListenerIsActivatedComponentState,
RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
); );
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const setCurrentTableCellInEditModePosition = useSetRecoilComponentStateV2( const setCurrentTableCellInEditModePosition = useSetRecoilComponentStateV2(
recordTableCellEditModePositionComponentState, recordTableCellEditModePositionComponentState,
tableScopeId, recordTableId,
); );
const { setDragSelectionStartEnabled } = useDragSelect(); const { setDragSelectionStartEnabled } = useDragSelect();
const leaveTableFocus = useLeaveTableFocus(tableScopeId); const leaveTableFocus = useLeaveTableFocus(recordTableId);
const { toggleClickOutside } = useClickOutsideListener( const { toggleClickOutside } = useClickOutsideListener(
FOCUS_CLICK_OUTSIDE_LISTENER_ID, FOCUS_CLICK_OUTSIDE_LISTENER_ID,
); );
@ -77,27 +75,25 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
viewableRecordNameSingularState, viewableRecordNameSingularState,
); );
const navigate = useNavigate();
const { setActiveDropdownFocusIdAndMemorizePrevious } = const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious(); useSetActiveDropdownFocusIdAndMemorizePrevious();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { openFieldInput } = useOpenFieldInputEditMode(); const { openFieldInput } = useOpenFieldInputEditMode();
const { activateRecordTableRow, deactivateRecordTableRow } = const { activateRecordTableRow, deactivateRecordTableRow } =
useActiveRecordTableRow(tableScopeId); useActiveRecordTableRow(recordTableId);
const { unfocusRecordTableRow } = useFocusedRecordTableRow(tableScopeId); const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordTableId);
const setIsRowFocusActive = useSetRecoilComponentStateV2( const setIsRowFocusActive = useSetRecoilComponentStateV2(
isRecordTableRowFocusActiveComponentState, isRecordTableRowFocusActiveComponentState,
tableScopeId, recordTableId,
); );
const setFocusPosition = useSetRecordTableFocusPosition(); const setFocusPosition = useSetRecordTableFocusPosition();
const { openRecordFromIndexView } = useOpenRecordFromIndexView();
const openTableCell = useRecoilCallback( const openTableCell = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
({ ({
@ -141,20 +137,13 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
.getLoadable(recordIndexOpenRecordInState) .getLoadable(recordIndexOpenRecordInState)
.getValue(); .getValue();
if (openRecordIn === ViewOpenRecordInType.RECORD_PAGE) {
navigate(indexIdentifierUrl(recordId));
}
if (openRecordIn === ViewOpenRecordInType.SIDE_PANEL) { if (openRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
objectNameSingular,
});
activateRecordTableRow(cellPosition.row); activateRecordTableRow(cellPosition.row);
unfocusRecordTableRow(); unfocusRecordTableRow();
} }
openRecordFromIndexView({ recordId });
return; return;
} }
@ -214,13 +203,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
toggleClickOutside, toggleClickOutside,
setActiveDropdownFocusIdAndMemorizePrevious, setActiveDropdownFocusIdAndMemorizePrevious,
leaveTableFocus, leaveTableFocus,
navigate,
indexIdentifierUrl,
openRecordInCommandMenu,
activateRecordTableRow, activateRecordTableRow,
unfocusRecordTableRow, unfocusRecordTableRow,
setViewableRecordId, setViewableRecordId,
setViewableRecordNameSingular, setViewableRecordNameSingular,
openRecordFromIndexView,
], ],
); );

View File

@ -1,27 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
import { useQueryVariablesFromView } from './useQueryVariablesFromView';
export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { view } = useViewOrDefaultViewFromPrefetchedViews({
objectMetadataItemId: objectMetadataItem.id,
});
const { filterValueDependencies } = useFilterValueDependencies();
const { filter, orderBy } = useQueryVariablesFromView({
objectMetadataItem,
view,
filterValueDependencies,
});
return {
filter,
orderBy,
};
};

View File

@ -0,0 +1,32 @@
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { getQueryVariablesFromFiltersAndSorts } from '../utils/getQueryVariablesFromFiltersAndSorts';
export const useQueryVariablesFromParentView = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const recordShowParentView = useRecoilComponentValueV2(
contextStoreRecordShowParentViewComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const { filterValueDependencies } = useFilterValueDependencies();
const { filter, orderBy } = getQueryVariablesFromFiltersAndSorts({
recordFilterGroups: recordShowParentView?.parentViewFilterGroups ?? [],
recordFilters: recordShowParentView?.parentViewFilters ?? [],
recordSorts: recordShowParentView?.parentViewSorts ?? [],
objectMetadataItem,
filterValueDependencies,
});
return {
filter,
orderBy,
};
};

View File

@ -1,58 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter';
import { View } from '@/views/types/View';
import { getFilterableFieldsWithVectorSearch } from '@/views/utils/getFilterableFieldsWithVectorSearch';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { isDefined } from 'twenty-shared/utils';
export const useQueryVariablesFromView = ({
view,
objectMetadataItem,
filterValueDependencies,
}: {
view: View | null | undefined;
objectMetadataItem: ObjectMetadataItem;
filterValueDependencies: RecordFilterValueDependencies;
}) => {
if (!isDefined(view)) {
return {
filter: undefined,
orderBy: undefined,
};
}
const { viewFilterGroups, viewFilters, viewSorts } = view;
const recordFilterGroups = mapViewFilterGroupsToRecordFilterGroups(
viewFilterGroups ?? [],
);
const filterableFieldMetadataItems =
getFilterableFieldsWithVectorSearch(objectMetadataItem);
const recordFilters = mapViewFiltersToFilters(
viewFilters,
filterableFieldMetadataItems,
);
const filter = computeRecordGqlOperationFilter({
fields: objectMetadataItem?.fields ?? [],
filterValueDependencies,
recordFilterGroups,
recordFilters,
});
const orderBy = turnSortsIntoOrderBy(
objectMetadataItem,
mapViewSortsToSorts(viewSorts),
);
return {
filter,
orderBy,
};
};

View File

@ -0,0 +1,35 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
export const getQueryVariablesFromFiltersAndSorts = ({
recordFilterGroups,
recordFilters,
recordSorts,
objectMetadataItem,
filterValueDependencies,
}: {
recordFilterGroups: RecordFilterGroup[];
recordFilters: RecordFilter[];
recordSorts: RecordSort[];
objectMetadataItem: ObjectMetadataItem;
filterValueDependencies: RecordFilterValueDependencies;
}) => {
const filter = computeRecordGqlOperationFilter({
fields: objectMetadataItem?.fields ?? [],
filterValueDependencies,
recordFilterGroups,
recordFilters,
});
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordSorts);
return {
filter,
orderBy,
};
};

View File

@ -10,6 +10,7 @@ import { RecordFiltersComponentInstanceContext } from '@/object-record/record-fi
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect'; import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton'; import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton';
import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageBody } from '@/ui/layout/page/components/PageBody';
@ -28,21 +29,24 @@ export const RecordShowPage = () => {
parameters.objectRecordId ?? '', parameters.objectRecordId ?? '',
); );
const recordShowComponentInstanceId =
computeRecordShowComponentInstanceId(objectRecordId);
return ( return (
<RecordFilterGroupsComponentInstanceContext.Provider <RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{ instanceId: recordShowComponentInstanceId }}
> >
<RecordFiltersComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{ instanceId: recordShowComponentInstanceId }}
> >
<RecordSortsComponentInstanceContext.Provider <RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{ instanceId: recordShowComponentInstanceId }}
> >
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }} value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
> >
<ActionMenuComponentInstanceContext.Provider <ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }} value={{ instanceId: recordShowComponentInstanceId }}
> >
<PageContainer> <PageContainer>
<RecordShowPageTitle <RecordShowPageTitle