diff --git a/packages/twenty-front/src/hooks/useScrollToPosition.ts b/packages/twenty-front/src/hooks/useScrollToPosition.ts
new file mode 100644
index 000000000000..34994c1f3b5a
--- /dev/null
+++ b/packages/twenty-front/src/hooks/useScrollToPosition.ts
@@ -0,0 +1,20 @@
+import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState';
+import { useRecoilCallback } from 'recoil';
+
+export const useScrollToPosition = () => {
+ const scrollToPosition = useRecoilCallback(
+ ({ snapshot }) =>
+ (scrollPositionInPx: number) => {
+ const overlayScrollbars = snapshot
+ .getLoadable(overlayScrollbarsState)
+ .getValue();
+
+ const scrollWrapper = overlayScrollbars?.elements().viewport;
+
+ scrollWrapper?.scrollTo({ top: scrollPositionInPx });
+ },
+ [],
+ );
+
+ return { scrollToPosition };
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
index 2a658225bb0f..e5ad988d3153 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
@@ -1,7 +1,7 @@
-import { useContext } from 'react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { format } from 'date-fns';
+import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { Avatar, AvatarGroup, IconArrowRight, IconLock } from 'twenty-ui';
@@ -14,8 +14,8 @@ import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEv
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent';
-import { CalendarChannelVisibility } from '~/generated/graphql';
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
+import { CalendarChannelVisibility } from '~/generated/graphql';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
import { isDefined } from '~/utils/isDefined';
@@ -169,7 +169,9 @@ export const CalendarEventRow = ({
? `${participant.firstName} ${participant.lastName}`
: participant.displayName
}
- entityId={participant.workspaceMemberId ?? participant.personId}
+ placeholderColorSeed={
+ participant.workspaceMemberId ?? participant.personId
+ }
type="rounded"
/>
))}
diff --git a/packages/twenty-front/src/modules/activities/comment/CommentHeader.tsx b/packages/twenty-front/src/modules/activities/comment/CommentHeader.tsx
index 9f87a81ebd39..d48f114e9d7f 100644
--- a/packages/twenty-front/src/modules/activities/comment/CommentHeader.tsx
+++ b/packages/twenty-front/src/modules/activities/comment/CommentHeader.tsx
@@ -62,7 +62,7 @@ export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
{authorName}
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
index 8b23973c5bb3..cb941c4e1861 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx
@@ -1,6 +1,6 @@
-import { useMemo, useRef } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
+import { useMemo, useRef } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Avatar, IconNotes, IconSparkles } from 'twenty-ui';
@@ -377,7 +377,7 @@ export const CommandMenu = () => {
{
to={`object/company/${company.id}`}
Icon={() => (
{
label={labelIdentifier}
Icon={() => (
0;
- const chipGeneratorPerObjectPerField = useMemo(() => {
- return getRecordChipGeneratorPerObjectPerField(objectMetadataItems);
- }, [objectMetadataItems]);
-
return (
<>
{shouldDisplayChildren ? (
-
+
{children}
-
+
) : (
)}
diff --git a/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx
new file mode 100644
index 000000000000..dcf9bc5cc73d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx
@@ -0,0 +1,30 @@
+import React, { useMemo } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators';
+
+export const PreComputedChipGeneratorsProvider = ({
+ children,
+}: React.PropsWithChildren) => {
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
+
+ const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } =
+ useMemo(() => {
+ return getRecordChipGenerators(objectMetadataItems);
+ }, [objectMetadataItems]);
+
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/context/PreComputedChipGeneratorsContext.ts b/packages/twenty-front/src/modules/object-metadata/context/PreComputedChipGeneratorsContext.ts
index fcb5b7d46b68..ed7b734bcc98 100644
--- a/packages/twenty-front/src/modules/object-metadata/context/PreComputedChipGeneratorsContext.ts
+++ b/packages/twenty-front/src/modules/object-metadata/context/PreComputedChipGeneratorsContext.ts
@@ -3,13 +3,19 @@ import { createContext } from 'react';
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-export type ChipGeneratorPerObjectPerField = Record<
+export type ChipGeneratorPerObjectNameSingularPerFieldName = Record<
string,
Record RecordChipData>
>;
+export type IdentifierChipGeneratorPerObject = Record<
+ string,
+ (record: ObjectRecord) => RecordChipData
+>;
+
export type PreComputedChipGeneratorsContextProps = {
- chipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField;
+ chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName;
+ identifierChipGeneratorPerObject: IdentifierChipGeneratorPerObject;
};
export const PreComputedChipGeneratorsContext =
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
index 7960047804ba..001cf4ecb06b 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
@@ -8,7 +8,7 @@ export const getLabelIdentifierFieldValue = (
record: ObjectRecord,
labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
objectNameSingular: string,
-) => {
+): string => {
if (
objectNameSingular === CoreObjectNameSingular.WorkspaceMember ||
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName
@@ -17,7 +17,7 @@ export const getLabelIdentifierFieldValue = (
}
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
- return record[labelIdentifierFieldMetadataItem.name] as string | number;
+ return String(record[labelIdentifierFieldMetadataItem.name]);
}
return '';
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLinkToShowPage.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLinkToShowPage.ts
index c5b747a1ebbb..4a6510dea2c1 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getLinkToShowPage.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getLinkToShowPage.ts
@@ -4,7 +4,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const getLinkToShowPage = (
objectNameSingular: string,
- record: ObjectRecord,
+ record: Pick,
) => {
const basePathToShowPage = getBasePathToShowPage({
objectNameSingular,
diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
index ea27165a15dd..94674aa57b21 100644
--- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
+++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
@@ -1,13 +1,17 @@
-import { EntityChip, EntityChipVariant } from 'twenty-ui';
+import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
-import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
+import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
+import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { isNonEmptyString } from '@sniptt/guards';
+import { MouseEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
export type RecordChipProps = {
objectNameSingular: string;
record: ObjectRecord;
className?: string;
- variant?: EntityChipVariant;
+ variant?: AvatarChipVariant;
};
export const RecordChip = ({
@@ -16,19 +20,29 @@ export const RecordChip = ({
className,
variant,
}: RecordChipProps) => {
- const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
+ const navigate = useNavigate();
+
+ const { recordChipData } = useRecordChipData({
objectNameSingular,
+ record,
});
- const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
+ const handleAvatarChipClick = (event: MouseEvent) => {
+ const linkToShowPage = getLinkToShowPage(objectNameSingular, record);
+
+ if (isNonEmptyString(linkToShowPage)) {
+ event.stopPropagation();
+ navigate(linkToShowPage);
+ }
+ };
return (
-
diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts
index 9dc0fed4f010..3abc3358a654 100644
--- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts
+++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationVariables.ts
@@ -1,8 +1,14 @@
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
+import { QueryCursorDirection } from '@/object-record/utils/generateFindManyRecordsQuery';
export type RecordGqlOperationVariables = {
filter?: RecordGqlOperationFilter;
orderBy?: RecordGqlOperationOrderBy;
limit?: number;
+ cursorFilter?: {
+ cursor: string;
+ cursorDirection: QueryCursorDirection;
+ limit: number;
+ };
};
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts
index 937548814f12..715cdc5af15d 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useFetchAllRecordIds.ts
@@ -38,7 +38,7 @@ export const useFetchAllRecordIds = ({
const firstQueryResult =
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
- const totalCount = firstQueryResult?.totalCount ?? 1;
+ const totalCount = firstQueryResult?.totalCount ?? 0;
const recordsCount = firstQueryResult?.edges.length ?? 0;
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts
index 00629c91a6b6..da5dc238af89 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts
@@ -34,6 +34,7 @@ export const useFindManyRecords = ({
fetchPolicy,
onError,
onCompleted,
+ cursorFilter,
}: UseFindManyRecordsParams) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItem } = useObjectMetadataItem({
@@ -42,6 +43,7 @@ export const useFindManyRecords = ({
const { findManyRecordsQuery } = useFindManyRecordsQuery({
objectNameSingular,
recordGqlFields,
+ cursorDirection: cursorFilter?.cursorDirection,
});
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
@@ -67,15 +69,16 @@ export const useFindManyRecords = ({
skip: skip || !objectMetadataItem || !currentWorkspaceMember,
variables: {
filter,
- limit,
orderBy,
+ lastCursor: cursorFilter?.cursor ?? undefined,
+ limit: cursorFilter?.limit ?? limit,
},
fetchPolicy: fetchPolicy,
onCompleted: handleFindManyRecordsCompleted,
onError: handleFindManyRecordsError,
});
- const { fetchMoreRecords, totalCount, records, hasNextPage } =
+ const { fetchMoreRecords, records, hasNextPage } =
useFetchMoreRecordsWithPagination({
objectNameSingular,
filter,
@@ -87,6 +90,9 @@ export const useFindManyRecords = ({
objectMetadataItem,
});
+ const pageInfo = data?.[objectMetadataItem.namePlural].pageInfo;
+ const totalCount = data?.[objectMetadataItem.namePlural].totalCount;
+
return {
objectMetadataItem,
records,
@@ -96,5 +102,6 @@ export const useFindManyRecords = ({
fetchMoreRecords,
queryStateIdentifier: queryIdentifier,
hasNextPage,
+ pageInfo,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts
index 55a6f270dcef..0ede4f9f009c 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecordsQuery.ts
@@ -3,16 +3,21 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
-import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
+import {
+ generateFindManyRecordsQuery,
+ QueryCursorDirection,
+} from '@/object-record/utils/generateFindManyRecordsQuery';
export const useFindManyRecordsQuery = ({
objectNameSingular,
recordGqlFields,
computeReferences,
+ cursorDirection = 'after',
}: {
objectNameSingular: string;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
computeReferences?: boolean;
+ cursorDirection?: QueryCursorDirection;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -25,6 +30,7 @@ export const useFindManyRecordsQuery = ({
objectMetadataItems,
recordGqlFields,
computeReferences,
+ cursorDirection,
});
return {
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
new file mode 100644
index 000000000000..1958a09eb535
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
@@ -0,0 +1,24 @@
+import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
+import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { useContext } from 'react';
+
+export const useRecordChipData = ({
+ objectNameSingular,
+ record,
+}: {
+ objectNameSingular: string;
+ record: ObjectRecord;
+}) => {
+ const { identifierChipGeneratorPerObject } = useContext(
+ PreComputedChipGeneratorsContext,
+ );
+
+ const generateRecordChipData =
+ identifierChipGeneratorPerObject[objectNameSingular] ??
+ generateDefaultRecordChipData;
+
+ const recordChipData = generateRecordChipData(record);
+
+ return { recordChipData };
+};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/GenericEntityFilterChip.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/GenericEntityFilterChip.tsx
index e11ae14c966d..fae9454c187a 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/GenericEntityFilterChip.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/GenericEntityFilterChip.tsx
@@ -1,4 +1,4 @@
-import { EntityChip, IconComponent } from 'twenty-ui';
+import { AvatarChip, IconComponent } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
@@ -13,8 +13,8 @@ export const GenericEntityFilterChip = ({
filter,
Icon,
}: GenericEntityFilterChipProps) => (
- {
}}
>
-
{isCompactModeActive && (
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
index c852eafa4831..f3448846d214 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
@@ -4,14 +4,13 @@ import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/dis
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
+import { isFieldChipDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldChipDisplay';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
-import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
-
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx
index 12d9b3ec786f..4a8682f3859e 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx
@@ -1,23 +1,21 @@
-import { EntityChip } from 'twenty-ui';
-
+import { RecordChip } from '@/object-record/components/RecordChip';
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
+import { RecordIndexRecordChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
export const ChipFieldDisplay = () => {
- const { recordValue, generateRecordChipData } = useChipFieldDisplay();
+ const { recordValue, objectNameSingular, isLabelIdentifier } =
+ useChipFieldDisplay();
if (!recordValue) {
return null;
}
- const recordChipData = generateRecordChipData(recordValue);
-
- return (
-
+ ) : (
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
index 44b05125dbac..adfaea988958 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
@@ -1,37 +1,27 @@
-import { EntityChip } from 'twenty-ui';
-
+import { RecordChip } from '@/object-record/components/RecordChip';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
-import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const RelationFromManyFieldDisplay = () => {
- const { fieldValue, fieldDefinition, generateRecordChipData } =
- useRelationFromManyFieldDisplay();
+ const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
const { isFocused } = useFieldFocus();
- if (
- !fieldValue ||
- !fieldDefinition?.metadata.relationObjectMetadataNameSingular
- ) {
+ const relationObjectNameSingular =
+ fieldDefinition?.metadata.relationObjectMetadataNameSingular;
+
+ if (!fieldValue || !relationObjectNameSingular) {
return null;
}
- const recordChipsData = fieldValue.map((fieldValueItem) =>
- generateRecordChipData(fieldValueItem),
- );
-
return (
- {recordChipsData.map((record) => {
+ {fieldValue.map((record) => {
return (
-
);
})}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx
index b0af7bdee97c..dede0f879534 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx
@@ -1,7 +1,5 @@
-import { EntityChip } from 'twenty-ui';
-
+import { RecordChip } from '@/object-record/components/RecordChip';
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
-import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const RelationToOneFieldDisplay = () => {
const { fieldValue, fieldDefinition, generateRecordChipData } =
@@ -17,12 +15,10 @@ export const RelationToOneFieldDisplay = () => {
const recordChipData = generateRecordChipData(fieldValue);
return (
-
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/utils/isFieldChipDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/utils/isFieldChipDisplay.ts
new file mode 100644
index 000000000000..cb45decfe7c3
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/utils/isFieldChipDisplay.ts
@@ -0,0 +1,11 @@
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
+import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
+import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
+
+export const isFieldChipDisplay = (
+ field: Pick,
+ isLabelIdentifier: boolean,
+) =>
+ isLabelIdentifier &&
+ (isFieldText(field) || isFieldFullName(field) || isFieldNumber(field));
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts
index 29cf1d3dc651..c0840bf7c06a 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts
@@ -1,8 +1,7 @@
-import { useContext } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
+import { useContext } from 'react';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
-import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
@@ -12,7 +11,8 @@ import { isDefined } from '~/utils/isDefined';
import { FieldContext } from '../../contexts/FieldContext';
export const useChipFieldDisplay = () => {
- const { entityId, fieldDefinition } = useContext(FieldContext);
+ const { entityId, fieldDefinition, isLabelIdentifier } =
+ useContext(FieldContext);
const { chipGeneratorPerObjectPerField } = useContext(
PreComputedChipGeneratorsContext,
@@ -31,18 +31,13 @@ export const useChipFieldDisplay = () => {
const recordValue = useRecordValue(entityId);
- if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
+ if (!isNonEmptyString(objectNameSingular)) {
throw new Error('Object metadata name singular is not a non-empty string');
}
- const generateRecordChipData =
- chipGeneratorPerObjectPerField[
- fieldDefinition.metadata.objectMetadataNameSingular
- ]?.[fieldDefinition.metadata.fieldName] ?? generateDefaultRecordChipData;
-
return {
objectNameSingular,
recordValue,
- generateRecordChipData,
+ isLabelIdentifier,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/lastShowPageRecordId.ts b/packages/twenty-front/src/modules/object-record/record-field/states/lastShowPageRecordId.ts
new file mode 100644
index 000000000000..dce923b37bda
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/states/lastShowPageRecordId.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const lastShowPageRecordIdState = createState({
+ key: 'lastShowPageRecordIdState',
+ defaultValue: null,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/recordPositionInternalState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/recordPositionInternalState.ts
new file mode 100644
index 000000000000..9b83e0dcd8d7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/states/recordPositionInternalState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const recordPositionInternalState = createState({
+ key: 'recordPositionInternalState',
+ defaultValue: null,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/RecordChipData.ts b/packages/twenty-front/src/modules/object-record/record-field/types/RecordChipData.ts
index 13eb99f84cd0..d6ef0c2f6948 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/RecordChipData.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/RecordChipData.ts
@@ -2,8 +2,9 @@ import { AvatarType } from 'twenty-ui';
export type RecordChipData = {
recordId: string;
- name: string | number;
+ name: string;
avatarType: AvatarType;
avatarUrl: string;
- linkToShowPage: string;
+ isLabelIdentifier: boolean;
+ objectNameSingular: string;
};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
index 28b6aa9a6a9d..c7098481c06d 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
@@ -1,15 +1,24 @@
import styled from '@emotion/styled';
-import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
+import {
+ useRecoilCallback,
+ useRecoilState,
+ useRecoilValue,
+ useSetRecoilState,
+} from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
+import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
+import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
+import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
+import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
@@ -17,15 +26,22 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
+import { useFindRecordCursorFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useFindRecordCursorFromFindManyCacheRootQuery';
+import { findView } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { ViewBar } from '@/views/components/ViewBar';
+import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
+import { View } from '@/views/types/View';
import { ViewField } from '@/views/types/ViewField';
import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
+import { useNavigate } from 'react-router-dom';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
@@ -108,6 +124,63 @@ export const RecordIndexContainer = ({
[columnDefinitions, setTableColumns],
);
+ const navigate = useNavigate();
+
+ const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const currentViewId = useRecoilValue(
+ currentViewIdComponentState({
+ scopeId: recordIndexId,
+ }),
+ );
+
+ const view = findView({
+ objectMetadataItemId: objectMetadataItem?.id ?? '',
+ viewId: currentViewId ?? null,
+ views,
+ });
+
+ const filter = turnObjectDropdownFilterIntoQueryFilter(
+ mapViewFiltersToFilters(view?.viewFilters ?? [], filterDefinitions),
+ objectMetadataItem?.fields ?? [],
+ );
+
+ const orderBy = turnSortsIntoOrderBy(
+ objectMetadataItem,
+ mapViewSortsToSorts(view?.viewSorts ?? [], sortDefinitions),
+ );
+
+ const { findCursorInCache } = useFindRecordCursorFromFindManyCacheRootQuery({
+ fieldVariables: {
+ filter,
+ orderBy,
+ },
+ objectNamePlural: objectNamePlural,
+ });
+
+ const handleIndexIdentifierClick = (recordId: string) => {
+ const cursor = findCursorInCache(recordId);
+
+ // TODO: use URL builder
+ navigate(
+ `/object/${objectNameSingular}/${recordId}?view=${currentViewId}`,
+ {
+ state: {
+ cursor,
+ },
+ },
+ );
+ };
+
+ const handleIndexRecordsLoaded = useRecoilCallback(
+ ({ set }) =>
+ () => {
+ // TODO: find a better way to reset this state ?
+ set(lastShowPageRecordIdState, null);
+ },
+ [],
+ );
+
return (
@@ -153,41 +226,46 @@ export const RecordIndexContainer = ({
/>
-
- {recordIndexViewType === ViewType.Table && (
- <>
-
-
- >
- )}
-
- {recordIndexViewType === ViewType.Kanban && (
-
-
-
-
-
- )}
+
+ {recordIndexViewType === ViewType.Table && (
+ <>
+
+
+ >
+ )}
+ {recordIndexViewType === ViewType.Kanban && (
+
+
+
+
+
+ )}
+
);
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx
new file mode 100644
index 000000000000..1a064570ca56
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx
@@ -0,0 +1,40 @@
+import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
+
+import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
+import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { useContext } from 'react';
+
+export type RecordIndexRecordChipProps = {
+ objectNameSingular: string;
+ record: ObjectRecord;
+ variant?: AvatarChipVariant;
+};
+
+export const RecordIndexRecordChip = ({
+ objectNameSingular,
+ record,
+ variant,
+}: RecordIndexRecordChipProps) => {
+ const { onIndexIdentifierClick } = useContext(RecordIndexEventContext);
+
+ const { recordChipData } = useRecordChipData({
+ objectNameSingular,
+ record,
+ });
+
+ const handleAvatarChipClick = () => {
+ onIndexIdentifierClick(record.id);
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexEventContext.tsx b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexEventContext.tsx
new file mode 100644
index 000000000000..7de19221dc79
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexEventContext.tsx
@@ -0,0 +1,9 @@
+import { createEventContext } from '~/utils/createEventContext';
+
+export type RecordIndexEventContextProps = {
+ onIndexIdentifierClick: (recordId: string) => void;
+ onIndexRecordsLoaded: () => void;
+};
+
+export const RecordIndexEventContext =
+ createEventContext();
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useFindRecordCursorFromFindManyCacheRootQuery.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useFindRecordCursorFromFindManyCacheRootQuery.ts
new file mode 100644
index 000000000000..878309217a11
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useFindRecordCursorFromFindManyCacheRootQuery.ts
@@ -0,0 +1,34 @@
+import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
+import { useApolloClient } from '@apollo/client';
+import { createApolloStoreFieldName } from '~/utils/createApolloStoreFieldName';
+
+export const useFindRecordCursorFromFindManyCacheRootQuery = ({
+ objectNamePlural,
+ fieldVariables,
+}: {
+ objectNamePlural: string;
+ fieldVariables: {
+ filter: any;
+ orderBy: any;
+ };
+}) => {
+ const apollo = useApolloClient();
+
+ const testsFieldNameOnRootQuery = createApolloStoreFieldName({
+ fieldName: objectNamePlural,
+ fieldVariables: fieldVariables,
+ });
+
+ const findCursorInCache = (recordId: string) => {
+ const extractedCache = apollo.cache.extract() as any;
+
+ const edgesInCache =
+ extractedCache?.['ROOT_QUERY']?.[testsFieldNameOnRootQuery]?.edges ?? [];
+
+ return edgesInCache.find(
+ (edge: RecordGqlRefEdge) => edge.node?.__ref.split(':')[1] === recordId,
+ )?.cursor;
+ };
+
+ return { findCursorInCache };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery.ts
new file mode 100644
index 000000000000..522c8112f219
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery.ts
@@ -0,0 +1,30 @@
+import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
+import { useApolloClient } from '@apollo/client';
+import { createApolloStoreFieldName } from '~/utils/createApolloStoreFieldName';
+
+export const useRecordIdsFromFindManyCacheRootQuery = ({
+ objectNamePlural,
+ fieldVariables,
+}: {
+ objectNamePlural: string;
+ fieldVariables: {
+ filter: any;
+ orderBy: any;
+ };
+}) => {
+ const apollo = useApolloClient();
+
+ const testsFieldNameOnRootQuery = createApolloStoreFieldName({
+ fieldName: objectNamePlural,
+ fieldVariables: fieldVariables,
+ });
+
+ const extractedCache = apollo.cache.extract() as any;
+
+ const recordIdsInCache: string[] =
+ extractedCache?.['ROOT_QUERY']?.[testsFieldNameOnRootQuery]?.edges?.map(
+ (edge: RecordGqlRefEdge) => edge.node?.__ref.split(':')[1],
+ ) ?? [];
+
+ return { recordIdsInCache };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts
new file mode 100644
index 000000000000..15092daba476
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts
@@ -0,0 +1,253 @@
+/* eslint-disable @nx/workspace-no-navigate-prefer-link */
+import { useMemo, useState } from 'react';
+import {
+ useLocation,
+ useNavigate,
+ useParams,
+ useSearchParams,
+} from 'react-router-dom';
+import { useSetRecoilState } from 'recoil';
+
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
+import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
+import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
+import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
+import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
+import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery';
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
+import { View } from '@/views/types/View';
+import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
+import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
+import { isNonEmptyString } from '@sniptt/guards';
+import { capitalize } from '~/utils/string/capitalize';
+
+export const findView = ({
+ viewId,
+ objectMetadataItemId,
+ views,
+}: {
+ viewId: string | null;
+ objectMetadataItemId: string;
+ views: View[];
+}) => {
+ if (!viewId) {
+ return views.find(
+ (view: any) =>
+ view.key === 'INDEX' && view?.objectMetadataId === objectMetadataItemId,
+ ) as View;
+ } else {
+ return views.find(
+ (view: any) =>
+ view?.id === viewId && view?.objectMetadataId === objectMetadataItemId,
+ ) as View;
+ }
+};
+
+export const useRecordShowPagePagination = (
+ propsObjectNameSingular: string,
+ propsObjectRecordId: string,
+) => {
+ const {
+ objectNameSingular: paramObjectNameSingular,
+ objectRecordId: paramObjectRecordId,
+ } = useParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const viewIdQueryParam = searchParams.get('view');
+
+ const setLastShowPageRecordId = useSetRecoilState(lastShowPageRecordIdState);
+
+ const [isLoadedRecords, setIsLoadedRecords] = useState(false);
+
+ const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular;
+ const objectRecordId = propsObjectRecordId || paramObjectRecordId;
+
+ if (!objectNameSingular || !objectRecordId) {
+ throw new Error('Object name or Record id is not defined');
+ }
+
+ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
+ const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const view = useMemo(() => {
+ return findView({
+ objectMetadataItemId: objectMetadataItem?.id ?? '',
+ viewId: viewIdQueryParam,
+ views,
+ });
+ }, [viewIdQueryParam, objectMetadataItem, views]);
+
+ const activeFieldMetadataItems = useMemo(
+ () =>
+ objectMetadataItem
+ ? objectMetadataItem.fields.filter(
+ ({ isActive, isSystem }) => isActive && !isSystem,
+ )
+ : [],
+ [objectMetadataItem],
+ );
+
+ const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
+ fields: activeFieldMetadataItems,
+ });
+
+ const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
+ fields: activeFieldMetadataItems,
+ });
+
+ const filter = turnObjectDropdownFilterIntoQueryFilter(
+ mapViewFiltersToFilters(view?.viewFilters ?? [], filterDefinitions),
+ objectMetadataItem?.fields ?? [],
+ );
+
+ const orderBy = turnSortsIntoOrderBy(
+ objectMetadataItem,
+ mapViewSortsToSorts(view?.viewSorts ?? [], sortDefinitions),
+ );
+
+ const recordGqlFields = generateDepthOneRecordGqlFields({
+ objectMetadataItem,
+ });
+
+ const { state } = useLocation();
+
+ const cursorFromIndexPage = state.cursor;
+
+ const { loading: loadingCurrentRecord, pageInfo: currentRecordsPageInfo } =
+ useFindManyRecords({
+ filter: {
+ id: { eq: objectRecordId },
+ },
+ orderBy,
+ skip: isLoadedRecords,
+ limit: 1,
+ objectNameSingular,
+ recordGqlFields,
+ });
+
+ const currentRecordCursor = currentRecordsPageInfo?.endCursor;
+
+ const cursor = cursorFromIndexPage ?? currentRecordCursor;
+
+ const {
+ loading: loadingRecordBefore,
+ records: recordsBefore,
+ pageInfo: pageInfoBefore,
+ totalCount: totalCountBefore,
+ } = useFindManyRecords({
+ filter,
+ orderBy,
+ skip: isLoadedRecords,
+ cursorFilter: isNonEmptyString(cursor)
+ ? {
+ cursorDirection: 'before',
+ cursor: cursor,
+ limit: 1,
+ }
+ : undefined,
+ objectNameSingular,
+ recordGqlFields,
+ });
+
+ const {
+ loading: loadingRecordAfter,
+ records: recordsAfter,
+ pageInfo: pageInfoAfter,
+ totalCount: totalCountAfter,
+ } = useFindManyRecords({
+ filter,
+ orderBy,
+ skip: isLoadedRecords,
+ cursorFilter: cursor
+ ? {
+ cursorDirection: 'after',
+ cursor: cursor,
+ limit: 1,
+ }
+ : undefined,
+ objectNameSingular,
+ recordGqlFields,
+ });
+
+ const totalCount = Math.max(totalCountBefore ?? 0, totalCountAfter ?? 0);
+
+ const loading =
+ loadingRecordAfter || loadingRecordBefore || loadingCurrentRecord;
+
+ const isThereARecordBefore = recordsBefore.length > 0;
+ const isThereARecordAfter = recordsAfter.length > 0;
+
+ const recordBefore = recordsBefore[0];
+ const recordAfter = recordsAfter[0];
+
+ const recordBeforeCursor = pageInfoBefore?.endCursor;
+ const recordAfterCursor = pageInfoAfter?.endCursor;
+
+ const navigateToPreviousRecord = () => {
+ navigate(
+ `/object/${objectNameSingular}/${recordBefore.id}${
+ viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
+ }`,
+ {
+ state: {
+ cursor: recordBeforeCursor,
+ },
+ },
+ );
+ };
+
+ const navigateToNextRecord = () => {
+ navigate(
+ `/object/${objectNameSingular}/${recordAfter.id}${
+ viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
+ }`,
+ {
+ state: {
+ cursor: recordAfterCursor,
+ },
+ },
+ );
+ };
+
+ const navigateToIndexView = () => {
+ const indexPath = `/objects/${objectMetadataItem.namePlural}${
+ viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
+ }`;
+
+ setLastShowPageRecordId(objectRecordId);
+
+ navigate(indexPath);
+ };
+
+ const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
+ objectNamePlural: objectMetadataItem.namePlural,
+ fieldVariables: {
+ filter,
+ orderBy,
+ },
+ });
+
+ const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
+
+ const rankFoundInFiew = rankInView > -1;
+
+ const objectLabel = capitalize(objectMetadataItem.namePlural);
+
+ const viewNameWithCount = rankFoundInFiew
+ ? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
+ : `${objectLabel} (${totalCount})`;
+
+ return {
+ viewName: viewNameWithCount,
+ hasPreviousRecord: isThereARecordBefore,
+ isLoadingPagination: loading,
+ hasNextRecord: isThereARecordAfter,
+ navigateToPreviousRecord,
+ navigateToNextRecord,
+ navigateToIndexView,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
index 8e5db342d972..5e3b8c23af3e 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
@@ -21,6 +21,7 @@ const StyledTable = styled.table`
`;
type RecordTableProps = {
+ viewBarId: string;
recordTableId: string;
objectNameSingular: string;
onColumnsChange: (columns: any) => void;
@@ -28,6 +29,7 @@ type RecordTableProps = {
};
export const RecordTable = ({
+ viewBarId,
recordTableId,
objectNameSingular,
onColumnsChange,
@@ -68,6 +70,7 @@ export const RecordTable = ({
{!isRecordTableInitialLoading &&
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
index 47fa787d25b6..7fe7343c9298 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx
@@ -18,10 +18,12 @@ import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocus
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
export const RecordTableContextProvider = ({
+ viewBarId,
recordTableId,
objectNameSingular,
children,
}: {
+ viewBarId: string;
recordTableId: string;
objectNameSingular: string;
children: ReactNode;
@@ -90,6 +92,7 @@ export const RecordTableContextProvider = ({
return (
{
leaveTableFocus();
},
- mode: ClickOutsideMode.comparePixels,
+ mode: ClickOutsideMode.compareHTMLRef,
});
useScopedHotkeys(
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
index 7a6cabbdddf1..85e860436e93 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx
@@ -76,6 +76,7 @@ export const RecordTableWithWrappers = ({
{},
onOpenTableCell: () => {},
diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
index 9bab734d9037..960e544620a2 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts
@@ -9,6 +9,7 @@ import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocus
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
export type RecordTableContextProps = {
+ viewBarId: string;
objectMetadataItem: ObjectMetadataItem;
onUpsertRecord: ({
persistField,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
index 2ba836121002..33816ad4d3ec 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
@@ -1,7 +1,8 @@
-import { useContext, useEffect } from 'react';
-import { useRecoilValue } from 'recoil';
+import { useContext, useEffect, useState } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
+import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
@@ -11,11 +12,17 @@ import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetch
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+import { isNonEmptyString } from '@sniptt/guards';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
+import { useScrollToPosition } from '~/hooks/useScrollToPosition';
+
+export const ROW_HEIGHT = 32;
export const RecordTableBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
+ const [hasInitializedScroll, setHasInitiazedScroll] = useState(false);
+
const {
fetchMoreRecords: fetchMoreObjects,
records,
@@ -76,9 +83,44 @@ export const RecordTableBodyEffect = () => {
}
}, [scrollLeft, setIsRecordTableScrolledLeft]);
- const rowHeight = 32;
+ const rowHeight = ROW_HEIGHT;
const viewportHeight = records.length * rowHeight;
+ const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState(
+ lastShowPageRecordIdState,
+ );
+
+ const { scrollToPosition } = useScrollToPosition();
+
+ useEffect(() => {
+ if (isNonEmptyString(lastShowPageRecordId) && !hasInitializedScroll) {
+ const isRecordAlreadyFetched = records.some(
+ (record) => record.id === lastShowPageRecordId,
+ );
+
+ if (isRecordAlreadyFetched) {
+ const recordPosition = records.findIndex(
+ (record) => record.id === lastShowPageRecordId,
+ );
+
+ const positionInPx = recordPosition * ROW_HEIGHT;
+
+ scrollToPosition(positionInPx);
+
+ setHasInitiazedScroll(true);
+ }
+ }
+ }, [
+ loading,
+ isFetchingMoreObjects,
+ lastShowPageRecordId,
+ fetchMoreObjects,
+ records,
+ scrollToPosition,
+ hasInitializedScroll,
+ setLastShowPageRecordId,
+ ]);
+
useScrollRestoration(viewportHeight);
useEffect(() => {
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
index b9e279b0b77a..2a379840276f 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
@@ -1,4 +1,3 @@
-import { useNavigate } from 'react-router-dom';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
@@ -21,6 +20,8 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined';
+import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
+import { useContext } from 'react';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@@ -40,13 +41,13 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
+ const { onIndexIdentifierClick } = useContext(RecordIndexEventContext);
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId);
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
- const navigate = useNavigate();
const leaveTableFocus = useLeaveTableFocus(tableScopeId);
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
@@ -66,7 +67,6 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
initialValue,
cellPosition,
isReadOnly,
- pathToShowPage,
objectNameSingular,
customCellHotkeyScope,
fieldDefinition,
@@ -94,7 +94,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
if (isFirstColumnCell && !isEmpty && !isActionButtonClick) {
leaveTableFocus();
- navigate(pathToShowPage);
+
+ onIndexIdentifierClick(entityId);
return;
}
@@ -142,7 +143,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
openRightDrawer,
setViewableRecordId,
setViewableRecordNameSingular,
- navigate,
+ onIndexIdentifierClick,
],
);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx
index d64f316e12bd..193c852d9e88 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx
@@ -1,10 +1,11 @@
-import { ReactNode, useContext } from 'react';
-import { useInView } from 'react-intersection-observer';
import { useTheme } from '@emotion/react';
import { Draggable } from '@hello-pangea/dnd';
+import { ReactNode, useContext, useEffect } from 'react';
+import { useInView } from 'react-intersection-observer';
import { useRecoilValue } from 'recoil';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
+import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
@@ -23,6 +24,7 @@ export const RecordTableRowWrapper = ({
children: ReactNode;
}) => {
const { objectMetadataItem } = useContext(RecordTableContext);
+ const { onIndexRecordsLoaded } = useContext(RecordIndexEventContext);
const theme = useTheme();
@@ -38,6 +40,13 @@ export const RecordTableRowWrapper = ({
rootMargin: '1000px',
});
+ // TODO: find a better way to emit this event
+ useEffect(() => {
+ if (inView) {
+ onIndexRecordsLoaded?.();
+ }
+ }, [inView, onIndexRecordsLoaded]);
+
return (
{(draggableProvided, draggableSnapshot) => (
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx
index e46f7d0362da..af92fedd19f0 100644
--- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx
@@ -66,7 +66,7 @@ export const MultipleObjectRecordSelectItem = ({
avatar={
gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
@@ -24,9 +27,11 @@ query FindMany${capitalize(
)}FilterInput, $orderBy: [${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput], $lastCursor: String, $limit: Int) {
- ${
- objectMetadataItem.namePlural
- }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
+ ${objectMetadataItem.namePlural}(filter: $filter, orderBy: $orderBy, ${
+ cursorDirection === 'before'
+ ? 'last: $limit, before: $lastCursor'
+ : 'first: $limit, after: $lastCursor'
+ } ){
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
@@ -37,11 +42,12 @@ query FindMany${capitalize(
cursor
}
pageInfo {
- ${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''}
+ hasNextPage
+ hasPreviousPage
startCursor
endCursor
}
- ${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''}
+ totalCount
}
}
`;
diff --git a/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts b/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts
index 20fc7d79f921..c43f7807a451 100644
--- a/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/getQueryIdentifier.ts
@@ -5,7 +5,12 @@ export const getQueryIdentifier = ({
filter,
orderBy,
limit,
+ cursorFilter,
}: RecordGqlOperationVariables & {
objectNameSingular: string;
}) =>
- objectNameSingular + JSON.stringify(filter) + JSON.stringify(orderBy) + limit;
+ objectNameSingular +
+ JSON.stringify(filter) +
+ JSON.stringify(orderBy) +
+ limit +
+ (cursorFilter ? JSON.stringify(cursorFilter) : undefined);
diff --git a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGeneratorPerObjectPerField.ts b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGeneratorPerObjectPerField.ts
deleted file mode 100644
index f127e275ec39..000000000000
--- a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGeneratorPerObjectPerField.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { ChipGeneratorPerObjectPerField } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
-import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
-import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
-import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
-import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
-import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
-import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
-import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
-import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
-import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
-import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
-import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
-import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
-import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
-import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
-import { isDefined } from '~/utils/isDefined';
-
-export const isFieldChipDisplay = (
- field: Pick,
- isLabelIdentifier: boolean,
-) =>
- isLabelIdentifier &&
- (isFieldText(field) || isFieldFullName(field) || isFieldNumber(field));
-
-export const getRecordChipGeneratorPerObjectPerField = (
- objectMetadataItems: ObjectMetadataItem[],
-) => {
- const recordChipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField =
- {};
-
- for (const objectMetadataItem of objectMetadataItems) {
- const generatorPerField = Object.fromEntries<
- (record: ObjectRecord) => RecordChipData
- >(
- objectMetadataItem.fields
- .filter(
- (fieldMetadataItem) =>
- isLabelIdentifierField({
- fieldMetadataItem: fieldMetadataItem,
- objectMetadataItem,
- }) ||
- fieldMetadataItem.type === FieldMetadataType.Relation ||
- isFieldChipDisplay(
- fieldMetadataItem,
- isLabelIdentifierField({
- fieldMetadataItem: fieldMetadataItem,
- objectMetadataItem,
- }),
- ),
- )
- .map((fieldMetadataItem) => {
- const objectNameSingularToFind = isLabelIdentifierField({
- fieldMetadataItem: fieldMetadataItem,
- objectMetadataItem: objectMetadataItem,
- })
- ? objectMetadataItem.nameSingular
- : isFieldRelation(fieldMetadataItem)
- ? fieldMetadataItem.relationDefinition?.targetObjectMetadata
- .nameSingular ?? undefined
- : undefined;
-
- const objectMetadataItemToUse = objectMetadataItems.find(
- (objectMetadataItem) =>
- objectMetadataItem.nameSingular === objectNameSingularToFind,
- );
-
- if (
- !isDefined(objectMetadataItemToUse) ||
- !isDefined(objectNameSingularToFind)
- ) {
- return ['', () => ({}) as any];
- }
-
- const labelIdentifierFieldMetadataItem =
- getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
-
- const imageIdentifierFieldMetadata =
- objectMetadataItemToUse.fields.find(
- (field) =>
- field.id ===
- objectMetadataItemToUse.imageIdentifierFieldMetadataId,
- );
-
- const avatarType = getAvatarType(objectNameSingularToFind);
-
- return [
- fieldMetadataItem.name,
- (record: ObjectRecord) => ({
- recordId: record.id,
- name: getLabelIdentifierFieldValue(
- record,
- labelIdentifierFieldMetadataItem,
- objectMetadataItemToUse.nameSingular,
- ),
- avatarUrl: getAvatarUrl(
- objectMetadataItemToUse.nameSingular,
- record,
- imageIdentifierFieldMetadata,
- ),
- avatarType,
- linkToShowPage: getLinkToShowPage(
- objectMetadataItemToUse.nameSingular,
- record,
- ),
- }),
- ];
- }),
- );
-
- recordChipGeneratorPerObjectPerField[objectMetadataItem.nameSingular] =
- generatorPerField;
- }
-
- return recordChipGeneratorPerObjectPerField;
-};
diff --git a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts
new file mode 100644
index 000000000000..c80fe1b76312
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts
@@ -0,0 +1,120 @@
+import {
+ ChipGeneratorPerObjectNameSingularPerFieldName,
+ IdentifierChipGeneratorPerObject,
+} from '@/object-metadata/context/PreComputedChipGeneratorsContext';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
+import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
+import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
+import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
+import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
+import { isFieldChipDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldChipDisplay';
+import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+import { isDefined } from '~/utils/isDefined';
+
+export const getRecordChipGenerators = (
+ objectMetadataItems: ObjectMetadataItem[],
+) => {
+ const chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName =
+ {};
+
+ const identifierChipGeneratorPerObject: IdentifierChipGeneratorPerObject = {};
+
+ for (const objectMetadataItem of objectMetadataItems) {
+ const labelIdentifierFieldMetadataItem =
+ getLabelIdentifierFieldMetadataItem(objectMetadataItem);
+
+ const generatorPerField = Object.fromEntries<
+ (record: ObjectRecord) => RecordChipData
+ >(
+ objectMetadataItem.fields
+ .filter(
+ (fieldMetadataItem) =>
+ labelIdentifierFieldMetadataItem?.id === fieldMetadataItem.id ||
+ fieldMetadataItem.type === FieldMetadataType.Relation ||
+ isFieldChipDisplay(
+ fieldMetadataItem,
+ isLabelIdentifierField({
+ fieldMetadataItem: fieldMetadataItem,
+ objectMetadataItem,
+ }),
+ ),
+ )
+ .map((fieldMetadataItem) => {
+ const isLabelIdentifier =
+ labelIdentifierFieldMetadataItem?.id === fieldMetadataItem.id;
+
+ const currentObjectNameSingular = objectMetadataItem.nameSingular;
+ const fieldRelationObjectNameSingular =
+ fieldMetadataItem.relationDefinition?.targetObjectMetadata
+ .nameSingular ?? undefined;
+
+ const objectNameSingularToFind = isLabelIdentifier
+ ? currentObjectNameSingular
+ : fieldRelationObjectNameSingular;
+
+ const objectMetadataItemToUse = objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.nameSingular === objectNameSingularToFind,
+ );
+
+ if (
+ !isDefined(objectMetadataItemToUse) ||
+ !isDefined(objectNameSingularToFind)
+ ) {
+ return ['', () => ({}) as any];
+ }
+
+ const labelIdentifierFieldMetadataItemToUse =
+ getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
+
+ const imageIdentifierFieldMetadataToUse =
+ objectMetadataItemToUse.fields.find(
+ (field) =>
+ field.id ===
+ objectMetadataItemToUse.imageIdentifierFieldMetadataId,
+ );
+
+ const avatarType = getAvatarType(objectNameSingularToFind);
+
+ return [
+ fieldMetadataItem.name,
+ (record: ObjectRecord) =>
+ ({
+ recordId: record.id,
+ name: getLabelIdentifierFieldValue(
+ record,
+ labelIdentifierFieldMetadataItemToUse,
+ objectMetadataItemToUse.nameSingular,
+ ),
+ avatarUrl: getAvatarUrl(
+ objectMetadataItemToUse.nameSingular,
+ record,
+ imageIdentifierFieldMetadataToUse,
+ ),
+ avatarType,
+ isLabelIdentifier,
+ objectNameSingular: objectNameSingularToFind,
+ }) satisfies RecordChipData,
+ ];
+ }),
+ );
+
+ chipGeneratorPerObjectPerField[objectMetadataItem.nameSingular] =
+ generatorPerField;
+
+ if (isDefined(labelIdentifierFieldMetadataItem)) {
+ identifierChipGeneratorPerObject[objectMetadataItem.nameSingular] =
+ chipGeneratorPerObjectPerField[objectMetadataItem.nameSingular]?.[
+ labelIdentifierFieldMetadataItem.name
+ ];
+ }
+ }
+
+ return {
+ chipGeneratorPerObjectPerField,
+ identifierChipGeneratorPerObject,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx
index ae92a178319c..49e7ba382dcb 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx
@@ -4,14 +4,15 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import {
- IconChevronLeft,
+ IconChevronDown,
+ IconChevronUp,
IconComponent,
+ IconX,
MOBILE_VIEWPORT,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { IconButton } from '@/ui/input/button/components/IconButton';
-import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@@ -53,6 +54,7 @@ const StyledLeftContainer = styled.div`
const StyledTitleContainer = styled.div`
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
+ font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(1)};
max-width: 50%;
`;
@@ -61,6 +63,7 @@ const StyledTopBarIconStyledTitleContainer = styled.div`
align-items: center;
display: flex;
flex: 1 0 auto;
+ gap: ${({ theme }) => theme.spacing(1)};
flex-direction: row;
`;
@@ -89,7 +92,13 @@ const StyledSkeletonLoader = () => {
type PageHeaderProps = ComponentProps<'div'> & {
title: string;
- hasBackButton?: boolean;
+ hasClosePageButton?: boolean;
+ onClosePage?: () => void;
+ hasPaginationButtons?: boolean;
+ hasPreviousRecord?: boolean;
+ hasNextRecord?: boolean;
+ navigateToPreviousRecord?: () => void;
+ navigateToNextRecord?: () => void;
Icon: IconComponent;
children?: ReactNode;
loading?: boolean;
@@ -97,7 +106,13 @@ type PageHeaderProps = ComponentProps<'div'> & {
export const PageHeader = ({
title,
- hasBackButton,
+ hasClosePageButton,
+ onClosePage,
+ hasPaginationButtons,
+ hasPreviousRecord,
+ hasNextRecord,
+ navigateToPreviousRecord,
+ navigateToNextRecord,
Icon,
children,
loading,
@@ -114,19 +129,36 @@ export const PageHeader = ({
)}
- {hasBackButton && (
-
-
-
+ {hasClosePageButton && (
+ onClosePage?.()}
+ />
)}
{loading ? (
) : (
+ {hasPaginationButtons && (
+ <>
+ navigateToPreviousRecord?.()}
+ />
+ navigateToNextRecord?.()}
+ />
+ >
+ )}
{Icon && }
diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
index 4ca592e1e1f2..6a6b26cc591c 100644
--- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx
@@ -1,7 +1,7 @@
-import { ChangeEvent, ReactNode, useRef } from 'react';
-import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { ChangeEvent, ReactNode, useRef } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { AppTooltip, Avatar, AvatarType } from 'twenty-ui';
import { v4 as uuidV4 } from 'uuid';
@@ -124,7 +124,7 @@ export const ShowPageSummaryCard = ({
avatarUrl={logoOrAvatar}
onClick={onUploadPicture ? handleAvatarClick : undefined}
size="xl"
- entityId={id}
+ placeholderColorSeed={id}
placeholder={avatarPlaceholder}
type={avatarType}
/>
diff --git a/packages/twenty-front/src/modules/users/components/UserChip.tsx b/packages/twenty-front/src/modules/users/components/UserChip.tsx
index 82beb9da53be..5f4fbc94ebf3 100644
--- a/packages/twenty-front/src/modules/users/components/UserChip.tsx
+++ b/packages/twenty-front/src/modules/users/components/UserChip.tsx
@@ -1,4 +1,4 @@
-import { EntityChip } from 'twenty-ui';
+import { AvatarChip } from 'twenty-ui';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
@@ -9,8 +9,8 @@ export type UserChipProps = {
};
export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
-
{workspaceMember.userEmail}
-
{accessory}
);
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
index e8d5bd90247c..1037467db9ff 100644
--- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
+import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { PageBody } from '@/ui/layout/page/PageBody';
@@ -35,16 +36,35 @@ export const RecordShowPage = () => {
parameters.objectRecordId ?? '',
);
+ const {
+ viewName,
+ hasPreviousRecord,
+ hasNextRecord,
+ navigateToPreviousRecord,
+ navigateToNextRecord,
+ navigateToIndexView,
+ isLoadingPagination,
+ } = useRecordShowPagePagination(
+ parameters.objectNameSingular ?? '',
+ parameters.objectRecordId ?? '',
+ );
+
return (
<>
= {
routePath: '/object/:objectNameSingular/:objectRecordId',
routeParams: {
':objectNameSingular': 'person',
- ':objectRecordId': '1234',
+ ':objectRecordId': peopleMock[0].id,
},
},
parameters: {
msw: {
handlers: [
+ graphql.query('FindManyPeople', () => {
+ return HttpResponse.json({
+ data: peopleQueryResult,
+ });
+ }),
graphql.query('FindOnePerson', () => {
return HttpResponse.json({
data: {
@@ -64,8 +69,8 @@ const meta: Meta = {
edges: [],
pageInfo: {
hasNextPage: false,
- startCursor: '1234',
- endCursor: '1234',
+ startCursor: peopleMock[0].id,
+ endCursor: peopleMock[0].id,
},
totalCount: 0,
},
diff --git a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx
index 6f9ac5d305bd..283e7046e0ab 100644
--- a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx
+++ b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx
@@ -1,21 +1,21 @@
-import { useMemo } from 'react';
import { Decorator } from '@storybook/react';
+import { useMemo } from 'react';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
-import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
+import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
export const ChipGeneratorsDecorator: Decorator = (Story) => {
- const chipGeneratorPerObjectPerField = useMemo(() => {
- return getRecordChipGeneratorPerObjectPerField(
- generatedMockObjectMetadataItems,
- );
- }, []);
+ const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } =
+ useMemo(() => {
+ return getRecordChipGenerators(generatedMockObjectMetadataItems);
+ }, []);
return (
diff --git a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx
index 1bf08a190ffa..c1582edbae07 100644
--- a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx
+++ b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx
@@ -1,13 +1,12 @@
-import { useEffect, useMemo } from 'react';
import { Decorator } from '@storybook/react';
+import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
-import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
+import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
-import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
import { mockedUserData } from '~/testing/mock-data/users';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@@ -23,20 +22,12 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
setCurrentUser(mockedUserData);
}, [setCurrentUser, setCurrentWorkspaceMember]);
- const chipGeneratorPerObjectPerField = useMemo(() => {
- return getRecordChipGeneratorPerObjectPerField(objectMetadataItems);
- }, [objectMetadataItems]);
-
return (
<>
-
+
{!!objectMetadataItems.length && }
-
+
>
);
};
diff --git a/packages/twenty-front/src/utils/createApolloStoreFieldName.ts b/packages/twenty-front/src/utils/createApolloStoreFieldName.ts
new file mode 100644
index 000000000000..7959a7f2a938
--- /dev/null
+++ b/packages/twenty-front/src/utils/createApolloStoreFieldName.ts
@@ -0,0 +1,9 @@
+export const createApolloStoreFieldName = ({
+ fieldName,
+ fieldVariables,
+}: {
+ fieldName: string;
+ fieldVariables: Record;
+}) => {
+ return `${fieldName}(${JSON.stringify(fieldVariables)})`;
+};
diff --git a/packages/twenty-front/src/utils/createEventContext.ts b/packages/twenty-front/src/utils/createEventContext.ts
new file mode 100644
index 000000000000..c69425003def
--- /dev/null
+++ b/packages/twenty-front/src/utils/createEventContext.ts
@@ -0,0 +1,12 @@
+import { Context, createContext } from 'react';
+
+type ObjectOfFunctions = {
+ [key: string]: (...args: any[]) => void;
+};
+
+export type EventContext =
+ T extends ObjectOfFunctions ? T : never;
+
+export const createEventContext = (): Context<
+ EventContext
+> => createContext>({} as EventContext);
diff --git a/packages/twenty-ui/src/display/avatar/components/Avatar.tsx b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
index a191549d4956..b0373b8cb8e5 100644
--- a/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
+++ b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx
@@ -1,6 +1,6 @@
-import { useContext } from 'react';
import { styled } from '@linaria/react';
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
+import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
@@ -50,7 +50,7 @@ export type AvatarProps = {
className?: string;
size?: AvatarSize;
placeholder: string | undefined;
- entityId?: string;
+ placeholderColorSeed?: string;
type?: Nullable;
color?: string;
backgroundColor?: string;
@@ -62,7 +62,7 @@ export const Avatar = ({
avatarUrl,
size = 'md',
placeholder,
- entityId = placeholder,
+ placeholderColorSeed = placeholder,
onClick,
type = 'squared',
color,
@@ -85,9 +85,10 @@ export const Avatar = ({
}
};
- const fixedColor = color ?? stringToHslColor(entityId ?? '', 75, 25);
+ const fixedColor =
+ color ?? stringToHslColor(placeholderColorSeed ?? '', 75, 25);
const fixedBackgroundColor =
- backgroundColor ?? stringToHslColor(entityId ?? '', 75, 85);
+ backgroundColor ?? stringToHslColor(placeholderColorSeed ?? '', 75, 85);
const showBackgroundColor = showPlaceholder;
diff --git a/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx b/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx
index 710837644b22..e3792988dc5a 100644
--- a/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx
+++ b/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx
@@ -13,7 +13,7 @@ import { AvatarGroup, AvatarGroupProps } from '../AvatarGroup';
const makeAvatar = (userName: string, props: Partial = {}) => (
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
);
const getAvatars = (commonProps: Partial = {}) => [
diff --git a/packages/twenty-ui/src/display/chip/components/EntityChip.tsx b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx
similarity index 60%
rename from packages/twenty-ui/src/display/chip/components/EntityChip.tsx
rename to packages/twenty-ui/src/display/chip/components/AvatarChip.tsx
index 13f95297ceb4..89c4bdabef11 100644
--- a/packages/twenty-ui/src/display/chip/components/EntityChip.tsx
+++ b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx
@@ -1,56 +1,47 @@
-import * as React from 'react';
-import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
-import { isNonEmptyString } from '@sniptt/guards';
import { Avatar } from '@ui/display/avatar/components/Avatar';
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
import { Chip, ChipVariant } from '@ui/display/chip/components/Chip';
import { IconComponent } from '@ui/display/icon/types/IconComponent';
+import { isDefined } from '@ui/utilities/isDefined';
import { Nullable } from '@ui/utilities/types/Nullable';
+import { MouseEvent } from 'react';
-export type EntityChipProps = {
- linkToEntity?: string;
- entityId: string;
+export type AvatarChipProps = {
name: string;
avatarUrl?: string;
avatarType?: Nullable;
- variant?: EntityChipVariant;
+ variant?: AvatarChipVariant;
LeftIcon?: IconComponent;
className?: string;
+ placeholderColorSeed?: string;
+ onClick?: (event: MouseEvent) => void;
};
-export enum EntityChipVariant {
+export enum AvatarChipVariant {
Regular = 'regular',
Transparent = 'transparent',
}
-export const EntityChip = ({
- linkToEntity,
- entityId,
+export const AvatarChip = ({
name,
avatarUrl,
avatarType = 'rounded',
- variant = EntityChipVariant.Regular,
+ variant = AvatarChipVariant.Regular,
LeftIcon,
className,
-}: EntityChipProps) => {
- const navigate = useNavigate();
+ placeholderColorSeed,
+ onClick,
+}: AvatarChipProps) => {
const theme = useTheme();
- const handleLinkClick = (event: React.MouseEvent) => {
- if (isNonEmptyString(linkToEntity)) {
- event.stopPropagation();
- navigate(linkToEntity);
- }
- };
-
return (
)
}
- clickable={!!linkToEntity}
- onClick={handleLinkClick}
+ clickable={isDefined(onClick)}
+ onClick={onClick}
className={className}
/>
);
diff --git a/packages/twenty-ui/src/display/chip/components/__stories__/EntityChip.stories.tsx b/packages/twenty-ui/src/display/chip/components/__stories__/EntityChip.stories.tsx
index 254fceef4018..2682eec0409e 100644
--- a/packages/twenty-ui/src/display/chip/components/__stories__/EntityChip.stories.tsx
+++ b/packages/twenty-ui/src/display/chip/components/__stories__/EntityChip.stories.tsx
@@ -1,21 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
+import { AvatarChip } from '@ui/display/chip/components/AvatarChip';
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
-import { EntityChip } from '../EntityChip';
-
-const meta: Meta = {
- title: 'UI/Display/Chip/EntityChip',
- component: EntityChip,
+const meta: Meta = {
+ title: 'UI/Display/Chip/AvatarChip',
+ component: AvatarChip,
decorators: [RouterDecorator, ComponentDecorator],
args: {
name: 'Entity name',
- linkToEntity: '/entity-link',
avatarType: 'squared',
},
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Default: Story = {};
diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts
index e0329a5bb704..fa961ff5abf3 100644
--- a/packages/twenty-ui/src/display/index.ts
+++ b/packages/twenty-ui/src/display/index.ts
@@ -6,8 +6,8 @@ export * from './avatar/types/AvatarSize';
export * from './avatar/types/AvatarType';
export * from './checkmark/components/AnimatedCheckmark';
export * from './checkmark/components/Checkmark';
+export * from './chip/components/AvatarChip';
export * from './chip/components/Chip';
-export * from './chip/components/EntityChip';
export * from './color/components/ColorSample';
export * from './icon/components/IconAddressBook';
export * from './icon/components/IconGmail';
diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts
index 1e20365c2bcf..3d99deafeead 100644
--- a/packages/twenty-ui/src/utilities/index.ts
+++ b/packages/twenty-ui/src/utilities/index.ts
@@ -1,3 +1,4 @@
export * from './color/utils/stringToHslColor';
+export * from './isDefined';
export * from './state/utils/createState';
export * from './types/Nullable';
diff --git a/packages/twenty-ui/src/utilities/isDefined.ts b/packages/twenty-ui/src/utilities/isDefined.ts
new file mode 100644
index 000000000000..81eb67203a03
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/isDefined.ts
@@ -0,0 +1,4 @@
+import { isNull, isUndefined } from '@sniptt/guards';
+
+export const isDefined = (value: T | null | undefined): value is T =>
+ !isUndefined(value) && !isNull(value);