From e04eef0461bb6b8fd0fb582a09ac4f3725819cef Mon Sep 17 00:00:00 2001 From: Parship Chowdhury Date: Mon, 8 Jun 2026 20:14:03 +0530 Subject: [PATCH] fix: wrong record count on deleted and normal records (#21292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Resolves #11977 - When looking into the deleted records from People tab (or any object list), the record detail header showing 0/(total records) instead of the correct position among deleted records only, e.g. 1/3 or 3/7. So, this PR makes the count match what users see in the deleted-records list. - Also normal records showing `0/N` in the header when opened from a list view (e.g. `0/48` -> `2/48`). ## Approach I tried to keep the change small and avoid extra server requests: - when a user came from a deleted-records view, we tell our existing queries to include soft-deleted records. - for the position number, we use the record list the user already had open (from the index view they came from) instead of apollo cache, which didn’t include records, especially deleted ones, but also normal records. - normal list behavior is not changed on the server side. ## Test plan - Open people/company, delete a record - Use the side menu -> “see deleted records” - open a deleted record’s details - confirm the header showing the correct position and total (e.g. 1/2, not 0/100) - for normal list: open People (normal list, not deleted) -> click a record -> open full page -> confirm header shows correct position and total (e.g. `2/48`, not `0/48`) ## Screenshots ### Before: Screenshot 2026-06-07 135204 Screenshot 2026-06-07 141735 ### After: Screenshot 2026-06-07 134946 Screenshot 2026-06-07 135045 Screenshot 2026-06-07 141642 --------- Signed-off-by: Parship Chowdhury Co-authored-by: Charles Bochet --- .../graphql/utils/computeCursorArgFilter.ts | 114 +++++++ .../graphql/utils/extractOrderByFieldNames.ts | 33 ++ .../graphql/utils/isOrderByDirection.ts | 11 + .../graphql/utils/reverseOrderBy.ts | 44 +++ .../utils/isRecordMatchingFilter.ts | 3 +- .../ObjectRecordShowPageBreadcrumb.tsx | 9 +- .../hooks/useRecordShowPagePagination.ts | 308 +++++++++--------- .../hooks/useQueryVariablesFromParentView.ts | 7 + .../src/types/RecordGqlOperationFilter.ts | 8 + .../filter/utils/isMatchingStringFilter.ts | 12 + .../filter/utils/isMatchingUUIDFilter.ts | 14 +- 11 files changed, 403 insertions(+), 160 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/graphql/utils/computeCursorArgFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/graphql/utils/extractOrderByFieldNames.ts create mode 100644 packages/twenty-front/src/modules/object-record/graphql/utils/isOrderByDirection.ts create mode 100644 packages/twenty-front/src/modules/object-record/graphql/utils/reverseOrderBy.ts diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/computeCursorArgFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/computeCursorArgFilter.ts new file mode 100644 index 0000000000..0ce586999a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/computeCursorArgFilter.ts @@ -0,0 +1,114 @@ +import { + type RecordGqlOperationFilter, + type RecordGqlOperationOrderBy, +} from 'twenty-shared/types'; +import { isPlainObject } from 'twenty-shared/utils'; + +import { isOrderByDirection } from '@/object-record/graphql/utils/isOrderByDirection'; + +type CursorOrderByField = { + fieldName: string; + direction: string; + subFieldName?: string; +}; + +const isAscendingOrder = (direction: string): boolean => + direction === 'AscNullsFirst' || direction === 'AscNullsLast'; + +const computeOperator = ( + isAscending: boolean, + isForwardPagination: boolean, +): string => + (isAscending ? isForwardPagination : !isForwardPagination) ? 'gt' : 'lt'; + +const getCursorValue = ( + record: Record, + field: CursorOrderByField, +): unknown => { + if (field.subFieldName) { + return (record[field.fieldName] as Record | undefined)?.[ + field.subFieldName + ]; + } + + return record[field.fieldName]; +}; + +const buildCursorWhereCondition = ( + field: CursorOrderByField, + operator: string, + value: unknown, +): RecordGqlOperationFilter => + field.subFieldName + ? { [field.fieldName]: { [field.subFieldName]: { [operator]: value } } } + : { [field.fieldName]: { [operator]: value } }; + +const resolveOrderByFields = ( + orderBy: RecordGqlOperationOrderBy, +): CursorOrderByField[] => { + const fields: CursorOrderByField[] = []; + + for (const entry of orderBy) { + for (const [fieldName, value] of Object.entries(entry)) { + if (isOrderByDirection(value)) { + fields.push({ fieldName, direction: value }); + } else if (isPlainObject(value)) { + for (const [subFieldName, subValue] of Object.entries( + value as Record, + )) { + if (isOrderByDirection(subValue)) { + fields.push({ fieldName, direction: subValue, subFieldName }); + } + } + } + } + } + + if (!fields.some((field) => field.fieldName === 'id')) { + fields.push({ fieldName: 'id', direction: 'AscNullsFirst' }); + } + + return fields; +}; + +export const computeCursorArgFilter = ({ + orderBy, + cursorRecordValues, + isForwardPagination, +}: { + orderBy: RecordGqlOperationOrderBy; + cursorRecordValues: Record; + isForwardPagination: boolean; +}): RecordGqlOperationFilter => { + const fields = resolveOrderByFields(orderBy); + + const cumulativeConditions: RecordGqlOperationFilter[] = fields.map( + (field, index) => { + const equalityPrefixes = fields + .slice(0, index) + .map((prevField) => + buildCursorWhereCondition( + prevField, + 'eq', + getCursorValue(cursorRecordValues, prevField), + ), + ); + + const ascending = isAscendingOrder(field.direction); + const operator = computeOperator(ascending, isForwardPagination); + const comparison = buildCursorWhereCondition( + field, + operator, + getCursorValue(cursorRecordValues, field), + ); + + const conditions = [...equalityPrefixes, comparison]; + + return conditions.length === 1 ? conditions[0] : { and: conditions }; + }, + ); + + if (cumulativeConditions.length === 0) return {}; + + return { or: cumulativeConditions }; +}; diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/extractOrderByFieldNames.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/extractOrderByFieldNames.ts new file mode 100644 index 0000000000..105c28ee46 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/extractOrderByFieldNames.ts @@ -0,0 +1,33 @@ +import { type RecordGqlOperationOrderBy } from 'twenty-shared/types'; +import { isPlainObject } from 'twenty-shared/utils'; + +import { isOrderByDirection } from '@/object-record/graphql/utils/isOrderByDirection'; + +export const extractOrderByFieldNames = ( + orderBy: RecordGqlOperationOrderBy, +): Record> => { + const gqlFields: Record> = { + id: true, + }; + + for (const entry of orderBy) { + for (const [fieldName, value] of Object.entries(entry)) { + if (isOrderByDirection(value)) { + gqlFields[fieldName] = true; + } else if (isPlainObject(value)) { + const subFields: Record = {}; + + for (const [subFieldName, subValue] of Object.entries( + value as Record, + )) { + if (isOrderByDirection(subValue)) { + subFields[subFieldName] = true; + } + } + gqlFields[fieldName] = subFields; + } + } + } + + return gqlFields; +}; diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/isOrderByDirection.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/isOrderByDirection.ts new file mode 100644 index 0000000000..3c7400c3d1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/isOrderByDirection.ts @@ -0,0 +1,11 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +const ORDER_DIRECTIONS = new Set([ + 'AscNullsFirst', + 'AscNullsLast', + 'DescNullsFirst', + 'DescNullsLast', +]); + +export const isOrderByDirection = (value: unknown): value is string => + isNonEmptyString(value) && ORDER_DIRECTIONS.has(value); diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/reverseOrderBy.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/reverseOrderBy.ts new file mode 100644 index 0000000000..1e2b374283 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/reverseOrderBy.ts @@ -0,0 +1,44 @@ +import { type RecordGqlOperationOrderBy } from 'twenty-shared/types'; +import { isPlainObject } from 'twenty-shared/utils'; + +import { isOrderByDirection } from '@/object-record/graphql/utils/isOrderByDirection'; + +const REVERSE_DIRECTION: Record = { + AscNullsFirst: 'DescNullsLast', + AscNullsLast: 'DescNullsFirst', + DescNullsFirst: 'AscNullsLast', + DescNullsLast: 'AscNullsFirst', +}; + +type OrderByEntry = RecordGqlOperationOrderBy[number]; +type OrderByValue = OrderByEntry[string]; + +export const reverseOrderBy = ( + orderBy: RecordGqlOperationOrderBy, +): RecordGqlOperationOrderBy => + orderBy.map((entry) => { + const reversed: OrderByEntry = {}; + + for (const [key, value] of Object.entries(entry)) { + reversed[key] = reverseValue(value); + } + + return reversed; + }); + +const reverseValue = (value: OrderByValue): OrderByValue => { + if (isOrderByDirection(value)) { + return (REVERSE_DIRECTION[value] ?? value) as OrderByValue; + } + if (isPlainObject(value)) { + const reversed: OrderByEntry = {}; + + for (const [key, subValue] of Object.entries(value as OrderByEntry)) { + reversed[key] = reverseValue(subValue); + } + + return reversed; + } + + return value; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 3db5fe5825..fae0faa2df 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -336,7 +336,8 @@ export const isRecordMatchingFilter = ({ }); } case FieldMetadataType.NUMBER: - case FieldMetadataType.NUMERIC: { + case FieldMetadataType.NUMERIC: + case FieldMetadataType.POSITION: { return isMatchingFloatFilter({ floatFilter: filterValue as FloatFilter, value: record[filterKey], diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/ObjectRecordShowPageBreadcrumb.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/ObjectRecordShowPageBreadcrumb.tsx index 0350dc0936..5c686ef3ab 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/ObjectRecordShowPageBreadcrumb.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/ObjectRecordShowPageBreadcrumb.tsx @@ -9,6 +9,7 @@ import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/u import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell'; import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; import { styled } from '@linaria/react'; +import { useState } from 'react'; import { FieldMetadataType } from 'twenty-shared/types'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -56,6 +57,8 @@ export const ObjectRecordShowPageBreadcrumb = ({ objectLabel: string; labelIdentifierFieldMetadataItem?: FieldMetadataItem; }) => { + const [isInitialLoad, setIsInitialLoad] = useState(true); + const { loading } = useFindOneRecord({ objectNameSingular, objectRecordId, @@ -81,7 +84,11 @@ export const ObjectRecordShowPageBreadcrumb = ({ const { navigateToIndexView, rankInView, totalCount } = useRecordShowPagePagination(objectNameSingular, objectRecordId); - if (loading) { + if (!loading && isInitialLoad) { + setIsInitialLoad(false); + } + + if (isInitialLoad && loading) { return null; } 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 index 77f9c8e8c0..5b092f211a 100644 --- 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 @@ -1,23 +1,22 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId'; +import { computeCursorArgFilter } from '@/object-record/graphql/utils/computeCursorArgFilter'; +import { extractOrderByFieldNames } from '@/object-record/graphql/utils/extractOrderByFieldNames'; +import { reverseOrderBy } from '@/object-record/graphql/utils/reverseOrderBy'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery'; import { useQueryVariablesFromParentView } from '@/views/hooks/useQueryVariablesFromParentView'; import { AppPath } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { combineFilters, isDefined } from 'twenty-shared/utils'; import { useNavigateApp } from '~/hooks/useNavigateApp'; export const useRecordShowPagePagination = ( propsObjectNameSingular: string, propsObjectRecordId: string, ) => { - const { t } = useLingui(); const { objectNameSingular: paramObjectNameSingular, objectRecordId: paramObjectRecordId, @@ -36,207 +35,202 @@ export const useRecordShowPagePagination = ( throw new Error('Object name or Record id is not defined'); } - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); - - const { filter, orderBy } = useQueryVariablesFromParentView({ - objectMetadataItem, + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, }); - const { loading: loadingCursor, pageInfo: currentRecordsPageInfo } = + const { filter, orderBy, isSoftDeleteFilterActive } = + useQueryVariablesFromParentView({ + objectMetadataItem, + }); + + const orderByGqlFields = extractOrderByFieldNames(orderBy); + + const reversedOrderBy = reverseOrderBy(orderBy); + + const { loading: loadingCurrentRecord, records: currentRecords } = useFindManyRecords({ - filter: { - id: { eq: objectRecordId }, - }, + filter: { id: { eq: objectRecordId } }, orderBy, limit: 1, objectNameSingular, - recordGqlFields: { id: true }, + recordGqlFields: { ...orderByGqlFields, deletedAt: true }, + withSoftDeleted: true, }); - const currentRecordCursorFromRequest = currentRecordsPageInfo?.endCursor; + const currentRecord = currentRecords[0]; + const isCurrentRecordDeleted = isDefined(currentRecord?.deletedAt); + const withSoftDeleted = isSoftDeleteFilterActive || isCurrentRecordDeleted; - const [totalCountBefore, setTotalCountBefore] = useState(0); - const [totalCountAfter, setTotalCountAfter] = useState(0); + const deletedOnlyFilter = isCurrentRecordDeleted + ? { deletedAt: { is: 'NOT_NULL' as const } } + : undefined; - const { loading: loadingRecordBefore, records: recordsBefore } = + const currentRecordKeysetValues: Record | undefined = + isDefined(currentRecord) + ? { + id: currentRecord.id, + ...Object.fromEntries( + Object.keys(orderByGqlFields).map((fieldName) => [ + fieldName, + currentRecord[fieldName], + ]), + ), + } + : undefined; + + const beforeFilter = isDefined(currentRecordKeysetValues) + ? computeCursorArgFilter({ + orderBy, + cursorRecordValues: currentRecordKeysetValues, + isForwardPagination: false, + }) + : undefined; + + const afterFilter = isDefined(currentRecordKeysetValues) + ? computeCursorArgFilter({ + orderBy, + cursorRecordValues: currentRecordKeysetValues, + isForwardPagination: true, + }) + : undefined; + + const hasKeysetFilters = isDefined(beforeFilter) && isDefined(afterFilter); + const skipNeighborQueries = loadingCurrentRecord || !hasKeysetFilters; + + const baseNeighborOptions = { + skip: skipNeighborQueries, + objectNameSingular, + recordGqlFields: { id: true }, + withSoftDeleted, + limit: 1, + }; + + const mergedFilter = combineFilters( + [filter, deletedOnlyFilter].filter(isDefined), + ); + + const { + loading: loadingRecordBefore, + records: recordsBefore, + totalCount: totalCountBefore, + } = useFindManyRecords({ + ...baseNeighborOptions, + fetchPolicy: 'network-only', + filter: combineFilters([mergedFilter, beforeFilter].filter(isDefined)), + orderBy: reversedOrderBy, + }); + + const { + loading: loadingRecordAfter, + records: recordsAfter, + totalCount: totalCountAfter, + } = useFindManyRecords({ + ...baseNeighborOptions, + fetchPolicy: 'network-only', + filter: combineFilters([mergedFilter, afterFilter].filter(isDefined)), + orderBy, + }); + + const isAtFirstRecord = !loadingRecordBefore && totalCountBefore === 0; + const isAtLastRecord = !loadingRecordAfter && totalCountAfter === 0; + + const { loading: loadingFirstRecord, records: firstRecords } = useFindManyRecords({ - skip: loadingCursor, - fetchPolicy: 'network-only', - filter: { - ...filter, - id: { neq: objectRecordId }, - }, + ...baseNeighborOptions, + skip: skipNeighborQueries || !isAtLastRecord, + filter: mergedFilter, orderBy, - limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined, - cursorFilter: isNonEmptyString(currentRecordCursorFromRequest) - ? { - cursorDirection: 'before', - cursor: currentRecordCursorFromRequest, - } - : undefined, - objectNameSingular, - recordGqlFields: { id: true }, - onCompleted: (_, pagination) => { - setTotalCountBefore(pagination?.totalCount ?? 0); - }, }); - const { loading: loadingRecordAfter, records: recordsAfter } = + const { loading: loadingLastRecord, records: lastRecords } = useFindManyRecords({ - skip: loadingCursor, - filter: { - ...filter, - id: { neq: objectRecordId }, - }, - fetchPolicy: 'network-only', - orderBy, - limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined, - cursorFilter: currentRecordCursorFromRequest - ? { - cursorDirection: 'after', - cursor: currentRecordCursorFromRequest, - } - : undefined, - objectNameSingular, - recordGqlFields: { id: true }, - onCompleted: (_, pagination) => { - setTotalCountAfter(pagination?.totalCount ?? 0); - }, + ...baseNeighborOptions, + skip: skipNeighborQueries || !isAtFirstRecord, + filter: mergedFilter, + orderBy: reversedOrderBy, }); - const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor; + const loading = + loadingRecordAfter || + loadingRecordBefore || + loadingCurrentRecord || + !hasKeysetFilters || + (isAtLastRecord && loadingFirstRecord) || + (isAtFirstRecord && loadingLastRecord); const recordBefore = recordsBefore[0]; const recordAfter = recordsAfter[0]; - const isFirstRecord = !loading && !isDefined(recordBefore); - const isLastRecord = !loading && !isDefined(recordAfter); - - const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({ - objectNamePlural: objectMetadataItem.namePlural, - fieldVariables: { - filter, - orderBy, - }, - }); - - const cacheIsAvailableForNavigation = - !loading && - (totalCountAfter > 0 || totalCountBefore > 0) && - recordIdsInCache.length > 0; - - const canNavigateToPreviousRecord = - !isFirstRecord || (isFirstRecord && cacheIsAvailableForNavigation); + // oxlint-disable-next-line twenty/no-navigate-prefer-link + const navigateToRecord = (targetRecordId: string) => { + navigate( + AppPath.RecordShowPage, + { objectNameSingular, objectRecordId: targetRecordId }, + { viewId: viewIdQueryParam }, + ); + }; const navigateToPreviousRecord = () => { - if (loading) { - return; + if (loading) return; + + if (isDefined(recordBefore)) { + return navigateToRecord(recordBefore.id); } - if (isFirstRecord) { - if (cacheIsAvailableForNavigation) { - const lastRecordIdFromCache = - recordIdsInCache[recordIdsInCache.length - 1]; - - navigate( - AppPath.RecordShowPage, - { - objectNameSingular, - objectRecordId: lastRecordIdFromCache, - }, - { - viewId: viewIdQueryParam, - }, - ); - } - } else { - navigate( - AppPath.RecordShowPage, - { - objectNameSingular, - objectRecordId: recordBefore.id, - }, - { - viewId: viewIdQueryParam, - }, - ); + if (isDefined(lastRecords[0])) { + return navigateToRecord(lastRecords[0].id); } }; - const canNavigateToNextRecord = - !isLastRecord || (isLastRecord && cacheIsAvailableForNavigation); - const navigateToNextRecord = () => { - if (loading) { - return; + if (loading) return; + + if (isDefined(recordAfter)) { + return navigateToRecord(recordAfter.id); } - if (isLastRecord) { - if (cacheIsAvailableForNavigation) { - const firstRecordIdFromCache = recordIdsInCache[0]; - - navigate( - AppPath.RecordShowPage, - { - objectNameSingular, - objectRecordId: firstRecordIdFromCache, - }, - { - viewId: viewIdQueryParam, - }, - ); - } - } else { - navigate( - AppPath.RecordShowPage, - { - objectNameSingular, - objectRecordId: recordAfter.id, - }, - { - viewId: viewIdQueryParam, - }, - ); + if (isDefined(firstRecords[0])) { + return navigateToRecord(firstRecords[0].id); } }; const navigateToIndexView = () => { navigate( AppPath.RecordIndexPage, - { - objectNamePlural: objectMetadataItem.namePlural, - }, - { - viewId: viewIdQueryParam, - }, + { objectNamePlural: objectMetadataItem.namePlural }, + { viewId: viewIdQueryParam }, ); - setLastShowPageRecordId(objectRecordId); }; - const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId); + const rankInView = isDefined(totalCountBefore) ? totalCountBefore : -1; + const totalCount = + rankInView > -1 && isDefined(totalCountAfter) + ? 1 + rankInView + totalCountAfter + : 0; - const rankFoundInView = rankInView > -1; + const [cachedPagination, setCachedPagination] = useState({ + rankInView, + totalCount, + }); - const objectLabelPlural = objectMetadataItem.labelPlural; - - const totalCount = 1 + Math.max(totalCountBefore, totalCountAfter); - - const currentRank = rankInView + 1; - const viewNameWithCount = rankFoundInView - ? t`${currentRank} of ${totalCount} in ${objectLabelPlural}` - : t`${objectLabelPlural} (${totalCount})`; + if (!loading && rankInView > -1) { + if ( + cachedPagination.rankInView !== rankInView || + cachedPagination.totalCount !== totalCount + ) { + setCachedPagination({ rankInView, totalCount }); + } + } return { - viewName: viewNameWithCount, isLoadingPagination: loading, navigateToPreviousRecord, navigateToNextRecord, navigateToIndexView, - canNavigateToNextRecord, - canNavigateToPreviousRecord, - rankInView, - totalCount, + rankInView: loading ? cachedPagination.rankInView : rankInView, + totalCount: loading ? cachedPagination.totalCount : totalCount, objectMetadataItem, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts index 10a67a2ef8..3da1969a00 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts @@ -4,6 +4,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { isRecordFilterAboutSoftDelete } from '@/object-record/record-filter/utils/isRecordFilterAboutSoftDelete'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { getQueryVariablesFromFiltersAndSorts } from '@/views/utils/getQueryVariablesFromFiltersAndSorts'; @@ -37,8 +38,14 @@ export const useQueryVariablesFromParentView = ({ filterValueDependencies, }); + const isSoftDeleteFilterActive = + contextStoreRecordShowParentView?.parentViewFilters.some((recordFilter) => + isRecordFilterAboutSoftDelete({ recordFilter, objectMetadataItems }), + ) ?? false; + return { filter, orderBy, + isSoftDeleteFilterActive, }; }; diff --git a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts index 5edb2460d8..1b478322d2 100644 --- a/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-shared/src/types/RecordGqlOperationFilter.ts @@ -4,7 +4,11 @@ export type IsFilter = 'NULL' | 'NOT_NULL'; export type UUIDFilter = { eq?: UUIDFilterValue; + gt?: UUIDFilterValue; + gte?: UUIDFilterValue; in?: UUIDFilterValue[]; + lt?: UUIDFilterValue; + lte?: UUIDFilterValue; neq?: UUIDFilterValue; is?: IsFilter; }; @@ -21,7 +25,11 @@ export type BooleanFilter = { export type StringFilter = { eq?: string; + gt?: string; + gte?: string; in?: string[]; + lt?: string; + lte?: string; neq?: string; startsWith?: string; like?: string; diff --git a/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts index 7de9d0af12..c4da94efab 100644 --- a/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts @@ -15,6 +15,18 @@ export const isMatchingStringFilter = ({ case stringFilter.neq !== undefined: { return value !== stringFilter.neq; } + case stringFilter.gt !== undefined: { + return value > stringFilter.gt; + } + case stringFilter.gte !== undefined: { + return value >= stringFilter.gte; + } + case stringFilter.lt !== undefined: { + return value < stringFilter.lt; + } + case stringFilter.lte !== undefined: { + return value <= stringFilter.lte; + } case stringFilter.like !== undefined: { const escapedPattern = escapeRegExp(stringFilter.like); const regexPattern = escapedPattern.replace(/%/g, '.*'); diff --git a/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts index 754b7a99c5..2f7c7339b4 100644 --- a/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts @@ -14,6 +14,18 @@ export const isMatchingUUIDFilter = ({ case uuidFilter.neq !== undefined: { return value !== uuidFilter.neq; } + case uuidFilter.gt !== undefined: { + return value > uuidFilter.gt; + } + case uuidFilter.gte !== undefined: { + return value >= uuidFilter.gte; + } + case uuidFilter.lt !== undefined: { + return value < uuidFilter.lt; + } + case uuidFilter.lte !== undefined: { + return value <= uuidFilter.lte; + } case uuidFilter.in !== undefined: { return uuidFilter.in.includes(value); } @@ -26,7 +38,7 @@ export const isMatchingUUIDFilter = ({ } default: { throw new Error( - `Unexpected value for string filter : ${JSON.stringify(uuidFilter)}`, + `Unexpected value for UUID filter: ${JSON.stringify(uuidFilter)}`, ); } }