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)}`, ); } }