From ffd7824ac54c1da04cf17aeb8e58e0d2381f9f4e Mon Sep 17 00:00:00 2001 From: ad-elias Date: Fri, 6 Dec 2024 22:34:10 +0100 Subject: [PATCH 1/4] Add "me" entry to workspaceMember picker --- .../select/hooks/useRecordsForSelect.ts | 26 ++++++++++++++----- .../utils/resolveFilterValue.ts | 12 +++++++-- .../utils/resolveRelationViewFilterValue.ts | 8 ++++++ 3 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index 3abb7bcefd5d..cc4729603de1 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -103,6 +103,17 @@ export const useRecordsForSelect = ({ objectNameSingular, }); + const specialSelectableItems: SelectableItem[] = + objectNameSingular === 'workspaceMember' + ? [ + { + id: 'me', + name: 'Me', + isSelected: false, + }, + ] + : []; + return { selectedRecords: selectedRecordsData .map(mapToObjectRecordIdentifier) @@ -116,12 +127,15 @@ export const useRecordsForSelect = ({ ...record, isSelected: true, })) as SelectableItem[], - recordsToSelect: recordsToSelectData - .map(mapToObjectRecordIdentifier) - .map((record) => ({ - ...record, - isSelected: false, - })) as SelectableItem[], + recordsToSelect: [ + ...specialSelectableItems, + ...(recordsToSelectData + .map(mapToObjectRecordIdentifier) + .map((record) => ({ + ...record, + isSelected: false, + })) as SelectableItem[]), + ], loading: recordsToSelectLoading || filteredSelectedRecordsLoading || diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts index 38fd27439bc8..042bd09c9998 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts @@ -1,13 +1,14 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue'; import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue'; +import { resolveRelationViewFilterValue } from '@/views/view-filter-value/utils/resolveRelationViewFilterValue'; import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue'; import { resolveDateViewFilterValue, ResolvedDateViewFilterValue, } from './resolveDateViewFilterValue'; -import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue'; type ResolvedFilterValue< T extends FilterableFieldType, @@ -20,7 +21,9 @@ type ResolvedFilterValue< ? string[] : T extends 'BOOLEAN' ? boolean - : string; + : T extends 'RELATION' + ? string + : string; type PartialFilter< T extends FilterableFieldType, @@ -47,6 +50,11 @@ export const resolveFilterValue = < return resolveSelectViewFilterValue(filter) as ResolvedFilterValue; case 'BOOLEAN': return resolveBooleanViewFilterValue(filter) as ResolvedFilterValue; + case 'RELATION': + return resolveRelationViewFilterValue(filter) as ResolvedFilterValue< + T, + O + >; default: return filter.value as ResolvedFilterValue; } diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts new file mode 100644 index 000000000000..91b35ca89742 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts @@ -0,0 +1,8 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const resolveRelationViewFilterValue = ( + viewFilter: Pick, +) => { + // TODO: convert 'CURRENT_USER' to the current user id + return viewFilter.value; +}; From 1839da52b58d973fee6af192aff07f5dba0fc3e2 Mon Sep 17 00:00:00 2001 From: ad-elias Date: Mon, 9 Dec 2024 02:23:22 +0100 Subject: [PATCH 2/4] Resolve relation filter value and refactor --- .../hooks/useDeleteMultipleRecordsAction.tsx | 4 +- .../hooks/useComputeContextStoreFilters.ts | 56 + ...seFindManyRecordsSelectedInContextStore.ts | 4 +- .../utils/computeContextStoreFilters.ts | 49 - .../ObjectFilterDropdownDateInput.tsx | 16 +- .../ObjectFilterDropdownRecordSelect.tsx | 2 +- ...useAggregateRecordsForRecordBoardColumn.ts | 6 +- .../useComputeViewRecordGqlOperationFilter.ts | 1000 +++++++++++++++++ .../computeViewRecordGqlOperationFilter.ts | 976 ---------------- ...textStoreNumberOfSelectedRecordsEffect.tsx | 4 +- .../export/hooks/useExportFetchRecords.ts | 4 +- .../useFindManyRecordIndexTableParams.ts | 5 +- .../hooks/useLoadRecordIndexBoard.ts | 5 +- .../hooks/useLoadRecordIndexBoardColumn.ts | 5 +- .../select/hooks/useRecordsForSelect.ts | 22 +- .../buildRecordFromImportedStructuredRow.ts | 17 +- .../hooks/useGetQueryVariablesFromView.ts | 65 ++ ...blesFromActiveFieldsOfViewOrDefaultView.ts | 4 +- .../views/hooks/useResolveFilterValue.ts | 67 ++ .../views/utils/getQueryVariablesFromView.ts | 56 - .../useResolveRelationViewFilterValue.ts | 38 + .../utils/resolveBooleanViewFilterValue.ts | 9 +- .../utils/resolveDateViewFilterValue.ts | 27 +- .../utils/resolveFilterValue.ts | 61 - .../utils/resolveNumberViewFilterValue.ts | 5 +- .../utils/resolveRelationViewFilterValue.ts | 8 - .../utils/resolveSelectViewFilterValue.ts | 5 +- .../safeStringArrayJSONSchema.ts | 14 + 28 files changed, 1320 insertions(+), 1214 deletions(-) create mode 100644 packages/twenty-front/src/modules/context-store/hooks/useComputeContextStoreFilters.ts delete mode 100644 packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useGetQueryVariablesFromView.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useResolveFilterValue.ts delete mode 100644 packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts create mode 100644 packages/twenty-front/src/modules/views/view-filter-value/hooks/useResolveRelationViewFilterValue.ts delete mode 100644 packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts delete mode 100644 packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts create mode 100644 packages/twenty-front/src/utils/validation-schemas/safeStringArrayJSONSchema.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx index 5e517ace1346..a4e04588ba70 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx @@ -4,10 +4,10 @@ import { ActionMenuEntryScope, ActionMenuEntryType, } from '@/action-menu/types/ActionMenuEntry'; +import { useComputeContextStoreFilters } from '@/context-store/hooks/useComputeContextStoreFilters'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -54,6 +54,8 @@ export const useDeleteMultipleRecordsAction = ({ contextStoreFiltersComponentState, ); + const { computeContextStoreFilters } = useComputeContextStoreFilters(); + const graphqlFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, diff --git a/packages/twenty-front/src/modules/context-store/hooks/useComputeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/hooks/useComputeContextStoreFilters.ts new file mode 100644 index 000000000000..1ea83fb37ddb --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/hooks/useComputeContextStoreFilters.ts @@ -0,0 +1,56 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; + +export const useComputeContextStoreFilters = () => { + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + + const computeContextStoreFilters = ( + contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, + contextStoreFilters: Filter[], + objectMetadataItem: ObjectMetadataItem, + ) => { + let queryFilter: RecordGqlOperationFilter | undefined; + + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + queryFilter = makeAndFilterVariables([ + computeViewRecordGqlOperationFilter( + contextStoreFilters, + objectMetadataItem?.fields ?? [], + [], + ), + contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 + ? { + not: { + id: { + in: contextStoreTargetedRecordsRule.excludedRecordIds, + }, + }, + } + : undefined, + ]); + } + if (contextStoreTargetedRecordsRule.mode === 'selection') { + queryFilter = + contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 + ? { + id: { + in: contextStoreTargetedRecordsRule.selectedRecordIds, + }, + } + : computeViewRecordGqlOperationFilter( + contextStoreFilters, + objectMetadataItem?.fields ?? [], + [], + ); + } + + return queryFilter; + }; + + return { computeContextStoreFilters }; +}; diff --git a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 4d6867e6d0f3..4e615b529aee 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -1,7 +1,7 @@ +import { useComputeContextStoreFilters } from '@/context-store/hooks/useComputeContextStoreFilters'; import { useContextStoreCurrentObjectMetadataIdOrThrow } from '@/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -30,6 +30,8 @@ export const useFindManyRecordsSelectedInContextStore = ({ instanceId, ); + const { computeContextStoreFilters } = useComputeContextStoreFilters(); + const queryFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts deleted file mode 100644 index e685c35f59aa..000000000000 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; - -export const computeContextStoreFilters = ( - contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, - contextStoreFilters: Filter[], - objectMetadataItem: ObjectMetadataItem, -) => { - let queryFilter: RecordGqlOperationFilter | undefined; - - if (contextStoreTargetedRecordsRule.mode === 'exclusion') { - queryFilter = makeAndFilterVariables([ - computeViewRecordGqlOperationFilter( - contextStoreFilters, - objectMetadataItem?.fields ?? [], - [], - ), - contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 - ? { - not: { - id: { - in: contextStoreTargetedRecordsRule.excludedRecordIds, - }, - }, - } - : undefined, - ]); - } - if (contextStoreTargetedRecordsRule.mode === 'selection') { - queryFilter = - contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 - ? { - id: { - in: contextStoreTargetedRecordsRule.selectedRecordIds, - }, - } - : computeViewRecordGqlOperationFilter( - contextStoreFilters, - objectMetadataItem?.fields ?? [], - [], - ); - } - - return queryFilter; -}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index 6e657d0bce21..5adb2fed29cb 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -5,13 +5,13 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { useResolveFilterValue } from '@/views/hooks/useResolveFilterValue'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue'; import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, } from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; -import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue'; import { useState } from 'react'; import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -31,13 +31,19 @@ export const ObjectFilterDropdownDateInput = () => { selectedOperandInDropdownState, ); + const { resolveFilterValue } = useResolveFilterValue(); + const selectedFilter = useRecoilValue(selectedFilterState) as | (Filter & { definition: { type: 'DATE' | 'DATE_TIME' } }) | null | undefined; const initialFilterValue = selectedFilter - ? resolveFilterValue(selectedFilter) + ? resolveFilterValue( + selectedFilter.definition.type, + selectedFilter.value, + selectedFilter.operand, + ) : null; const [internalDate, setInternalDate] = useState( initialFilterValue instanceof Date ? initialFilterValue : null, @@ -98,7 +104,11 @@ export const ObjectFilterDropdownDateInput = () => { selectedOperandInDropdown === ViewFilterOperand.IsRelative; const resolvedValue = selectedFilter - ? resolveFilterValue(selectedFilter) + ? resolveFilterValue( + selectedFilter.definition.type, + selectedFilter.value, + selectedFilter.operand, + ) : null; const relativeDate = diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 9a61611745bb..ff53a14ba7ea 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -64,7 +64,7 @@ export const ObjectFilterDropdownRecordSelect = ({ const { loading, filteredSelectedRecords, recordsToSelect, selectedRecords } = useRecordsForSelect({ searchFilterText: objectFilterDropdownSearchInput, - selectedIds: objectFilterDropdownSelectedRecordIds, + selectedRecordIdsAndSpecialIds: objectFilterDropdownSelectedRecordIds, objectNameSingular, limit: 10, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index b16db61e0ced..80fe613b5216 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -3,7 +3,7 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; @@ -53,6 +53,10 @@ export const useAggregateRecordsForRecordBoardColumn = () => { ); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); + + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + const requestFilters = computeViewRecordGqlOperationFilter( recordIndexFilters, objectMetadataItem.fields, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts new file mode 100644 index 000000000000..02214ff16016 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts @@ -0,0 +1,1000 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { + ActorFilter, + AddressFilter, + ArrayFilter, + BooleanFilter, + CurrencyFilter, + DateFilter, + EmailsFilter, + FloatFilter, + MultiSelectFilter, + RatingFilter, + RawJsonFilter, + RecordGqlOperationFilter, + RelationFilter, + SelectFilter, + StringFilter, +} from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { Field } from '~/generated/graphql'; +import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; +import { isDefined } from '~/utils/isDefined'; + +import { + convertGreaterThanRatingToArrayOfRatingValues, + convertLessThanRatingToArrayOfRatingValues, + convertRatingToRatingValue, +} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter'; +import { useResolveFilterValue } from '@/views/hooks/useResolveFilterValue'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; +import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; +import { z } from 'zod'; + +const useComputeFilterRecordGqlOperationFilter = () => { + const { resolveFilterValue } = useResolveFilterValue(); + + const computeFilterRecordGqlOperationFilter = ( + filter: Filter, + fields: Pick[], + ): RecordGqlOperationFilter | undefined => { + const correspondingField = fields.find( + (field) => field.id === filter.fieldMetadataId, + ); + + const compositeFieldName = filter.definition.compositeFieldName; + + const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); + + const isEmptyOperand = [ + ViewFilterOperand.IsEmpty, + ViewFilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ].includes(filter.operand); + + if (!correspondingField) { + return; + } + + if (!isEmptyOperand) { + if (!isDefined(filter.value) || filter.value === '') { + return; + } + } + + switch (filter.definition.type) { + case 'TEXT': + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + [correspondingField.name]: { + ilike: `%${filter.value}%`, + } as StringFilter, + }; + case ViewFilterOperand.DoesNotContain: + return { + not: { + [correspondingField.name]: { + ilike: `%${filter.value}%`, + } as StringFilter, + }, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'RAW_JSON': + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + [correspondingField.name]: { + like: `%${filter.value}%`, + } as RawJsonFilter, + }; + case ViewFilterOperand.DoesNotContain: + return { + not: { + [correspondingField.name]: { + like: `%${filter.value}%`, + } as RawJsonFilter, + }, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'DATE': + case 'DATE_TIME': { + const resolvedFilterValue = resolveFilterValue( + filter.definition.type, + filter.value, + filter.operand, + ); + const now = roundToNearestMinutes(new Date()); + const date = + resolvedFilterValue instanceof Date ? resolvedFilterValue : now; + + switch (filter.operand) { + case ViewFilterOperand.IsAfter: { + return { + [correspondingField.name]: { + gt: date.toISOString(), + } as DateFilter, + }; + } + case ViewFilterOperand.IsBefore: { + return { + [correspondingField.name]: { + lt: date.toISOString(), + } as DateFilter, + }; + } + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: { + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + } + case ViewFilterOperand.IsRelative: { + const dateRange = z + .object({ start: z.date(), end: z.date() }) + .safeParse(resolvedFilterValue).data; + + const defaultDateRange = resolveFilterValue( + 'DATE', + 'PAST_1_DAY', + ViewFilterOperand.IsRelative, + ); + + if (!defaultDateRange) { + throw new Error('Failed to resolve default date range'); + } + + const { start, end } = dateRange ?? defaultDateRange; + + return { + and: [ + { + [correspondingField.name]: { + gte: start.toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + lte: end.toISOString(), + } as DateFilter, + }, + ], + }; + } + case ViewFilterOperand.Is: { + const isValid = resolvedFilterValue instanceof Date; + const date = isValid ? resolvedFilterValue : now; + + return { + and: [ + { + [correspondingField.name]: { + lte: endOfDay(date).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(date).toISOString(), + } as DateFilter, + }, + ], + }; + } + case ViewFilterOperand.IsInPast: + return { + [correspondingField.name]: { + lte: now.toISOString(), + } as DateFilter, + }; + case ViewFilterOperand.IsInFuture: + return { + [correspondingField.name]: { + gte: now.toISOString(), + } as DateFilter, + }; + case ViewFilterOperand.IsToday: { + return { + and: [ + { + [correspondingField.name]: { + lte: endOfDay(now).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(now).toISOString(), + } as DateFilter, + }, + ], + }; + } + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, // + ); + } + } + case 'RATING': + switch (filter.operand) { + case ViewFilterOperand.Is: + return { + [correspondingField.name]: { + eq: convertRatingToRatingValue(parseFloat(filter.value)), + } as RatingFilter, + }; + case ViewFilterOperand.GreaterThan: + return { + [correspondingField.name]: { + in: convertGreaterThanRatingToArrayOfRatingValues( + parseFloat(filter.value), + ), + } as RatingFilter, + }; + case ViewFilterOperand.LessThan: + return { + [correspondingField.name]: { + in: convertLessThanRatingToArrayOfRatingValues( + parseFloat(filter.value), + ), + } as RatingFilter, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'NUMBER': + switch (filter.operand) { + case ViewFilterOperand.GreaterThan: + return { + [correspondingField.name]: { + gte: parseFloat(filter.value), + } as FloatFilter, + }; + case ViewFilterOperand.LessThan: + return { + [correspondingField.name]: { + lte: parseFloat(filter.value), + } as FloatFilter, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'RELATION': { + if (!isEmptyOperand) { + const recordIds = resolveFilterValue( + filter.definition.type, + filter.value, + filter.operand, + ); + + if (recordIds.length === 0) return; + switch (filter.operand) { + case ViewFilterOperand.Is: + return { + [correspondingField.name + 'Id']: { + in: recordIds, + } as RelationFilter, + }; + case ViewFilterOperand.IsNot: { + if (recordIds.length === 0) return; + return { + not: { + [correspondingField.name + 'Id']: { + in: recordIds, + } as RelationFilter, + }, + }; + } + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } else { + switch (filter.operand) { + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown empty operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + } + case 'CURRENCY': + switch (filter.operand) { + case ViewFilterOperand.GreaterThan: + return { + [correspondingField.name]: { + amountMicros: { gte: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }; + case ViewFilterOperand.LessThan: + return { + [correspondingField.name]: { + amountMicros: { lte: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'LINKS': { + const linksFilters = generateILikeFiltersForCompositeFields( + filter.value, + correspondingField.name, + ['primaryLinkLabel', 'primaryLinkUrl'], + ); + + switch (filter.operand) { + case ViewFilterOperand.Contains: + if (!isCompositeFieldFiter) { + return { + or: linksFilters, + }; + } else { + return { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + }, + }, + }; + } + case ViewFilterOperand.DoesNotContain: + if (!isCompositeFieldFiter) { + return { + and: linksFilters.map((filter) => { + return { + not: filter, + }; + }), + }; + } else { + return { + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + }, + }, + }, + }; + } + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + case 'FULL_NAME': { + const fullNameFilters = generateILikeFiltersForCompositeFields( + filter.value, + correspondingField.name, + ['firstName', 'lastName'], + ); + switch (filter.operand) { + case ViewFilterOperand.Contains: + if (!isCompositeFieldFiter) { + return { + or: fullNameFilters, + }; + } else { + return { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + }, + }, + }; + } + case ViewFilterOperand.DoesNotContain: + if (!isCompositeFieldFiter) { + return { + and: fullNameFilters.map((filter) => { + return { + not: filter, + }; + }), + }; + } else { + return { + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + }, + }, + }, + }; + } + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + case 'ADDRESS': + switch (filter.operand) { + case ViewFilterOperand.Contains: + if (!isCompositeFieldFiter) { + return { + or: [ + { + [correspondingField.name]: { + addressStreet1: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressStreet2: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCity: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressState: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCountry: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressPostcode: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + ], + }; + } else { + return { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + } as AddressFilter, + }, + }; + } + case ViewFilterOperand.DoesNotContain: + if (!isCompositeFieldFiter) { + return { + and: [ + { + not: { + [correspondingField.name]: { + addressStreet1: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + { + not: { + [correspondingField.name]: { + addressStreet2: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + { + not: { + [correspondingField.name]: { + addressCity: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + ], + }; + } else { + return { + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${filter.value}%`, + } as AddressFilter, + }, + }, + }; + } + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'MULTI_SELECT': { + if (isEmptyOperand) { + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + } + + const options = resolveFilterValue( + filter.definition.type, + filter.value, + filter.operand, + ); + + if (options.length === 0) return; + + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + [correspondingField.name]: { + containsAny: options, + } as MultiSelectFilter, + }; + case ViewFilterOperand.DoesNotContain: + return { + or: [ + { + not: { + [correspondingField.name]: { + containsAny: options, + } as MultiSelectFilter, + }, + }, + { + [correspondingField.name]: { + isEmptyArray: true, + } as MultiSelectFilter, + }, + { + [correspondingField.name]: { + is: 'NULL', + } as MultiSelectFilter, + }, + ], + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + case 'SELECT': { + if (isEmptyOperand) { + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + } + const options = resolveFilterValue( + filter.definition.type, + filter.value, + filter.operand, + ); + + if (options.length === 0) return; + + switch (filter.operand) { + case ViewFilterOperand.Is: + return { + [correspondingField.name]: { + in: options, + } as SelectFilter, + }; + case ViewFilterOperand.IsNot: + return { + not: { + [correspondingField.name]: { + in: options, + } as SelectFilter, + }, + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + case 'ARRAY': { + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + [correspondingField.name]: { + containsIlike: `%${filter.value}%`, + } as ArrayFilter, + }; + case ViewFilterOperand.DoesNotContain: + return { + not: { + [correspondingField.name]: { + containsIlike: `%${filter.value}%`, + } as ArrayFilter, + }, + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + // TODO: fix this with a new composite field in ViewFilter entity + case 'ACTOR': { + switch (filter.operand) { + case ViewFilterOperand.Is: { + const parsedRecordIds = JSON.parse(filter.value) as string[]; + + return { + [correspondingField.name]: { + source: { + in: parsedRecordIds, + } as RelationFilter, + }, + }; + } + case ViewFilterOperand.IsNot: { + const parsedRecordIds = JSON.parse(filter.value) as string[]; + + if (parsedRecordIds.length === 0) return; + + return { + not: { + [correspondingField.name]: { + source: { + in: parsedRecordIds, + } as RelationFilter, + }, + }, + }; + } + case ViewFilterOperand.Contains: + return { + or: [ + { + [correspondingField.name]: { + name: { + ilike: `%${filter.value}%`, + }, + } as ActorFilter, + }, + ], + }; + case ViewFilterOperand.DoesNotContain: + return { + and: [ + { + not: { + [correspondingField.name]: { + name: { + ilike: `%${filter.value}%`, + }, + } as ActorFilter, + }, + }, + ], + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.label} filter`, + ); + } + } + case 'EMAILS': + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + or: [ + { + [correspondingField.name]: { + primaryEmail: { + ilike: `%${filter.value}%`, + }, + } as EmailsFilter, + }, + ], + }; + case ViewFilterOperand.DoesNotContain: + return { + and: [ + { + not: { + [correspondingField.name]: { + primaryEmail: { + ilike: `%${filter.value}%`, + }, + } as EmailsFilter, + }, + }, + ], + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + case 'PHONES': { + const phonesFilters = generateILikeFiltersForCompositeFields( + filter.value, + correspondingField.name, + ['primaryPhoneNumber', 'primaryPhoneCountryCode'], + ); + switch (filter.operand) { + case ViewFilterOperand.Contains: + return { + or: phonesFilters, + }; + case ViewFilterOperand.DoesNotContain: + return { + and: phonesFilters.map((filter) => { + return { + not: filter, + }; + }), + }; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + return getEmptyRecordGqlOperationFilter( + filter.operand, + correspondingField, + filter.definition, + ); + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + case 'BOOLEAN': { + return { + [correspondingField.name]: { + eq: filter.value === 'true', + } as BooleanFilter, + }; + } + default: + throw new Error('Unknown filter type'); + } + }; + + return { computeFilterRecordGqlOperationFilter }; +}; + +const useComputeViewFilterGroupRecordGqlOperationFilter = () => { + const { computeFilterRecordGqlOperationFilter } = + useComputeFilterRecordGqlOperationFilter(); + + const computeViewFilterGroupRecordGqlOperationFilter = ( + filters: Filter[], + fields: Pick[], + viewFilterGroups: ViewFilterGroup[], + currentViewFilterGroupId?: string, + ): RecordGqlOperationFilter | undefined => { + const currentViewFilterGroup = viewFilterGroups.find( + (viewFilterGroup) => viewFilterGroup.id === currentViewFilterGroupId, + ); + + if (!currentViewFilterGroup) { + return; + } + + const groupFilters = filters.filter( + (filter) => filter.viewFilterGroupId === currentViewFilterGroupId, + ); + + const groupRecordGqlOperationFilters = groupFilters + .map((filter) => computeFilterRecordGqlOperationFilter(filter, fields)) + .filter(isDefined); + + const subGroupRecordGqlOperationFilters = viewFilterGroups + .filter( + (viewFilterGroup) => + viewFilterGroup.parentViewFilterGroupId === currentViewFilterGroupId, + ) + .map((subViewFilterGroup) => + computeViewFilterGroupRecordGqlOperationFilter( + filters, + fields, + viewFilterGroups, + subViewFilterGroup.id, + ), + ) + .filter(isDefined); + + if ( + currentViewFilterGroup.logicalOperator === + ViewFilterGroupLogicalOperator.AND + ) { + return { + and: [ + ...groupRecordGqlOperationFilters, + ...subGroupRecordGqlOperationFilters, + ], + }; + } else if ( + currentViewFilterGroup.logicalOperator === ViewFilterGroupLogicalOperator.OR + ) { + return { + or: [ + ...groupRecordGqlOperationFilters, + ...subGroupRecordGqlOperationFilters, + ], + }; + } else { + throw new Error( + `Unknown logical operator ${currentViewFilterGroup.logicalOperator}`, + ); + } + }; + + return { computeViewFilterGroupRecordGqlOperationFilter }; +}; + +export const useComputeViewRecordGqlOperationFilter = () => { + const { computeFilterRecordGqlOperationFilter } = + useComputeFilterRecordGqlOperationFilter(); + const { computeViewFilterGroupRecordGqlOperationFilter } = + useComputeViewFilterGroupRecordGqlOperationFilter(); + + const computeViewRecordGqlOperationFilter = ( + filters: Filter[], + fields: Pick[], + viewFilterGroups: ViewFilterGroup[], + ): RecordGqlOperationFilter => { + const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = filters + .filter((filter) => !filter.viewFilterGroupId) + .map((regularFilter) => + computeFilterRecordGqlOperationFilter(regularFilter, fields), + ) + .filter(isDefined); + + const outermostFilterGroupId = viewFilterGroups.find( + (viewFilterGroup) => !viewFilterGroup.parentViewFilterGroupId, + )?.id; + + const advancedRecordGqlOperationFilter = + computeViewFilterGroupRecordGqlOperationFilter( + filters, + fields, + viewFilterGroups, + outermostFilterGroupId, + ); + + const recordGqlOperationFilters = [ + ...regularRecordGqlOperationFilter, + advancedRecordGqlOperationFilter, + ].filter(isDefined); + + if (recordGqlOperationFilters.length === 0) { + return {}; + } + + if (recordGqlOperationFilters.length === 1) { + return recordGqlOperationFilters[0]; + } + + const recordGqlOperationFilter = { + and: recordGqlOperationFilters, + }; + + return recordGqlOperationFilter; + }; + + return { computeViewRecordGqlOperationFilter }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts deleted file mode 100644 index 3a342704439e..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts +++ /dev/null @@ -1,976 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; - -import { - ActorFilter, - AddressFilter, - ArrayFilter, - BooleanFilter, - CurrencyFilter, - DateFilter, - EmailsFilter, - FloatFilter, - MultiSelectFilter, - RatingFilter, - RawJsonFilter, - RecordGqlOperationFilter, - RelationFilter, - SelectFilter, - StringFilter, -} from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { Field } from '~/generated/graphql'; -import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; -import { isDefined } from '~/utils/isDefined'; - -import { - convertGreaterThanRatingToArrayOfRatingValues, - convertLessThanRatingToArrayOfRatingValues, - convertRatingToRatingValue, -} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter'; -import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; -import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; -import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue'; -import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; -import { z } from 'zod'; - -const computeFilterRecordGqlOperationFilter = ( - filter: Filter, - fields: Pick[], -): RecordGqlOperationFilter | undefined => { - const correspondingField = fields.find( - (field) => field.id === filter.fieldMetadataId, - ); - - const compositeFieldName = filter.definition.compositeFieldName; - - const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); - - const isEmptyOperand = [ - ViewFilterOperand.IsEmpty, - ViewFilterOperand.IsNotEmpty, - ViewFilterOperand.IsInPast, - ViewFilterOperand.IsInFuture, - ViewFilterOperand.IsToday, - ].includes(filter.operand); - - if (!correspondingField) { - return; - } - - if (!isEmptyOperand) { - if (!isDefined(filter.value) || filter.value === '') { - return; - } - } - - switch (filter.definition.type) { - case 'TEXT': - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - [correspondingField.name]: { - ilike: `%${filter.value}%`, - } as StringFilter, - }; - case ViewFilterOperand.DoesNotContain: - return { - not: { - [correspondingField.name]: { - ilike: `%${filter.value}%`, - } as StringFilter, - }, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'RAW_JSON': - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - [correspondingField.name]: { - like: `%${filter.value}%`, - } as RawJsonFilter, - }; - case ViewFilterOperand.DoesNotContain: - return { - not: { - [correspondingField.name]: { - like: `%${filter.value}%`, - } as RawJsonFilter, - }, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'DATE': - case 'DATE_TIME': { - const resolvedFilterValue = resolveFilterValue(filter); - const now = roundToNearestMinutes(new Date()); - const date = - resolvedFilterValue instanceof Date ? resolvedFilterValue : now; - - switch (filter.operand) { - case ViewFilterOperand.IsAfter: { - return { - [correspondingField.name]: { - gt: date.toISOString(), - } as DateFilter, - }; - } - case ViewFilterOperand.IsBefore: { - return { - [correspondingField.name]: { - lt: date.toISOString(), - } as DateFilter, - }; - } - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } - case ViewFilterOperand.IsRelative: { - const dateRange = z - .object({ start: z.date(), end: z.date() }) - .safeParse(resolvedFilterValue).data; - - const defaultDateRange = resolveFilterValue({ - value: 'PAST_1_DAY', - definition: { - type: 'DATE', - }, - operand: ViewFilterOperand.IsRelative, - }); - - if (!defaultDateRange) { - throw new Error('Failed to resolve default date range'); - } - - const { start, end } = dateRange ?? defaultDateRange; - - return { - and: [ - { - [correspondingField.name]: { - gte: start.toISOString(), - } as DateFilter, - }, - { - [correspondingField.name]: { - lte: end.toISOString(), - } as DateFilter, - }, - ], - }; - } - case ViewFilterOperand.Is: { - const isValid = resolvedFilterValue instanceof Date; - const date = isValid ? resolvedFilterValue : now; - - return { - and: [ - { - [correspondingField.name]: { - lte: endOfDay(date).toISOString(), - } as DateFilter, - }, - { - [correspondingField.name]: { - gte: startOfDay(date).toISOString(), - } as DateFilter, - }, - ], - }; - } - case ViewFilterOperand.IsInPast: - return { - [correspondingField.name]: { - lte: now.toISOString(), - } as DateFilter, - }; - case ViewFilterOperand.IsInFuture: - return { - [correspondingField.name]: { - gte: now.toISOString(), - } as DateFilter, - }; - case ViewFilterOperand.IsToday: { - return { - and: [ - { - [correspondingField.name]: { - lte: endOfDay(now).toISOString(), - } as DateFilter, - }, - { - [correspondingField.name]: { - gte: startOfDay(now).toISOString(), - } as DateFilter, - }, - ], - }; - } - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, // - ); - } - } - case 'RATING': - switch (filter.operand) { - case ViewFilterOperand.Is: - return { - [correspondingField.name]: { - eq: convertRatingToRatingValue(parseFloat(filter.value)), - } as RatingFilter, - }; - case ViewFilterOperand.GreaterThan: - return { - [correspondingField.name]: { - in: convertGreaterThanRatingToArrayOfRatingValues( - parseFloat(filter.value), - ), - } as RatingFilter, - }; - case ViewFilterOperand.LessThan: - return { - [correspondingField.name]: { - in: convertLessThanRatingToArrayOfRatingValues( - parseFloat(filter.value), - ), - } as RatingFilter, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'NUMBER': - switch (filter.operand) { - case ViewFilterOperand.GreaterThan: - return { - [correspondingField.name]: { - gte: parseFloat(filter.value), - } as FloatFilter, - }; - case ViewFilterOperand.LessThan: - return { - [correspondingField.name]: { - lte: parseFloat(filter.value), - } as FloatFilter, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'RELATION': { - if (!isEmptyOperand) { - try { - JSON.parse(filter.value); - } catch (e) { - throw new Error( - `Cannot parse filter value for RELATION filter : "${filter.value}"`, - ); - } - - const parsedRecordIds = JSON.parse(filter.value) as string[]; - - if (parsedRecordIds.length === 0) return; - switch (filter.operand) { - case ViewFilterOperand.Is: - return { - [correspondingField.name + 'Id']: { - in: parsedRecordIds, - } as RelationFilter, - }; - case ViewFilterOperand.IsNot: { - if (parsedRecordIds.length === 0) return; - return { - not: { - [correspondingField.name + 'Id']: { - in: parsedRecordIds, - } as RelationFilter, - }, - }; - } - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } else { - switch (filter.operand) { - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown empty operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - } - case 'CURRENCY': - switch (filter.operand) { - case ViewFilterOperand.GreaterThan: - return { - [correspondingField.name]: { - amountMicros: { gte: parseFloat(filter.value) * 1000000 }, - } as CurrencyFilter, - }; - case ViewFilterOperand.LessThan: - return { - [correspondingField.name]: { - amountMicros: { lte: parseFloat(filter.value) * 1000000 }, - } as CurrencyFilter, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'LINKS': { - const linksFilters = generateILikeFiltersForCompositeFields( - filter.value, - correspondingField.name, - ['primaryLinkLabel', 'primaryLinkUrl'], - ); - - switch (filter.operand) { - case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { - return { - or: linksFilters, - }; - } else { - return { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }; - } - case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { - return { - and: linksFilters.map((filter) => { - return { - not: filter, - }; - }), - }; - } else { - return { - not: { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }, - }; - } - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - case 'FULL_NAME': { - const fullNameFilters = generateILikeFiltersForCompositeFields( - filter.value, - correspondingField.name, - ['firstName', 'lastName'], - ); - switch (filter.operand) { - case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { - return { - or: fullNameFilters, - }; - } else { - return { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }; - } - case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { - return { - and: fullNameFilters.map((filter) => { - return { - not: filter, - }; - }), - }; - } else { - return { - not: { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }, - }; - } - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - case 'ADDRESS': - switch (filter.operand) { - case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { - return { - or: [ - { - [correspondingField.name]: { - addressStreet1: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressStreet2: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCity: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressState: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCountry: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressPostcode: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - ], - }; - } else { - return { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - } as AddressFilter, - }, - }; - } - case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { - return { - and: [ - { - not: { - [correspondingField.name]: { - addressStreet1: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - }, - { - not: { - [correspondingField.name]: { - addressStreet2: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - }, - { - not: { - [correspondingField.name]: { - addressCity: { - ilike: `%${filter.value}%`, - }, - } as AddressFilter, - }, - }, - ], - }; - } else { - return { - not: { - [correspondingField.name]: { - [compositeFieldName]: { - ilike: `%${filter.value}%`, - } as AddressFilter, - }, - }, - }; - } - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'MULTI_SELECT': { - if (isEmptyOperand) { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } - - const options = resolveFilterValue( - filter as Filter & { definition: { type: 'MULTI_SELECT' } }, - ); - - if (options.length === 0) return; - - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - [correspondingField.name]: { - containsAny: options, - } as MultiSelectFilter, - }; - case ViewFilterOperand.DoesNotContain: - return { - or: [ - { - not: { - [correspondingField.name]: { - containsAny: options, - } as MultiSelectFilter, - }, - }, - { - [correspondingField.name]: { - isEmptyArray: true, - } as MultiSelectFilter, - }, - { - [correspondingField.name]: { - is: 'NULL', - } as MultiSelectFilter, - }, - ], - }; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - case 'SELECT': { - if (isEmptyOperand) { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } - const options = resolveFilterValue( - filter as Filter & { definition: { type: 'SELECT' } }, - ); - - if (options.length === 0) return; - - switch (filter.operand) { - case ViewFilterOperand.Is: - return { - [correspondingField.name]: { - in: options, - } as SelectFilter, - }; - case ViewFilterOperand.IsNot: - return { - not: { - [correspondingField.name]: { - in: options, - } as SelectFilter, - }, - }; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - case 'ARRAY': { - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - [correspondingField.name]: { - containsIlike: `%${filter.value}%`, - } as ArrayFilter, - }; - case ViewFilterOperand.DoesNotContain: - return { - not: { - [correspondingField.name]: { - containsIlike: `%${filter.value}%`, - } as ArrayFilter, - }, - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - // TODO: fix this with a new composite field in ViewFilter entity - case 'ACTOR': { - switch (filter.operand) { - case ViewFilterOperand.Is: { - const parsedRecordIds = JSON.parse(filter.value) as string[]; - - return { - [correspondingField.name]: { - source: { - in: parsedRecordIds, - } as RelationFilter, - }, - }; - } - case ViewFilterOperand.IsNot: { - const parsedRecordIds = JSON.parse(filter.value) as string[]; - - if (parsedRecordIds.length === 0) return; - - return { - not: { - [correspondingField.name]: { - source: { - in: parsedRecordIds, - } as RelationFilter, - }, - }, - }; - } - case ViewFilterOperand.Contains: - return { - or: [ - { - [correspondingField.name]: { - name: { - ilike: `%${filter.value}%`, - }, - } as ActorFilter, - }, - ], - }; - case ViewFilterOperand.DoesNotContain: - return { - and: [ - { - not: { - [correspondingField.name]: { - name: { - ilike: `%${filter.value}%`, - }, - } as ActorFilter, - }, - }, - ], - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.label} filter`, - ); - } - } - case 'EMAILS': - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - or: [ - { - [correspondingField.name]: { - primaryEmail: { - ilike: `%${filter.value}%`, - }, - } as EmailsFilter, - }, - ], - }; - case ViewFilterOperand.DoesNotContain: - return { - and: [ - { - not: { - [correspondingField.name]: { - primaryEmail: { - ilike: `%${filter.value}%`, - }, - } as EmailsFilter, - }, - }, - ], - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - case 'PHONES': { - const phonesFilters = generateILikeFiltersForCompositeFields( - filter.value, - correspondingField.name, - ['primaryPhoneNumber', 'primaryPhoneCountryCode'], - ); - switch (filter.operand) { - case ViewFilterOperand.Contains: - return { - or: phonesFilters, - }; - case ViewFilterOperand.DoesNotContain: - return { - and: phonesFilters.map((filter) => { - return { - not: filter, - }; - }), - }; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } - case 'BOOLEAN': { - return { - [correspondingField.name]: { - eq: filter.value === 'true', - } as BooleanFilter, - }; - } - default: - throw new Error('Unknown filter type'); - } -}; - -const computeViewFilterGroupRecordGqlOperationFilter = ( - filters: Filter[], - fields: Pick[], - viewFilterGroups: ViewFilterGroup[], - currentViewFilterGroupId?: string, -): RecordGqlOperationFilter | undefined => { - const currentViewFilterGroup = viewFilterGroups.find( - (viewFilterGroup) => viewFilterGroup.id === currentViewFilterGroupId, - ); - - if (!currentViewFilterGroup) { - return; - } - - const groupFilters = filters.filter( - (filter) => filter.viewFilterGroupId === currentViewFilterGroupId, - ); - - const groupRecordGqlOperationFilters = groupFilters - .map((filter) => computeFilterRecordGqlOperationFilter(filter, fields)) - .filter(isDefined); - - const subGroupRecordGqlOperationFilters = viewFilterGroups - .filter( - (viewFilterGroup) => - viewFilterGroup.parentViewFilterGroupId === currentViewFilterGroupId, - ) - .map((subViewFilterGroup) => - computeViewFilterGroupRecordGqlOperationFilter( - filters, - fields, - viewFilterGroups, - subViewFilterGroup.id, - ), - ) - .filter(isDefined); - - if ( - currentViewFilterGroup.logicalOperator === - ViewFilterGroupLogicalOperator.AND - ) { - return { - and: [ - ...groupRecordGqlOperationFilters, - ...subGroupRecordGqlOperationFilters, - ], - }; - } else if ( - currentViewFilterGroup.logicalOperator === ViewFilterGroupLogicalOperator.OR - ) { - return { - or: [ - ...groupRecordGqlOperationFilters, - ...subGroupRecordGqlOperationFilters, - ], - }; - } else { - throw new Error( - `Unknown logical operator ${currentViewFilterGroup.logicalOperator}`, - ); - } -}; - -export const computeViewRecordGqlOperationFilter = ( - filters: Filter[], - fields: Pick[], - viewFilterGroups: ViewFilterGroup[], -): RecordGqlOperationFilter => { - const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = filters - .filter((filter) => !filter.viewFilterGroupId) - .map((regularFilter) => - computeFilterRecordGqlOperationFilter(regularFilter, fields), - ) - .filter(isDefined); - - const outermostFilterGroupId = viewFilterGroups.find( - (viewFilterGroup) => !viewFilterGroup.parentViewFilterGroupId, - )?.id; - - const advancedRecordGqlOperationFilter = - computeViewFilterGroupRecordGqlOperationFilter( - filters, - fields, - viewFilterGroups, - outermostFilterGroupId, - ); - - const recordGqlOperationFilters = [ - ...regularRecordGqlOperationFilter, - advancedRecordGqlOperationFilter, - ].filter(isDefined); - - if (recordGqlOperationFilters.length === 0) { - return {}; - } - - if (recordGqlOperationFilters.length === 1) { - return recordGqlOperationFilters[0]; - } - - const recordGqlOperationFilter = { - and: recordGqlOperationFilters, - }; - - return recordGqlOperationFilter; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index 2c8106f6d7e6..fa7853a73524 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -1,7 +1,7 @@ +import { useComputeContextStoreFilters } from '@/context-store/hooks/useComputeContextStoreFilters'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -40,6 +40,8 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreFiltersComponentState, ); + const { computeContextStoreFilters } = useComputeContextStoreFilters(); + const { totalCount } = useFindManyRecords({ ...findManyRecordsParams, recordGqlFields: { diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts index bc256a8bcfc6..2109d1572d8d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts @@ -5,9 +5,9 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; +import { useComputeContextStoreFilters } from '@/context-store/hooks/useComputeContextStoreFilters'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; @@ -88,6 +88,8 @@ export const useExportFetchRecords = ({ contextStoreFiltersComponentState, ); + const { computeContextStoreFilters } = useComputeContextStoreFilters(); + const queryFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index e77e993ab708..292d98ae482a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -1,6 +1,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState'; @@ -32,6 +32,9 @@ export const useFindManyRecordIndexTableParams = ( recordTableId, ); + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + const stateFilter = computeViewRecordGqlOperationFilter( tableFilters, objectMetadataItem?.fields ?? [], diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index fee8cc97aa9a..95408bf563d4 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -7,7 +7,7 @@ import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/useSetRecordBoardRecordIds'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -56,6 +56,9 @@ export const useLoadRecordIndexBoard = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + const requestFilters = computeViewRecordGqlOperationFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 2738118054fe..da6573b7dd86 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -43,6 +43,9 @@ export const useLoadRecordIndexBoardColumn = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + const requestFilters = computeViewRecordGqlOperationFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index cc4729603de1..b74781b437c8 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -10,18 +10,19 @@ import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFil import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { OrderBy } from '@/types/OrderBy'; +import { useMapRelationViewFilterValueSpecialIdsToRecordIds } from '@/views/view-filter-value/hooks/useResolveRelationViewFilterValue'; export const useRecordsForSelect = ({ searchFilterText, sortOrder = 'AscNullsLast', - selectedIds, + selectedRecordIdsAndSpecialIds, limit, excludedRecordIds = [], objectNameSingular, }: { searchFilterText: string; sortOrder?: OrderBy; - selectedIds: string[]; + selectedRecordIdsAndSpecialIds: string[]; limit?: number; excludedRecordIds?: string[]; objectNameSingular: string; @@ -41,15 +42,22 @@ export const useRecordsForSelect = ({ objectNameSingular, }); + const { mapRelationViewFilterValueSpecialIdsToRecordIds } = + useMapRelationViewFilterValueSpecialIdsToRecordIds(); + + const selectedRecordIds = mapRelationViewFilterValueSpecialIdsToRecordIds( + selectedRecordIdsAndSpecialIds, + ); + const orderByField = getObjectOrderByField(sortOrder); - const selectedIdsFilter = { id: { in: selectedIds } }; + const selectedIdsFilter = { id: { in: selectedRecordIds } }; const { loading: selectedRecordsLoading, records: selectedRecordsData } = useFindManyRecords({ filter: selectedIdsFilter, orderBy: orderByField, objectNameSingular, - skip: !selectedIds.length, + skip: !selectedRecordIds.length, }); const searchFilters = filters.map(({ fieldNames, filter }) => { @@ -88,10 +96,10 @@ export const useRecordsForSelect = ({ filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), orderBy: orderByField, objectNameSingular, - skip: !selectedIds.length, + skip: !selectedRecordIds.length, }); - const notFilterIds = [...selectedIds, ...excludedRecordIds]; + const notFilterIds = [...selectedRecordIds, ...excludedRecordIds]; const notFilter = notFilterIds.length ? { not: { id: { in: notFilterIds } } } : undefined; @@ -107,7 +115,7 @@ export const useRecordsForSelect = ({ objectNameSingular === 'workspaceMember' ? [ { - id: 'me', + id: 'CURRENT_WORKSPACE_MEMBER', name: 'Me', isSelected: false, }, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index 78bd4310f98b..929096c8fc41 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -9,10 +9,10 @@ import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-impor import { ImportedStructuredRow } from '@/spreadsheet-import/types'; import { isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-ui'; -import { z } from 'zod'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { castToString } from '~/utils/castToString'; import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros'; +import { safeStringArrayJSONSchema } from '~/utils/validation-schemas/safeStringArrayJSONSchema'; export const buildRecordFromImportedStructuredRow = ( importedStructuredRow: ImportedStructuredRow, @@ -206,21 +206,8 @@ export const buildRecordFromImportedStructuredRow = ( break; case FieldMetadataType.Array: case FieldMetadataType.MultiSelect: { - const stringArrayJSONSchema = z - .preprocess((value) => { - try { - if (typeof value !== 'string') { - return []; - } - return JSON.parse(value); - } catch { - return []; - } - }, z.array(z.string())) - .catch([]); - recordToBuild[field.name] = - stringArrayJSONSchema.parse(importedFieldValue); + safeStringArrayJSONSchema.parse(importedFieldValue); break; } case FieldMetadataType.RawJson: { diff --git a/packages/twenty-front/src/modules/views/hooks/useGetQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/hooks/useGetQueryVariablesFromView.ts new file mode 100644 index 000000000000..23bc62eed455 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useGetQueryVariablesFromView.ts @@ -0,0 +1,65 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; +import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { useComputeViewRecordGqlOperationFilter } from '@/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter'; +import { View } from '@/views/types/View'; +import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; +import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; +import { isDefined } from '~/utils/isDefined'; + +export const useGetQueryVariablesFromView = () => { + const { computeViewRecordGqlOperationFilter } = + useComputeViewRecordGqlOperationFilter(); + + const getQueryVariablesFromView = ({ + view, + fieldMetadataItems, + objectMetadataItem, + isJsonFilterEnabled, + }: { + view: View | null | undefined; + fieldMetadataItems: FieldMetadataItem[]; + objectMetadataItem: ObjectMetadataItem; + isJsonFilterEnabled: boolean; + }) => { + if (!isDefined(view)) { + return { + filter: undefined, + orderBy: undefined, + }; + } + + const { viewFilterGroups, viewFilters, viewSorts } = view; + + const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ + fields: fieldMetadataItems, + isJsonFilterEnabled, + }); + + const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ + fields: fieldMetadataItems, + }); + + const filter = computeViewRecordGqlOperationFilter( + mapViewFiltersToFilters(viewFilters, filterDefinitions), + objectMetadataItem?.fields ?? [], + viewFilterGroups ?? [], + ); + + const orderBy = turnSortsIntoOrderBy( + objectMetadataItem, + mapViewSortsToSorts(viewSorts, sortDefinitions), + ); + + return { + filter, + orderBy, + }; + }; + + return { + getQueryVariablesFromView, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts index 4b28e5e781f9..6fb0b4dca8bd 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts @@ -1,7 +1,7 @@ import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useGetQueryVariablesFromView } from '@/views/hooks/useGetQueryVariablesFromView'; import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; -import { getQueryVariablesFromView } from '@/views/utils/getQueryVariablesFromView'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ @@ -11,6 +11,8 @@ export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ objectMetadataItem: ObjectMetadataItem; viewId: string | null | undefined; }) => { + const { getQueryVariablesFromView } = useGetQueryVariablesFromView(); + const { view } = useViewOrDefaultViewFromPrefetchedViews({ objectMetadataItemId: objectMetadataItem.id, viewId, diff --git a/packages/twenty-front/src/modules/views/hooks/useResolveFilterValue.ts b/packages/twenty-front/src/modules/views/hooks/useResolveFilterValue.ts new file mode 100644 index 000000000000..544ec1d1a129 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useResolveFilterValue.ts @@ -0,0 +1,67 @@ +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { useResolveRelationViewFilterValue } from '@/views/view-filter-value/hooks/useResolveRelationViewFilterValue'; +import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue'; +import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue'; +import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue'; +import { + resolveDateViewFilterValue, + ResolvedDateViewFilterValue, +} from '../view-filter-value/utils/resolveDateViewFilterValue'; + +type ResolvedFilterValue< + T extends FilterableFieldType, + O extends ViewFilterOperand, +> = T extends 'DATE' | 'DATE_TIME' + ? ResolvedDateViewFilterValue + : T extends 'NUMBER' + ? ReturnType + : T extends 'SELECT' | 'MULTI_SELECT' + ? string[] + : T extends 'BOOLEAN' + ? boolean + : T extends 'RELATION' + ? string[] + : string; + +// TODO: Convert all resolve functions into hooks +export const useResolveFilterValue = () => { + const { resolveRelationViewFilterValue } = useResolveRelationViewFilterValue(); + + const resolveFilterValue = < + T extends FilterableFieldType, + O extends ViewFilterOperand, + >( + filterDefinitionType: T, + viewFilterValue: string, + viewFilterOperand: O, + ): ResolvedFilterValue => { + switch (filterDefinitionType) { + case 'DATE': + case 'DATE_TIME': + return resolveDateViewFilterValue( + viewFilterValue, + viewFilterOperand, + ) as ResolvedFilterValue; + /* case 'NUMBER': + return resolveNumberViewFilterValue(viewFilterValue); */ + case 'SELECT': + case 'MULTI_SELECT': + return resolveSelectViewFilterValue( + viewFilterValue, + ) as ResolvedFilterValue; + case 'BOOLEAN': + return resolveBooleanViewFilterValue( + viewFilterValue, + ) as ResolvedFilterValue; + case 'RELATION': + return resolveRelationViewFilterValue( + viewFilterValue + ) as ResolvedFilterValue; + default: + return viewFilterValue as ResolvedFilterValue; + } + }; + + return { resolveFilterValue }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts deleted file mode 100644 index 7768035b6359..000000000000 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; -import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; -import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { View } from '@/views/types/View'; -import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; -import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; -import { isDefined } from '~/utils/isDefined'; - -export const getQueryVariablesFromView = ({ - view, - fieldMetadataItems, - objectMetadataItem, - isJsonFilterEnabled, -}: { - view: View | null | undefined; - fieldMetadataItems: FieldMetadataItem[]; - objectMetadataItem: ObjectMetadataItem; - isJsonFilterEnabled: boolean; -}) => { - if (!isDefined(view)) { - return { - filter: undefined, - orderBy: undefined, - }; - } - - const { viewFilterGroups, viewFilters, viewSorts } = view; - - const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ - fields: fieldMetadataItems, - isJsonFilterEnabled, - }); - - const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ - fields: fieldMetadataItems, - }); - - const filter = computeViewRecordGqlOperationFilter( - mapViewFiltersToFilters(viewFilters, filterDefinitions), - objectMetadataItem?.fields ?? [], - viewFilterGroups ?? [], - ); - - const orderBy = turnSortsIntoOrderBy( - objectMetadataItem, - mapViewSortsToSorts(viewSorts, sortDefinitions), - ); - - return { - filter, - orderBy, - }; -}; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/hooks/useResolveRelationViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/hooks/useResolveRelationViewFilterValue.ts new file mode 100644 index 000000000000..8f3c3f13aab6 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/hooks/useResolveRelationViewFilterValue.ts @@ -0,0 +1,38 @@ +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useRecoilValue } from 'recoil'; +import { safeStringArrayJSONSchema } from '~/utils/validation-schemas/safeStringArrayJSONSchema'; + +export const useMapRelationViewFilterValueSpecialIdsToRecordIds = () => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const mapRelationViewFilterValueSpecialIdsToRecordIds = ( + recordIdsAndSpecialIds: string[], + ) => + recordIdsAndSpecialIds.map((recordIdOrSpecialId) => { + if (!currentWorkspaceMember) { + throw new Error('Current workspace member is not defined'); + } + + return recordIdOrSpecialId === 'CURRENT_WORKSPACE_MEMBER' + ? currentWorkspaceMember.id + : recordIdOrSpecialId; + }); + + return { mapRelationViewFilterValueSpecialIdsToRecordIds }; +}; + +export const useResolveRelationViewFilterValue = () => { + const { mapRelationViewFilterValueSpecialIdsToRecordIds } = + useMapRelationViewFilterValueSpecialIdsToRecordIds(); + + const resolveRelationViewFilterValue = (viewFilterValue: string) => { + const recordIdsAndSpecialIds = + safeStringArrayJSONSchema.parse(viewFilterValue); + + return mapRelationViewFilterValueSpecialIdsToRecordIds( + recordIdsAndSpecialIds, + ); + }; + + return { resolveRelationViewFilterValue }; +}; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveBooleanViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveBooleanViewFilterValue.ts index 0112db9e84f4..f10cd634cd0c 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveBooleanViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveBooleanViewFilterValue.ts @@ -1,7 +1,2 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; - -export const resolveBooleanViewFilterValue = ( - viewFilter: Pick, -) => { - return viewFilter.value === 'true'; -}; +export const resolveBooleanViewFilterValue = (viewFilterValue: string) => + viewFilterValue === 'true'; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts index da940310505c..8c165002b407 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts @@ -1,4 +1,3 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { addDays, @@ -159,32 +158,28 @@ const resolveVariableDateViewFilterValueFromRelativeDate = (relativeDate: { } }; -const resolveVariableDateViewFilterValue = (value?: string | null) => { - if (!value) return null; - +const resolveVariableDateViewFilterValue = (value: string) => { const relativeDate = variableDateViewFilterValueSchema.parse(value); return resolveVariableDateViewFilterValueFromRelativeDate(relativeDate); }; +export type ResolvedRelativeDateFilterValue = ReturnType< + typeof resolveVariableDateViewFilterValue +>; + export type ResolvedDateViewFilterValue = O extends ViewFilterOperand.IsRelative - ? ReturnType + ? ResolvedRelativeDateFilterValue : Date | null; -type PartialViewFilter = Pick< - ViewFilter, - 'value' -> & { operand: O }; - export const resolveDateViewFilterValue = ( - viewFilter: PartialViewFilter, + viewFilterValue: string, + viewFilterOperand: O, ): ResolvedDateViewFilterValue => { - if (!viewFilter.value) return null; - - if (viewFilter.operand === ViewFilterOperand.IsRelative) { + if (viewFilterOperand === ViewFilterOperand.IsRelative) { return resolveVariableDateViewFilterValue( - viewFilter.value, + viewFilterValue, ) as ResolvedDateViewFilterValue; } - return new Date(viewFilter.value) as ResolvedDateViewFilterValue; + return new Date(viewFilterValue) as ResolvedDateViewFilterValue; }; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts deleted file mode 100644 index 042bd09c9998..000000000000 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; -import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue'; -import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue'; -import { resolveRelationViewFilterValue } from '@/views/view-filter-value/utils/resolveRelationViewFilterValue'; -import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue'; -import { - resolveDateViewFilterValue, - ResolvedDateViewFilterValue, -} from './resolveDateViewFilterValue'; - -type ResolvedFilterValue< - T extends FilterableFieldType, - O extends ViewFilterOperand, -> = T extends 'DATE' | 'DATE_TIME' - ? ResolvedDateViewFilterValue - : T extends 'NUMBER' - ? ReturnType - : T extends 'SELECT' | 'MULTI_SELECT' - ? string[] - : T extends 'BOOLEAN' - ? boolean - : T extends 'RELATION' - ? string - : string; - -type PartialFilter< - T extends FilterableFieldType, - O extends ViewFilterOperand, -> = Pick & { - definition: { type: T }; - operand: O; -}; - -export const resolveFilterValue = < - T extends FilterableFieldType, - O extends ViewFilterOperand, ->( - filter: PartialFilter, -) => { - switch (filter.definition.type) { - case 'DATE': - case 'DATE_TIME': - return resolveDateViewFilterValue(filter) as ResolvedFilterValue; - case 'NUMBER': - return resolveNumberViewFilterValue(filter) as ResolvedFilterValue; - case 'SELECT': - case 'MULTI_SELECT': - return resolveSelectViewFilterValue(filter) as ResolvedFilterValue; - case 'BOOLEAN': - return resolveBooleanViewFilterValue(filter) as ResolvedFilterValue; - case 'RELATION': - return resolveRelationViewFilterValue(filter) as ResolvedFilterValue< - T, - O - >; - default: - return filter.value as ResolvedFilterValue; - } -}; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts index 4e26ca096332..84d542cbe34b 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts @@ -1,7 +1,6 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; export const resolveNumberViewFilterValue = ( - viewFilter: Pick, + viewFilterValue: string ) => { - return viewFilter.value === '' ? null : +viewFilter.value; + return viewFilterValue === '' ? null : +viewFilterValue; }; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts deleted file mode 100644 index 91b35ca89742..000000000000 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveRelationViewFilterValue.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; - -export const resolveRelationViewFilterValue = ( - viewFilter: Pick, -) => { - // TODO: convert 'CURRENT_USER' to the current user id - return viewFilter.value; -}; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveSelectViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveSelectViewFilterValue.ts index c5253fb2e1a1..6ca324779cda 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveSelectViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveSelectViewFilterValue.ts @@ -1,4 +1,3 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; import { z } from 'zod'; const selectViewFilterValueSchema = z @@ -13,7 +12,7 @@ const selectViewFilterValueSchema = z ); export const resolveSelectViewFilterValue = ( - viewFilter: Pick, + viewFilterValue: string, ) => { - return selectViewFilterValueSchema.parse(viewFilter.value); + return selectViewFilterValueSchema.parse(viewFilterValue); }; diff --git a/packages/twenty-front/src/utils/validation-schemas/safeStringArrayJSONSchema.ts b/packages/twenty-front/src/utils/validation-schemas/safeStringArrayJSONSchema.ts new file mode 100644 index 000000000000..88037b99da37 --- /dev/null +++ b/packages/twenty-front/src/utils/validation-schemas/safeStringArrayJSONSchema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const safeStringArrayJSONSchema = z + .preprocess((value) => { + try { + if (typeof value !== 'string') { + return []; + } + return JSON.parse(value); + } catch { + return []; + } + }, z.array(z.string())) + .catch([]); From b4a3c44ce01ae561a067aa33f6477a639c7d7dea Mon Sep 17 00:00:00 2001 From: ad-elias Date: Mon, 9 Dec 2024 03:01:04 +0100 Subject: [PATCH 3/4] Fix 'Me' option icon --- .../modules/object-record/select/hooks/useRecordsForSelect.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index b74781b437c8..1a564b38aab0 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -11,6 +11,7 @@ import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVaria import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { OrderBy } from '@/types/OrderBy'; import { useMapRelationViewFilterValueSpecialIdsToRecordIds } from '@/views/view-filter-value/hooks/useResolveRelationViewFilterValue'; +import { IconUserCircle } from 'twenty-ui'; export const useRecordsForSelect = ({ searchFilterText, @@ -118,6 +119,7 @@ export const useRecordsForSelect = ({ id: 'CURRENT_WORKSPACE_MEMBER', name: 'Me', isSelected: false, + AvatarIcon: IconUserCircle, }, ] : []; From c607cbe8da551a4362ebd65814cf17dc811f166d Mon Sep 17 00:00:00 2001 From: ad-elias Date: Tue, 10 Dec 2024 12:41:02 +0100 Subject: [PATCH 4/4] Fix typo --- .../useComputeViewRecordGqlOperationFilter.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts index 02214ff16016..b6c56fb64322 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useComputeViewRecordGqlOperationFilter.ts @@ -1,21 +1,21 @@ import { isNonEmptyString } from '@sniptt/guards'; import { - ActorFilter, - AddressFilter, - ArrayFilter, - BooleanFilter, - CurrencyFilter, - DateFilter, - EmailsFilter, - FloatFilter, - MultiSelectFilter, - RatingFilter, - RawJsonFilter, - RecordGqlOperationFilter, - RelationFilter, - SelectFilter, - StringFilter, + ActorFilter, + AddressFilter, + ArrayFilter, + BooleanFilter, + CurrencyFilter, + DateFilter, + EmailsFilter, + FloatFilter, + MultiSelectFilter, + RatingFilter, + RawJsonFilter, + RecordGqlOperationFilter, + RelationFilter, + SelectFilter, + StringFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { Field } from '~/generated/graphql'; @@ -23,9 +23,9 @@ import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateIL import { isDefined } from '~/utils/isDefined'; import { - convertGreaterThanRatingToArrayOfRatingValues, - convertLessThanRatingToArrayOfRatingValues, - convertRatingToRatingValue, + convertGreaterThanRatingToArrayOfRatingValues, + convertLessThanRatingToArrayOfRatingValues, + convertRatingToRatingValue, } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter'; @@ -48,7 +48,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { const compositeFieldName = filter.definition.compositeFieldName; - const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); + const isCompositeFieldFilter = isNonEmptyString(compositeFieldName); const isEmptyOperand = [ ViewFilterOperand.IsEmpty, @@ -388,7 +388,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { switch (filter.operand) { case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { or: linksFilters, }; @@ -402,7 +402,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { }; } case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { and: linksFilters.map((filter) => { return { @@ -442,7 +442,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { ); switch (filter.operand) { case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { or: fullNameFilters, }; @@ -456,7 +456,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { }; } case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { and: fullNameFilters.map((filter) => { return { @@ -491,7 +491,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { case 'ADDRESS': switch (filter.operand) { case ViewFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { or: [ { @@ -548,7 +548,7 @@ const useComputeFilterRecordGqlOperationFilter = () => { }; } case ViewFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isCompositeFieldFilter) { return { and: [ {