diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts index 0ba17a0187..47297467c2 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts @@ -1,24 +1,12 @@ -import { CustomError } from '@/error-handler/CustomError'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById'; import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -const FIELD_METADATA_ITEM_NOT_FOUND_ERROR_CODE = - 'FIELD_METADATA_ITEM_NOT_FOUND'; export const useFieldMetadataItemById = (fieldMetadataId: string) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const fieldMetadataItem = objectMetadataItems - .flatMap((objectMetadataItem) => objectMetadataItem.fields) - .find((field) => field.id === fieldMetadataId); - - if (!isDefined(fieldMetadataItem)) { - throw new CustomError( - `Field metadata item not found for id ${fieldMetadataId}`, - FIELD_METADATA_ITEM_NOT_FOUND_ERROR_CODE, - ); - } - - return { fieldMetadataItem }; + return getFieldMetadataItemById({ + fieldMetadataId, + objectMetadataItems, + }); }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemByIdOrThrow.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemByIdOrThrow.ts new file mode 100644 index 0000000000..247a4fdd52 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemByIdOrThrow.ts @@ -0,0 +1,12 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow'; +import { useRecoilValue } from 'recoil'; + +export const useFieldMetadataItemByIdOrThrow = (fieldMetadataId: string) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + return getFieldMetadataItemByIdOrThrow({ + fieldMetadataId, + objectMetadataItems, + }); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts index 8e9ad3ccd9..8debd87c5e 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts @@ -1,23 +1,29 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow'; +import { useRecoilCallback } from 'recoil'; -export const useGetFieldMetadataItemById = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const getFieldMetadataItemById = (fieldMetadataId: string) => { - const fieldMetadataItem = objectMetadataItems - .flatMap((objectMetadataItem) => objectMetadataItem.fields) - .find((field) => field.id === fieldMetadataId); - - if (!isDefined(fieldMetadataItem)) { - throw new Error( - `Field metadata item not found for id ${fieldMetadataId}`, - ); - } - - return fieldMetadataItem; - }; - - return { getFieldMetadataItemById }; +type GetFieldMetadataItemByIdOrThrowResult = { + fieldMetadataItem: FieldMetadataItem; + objectMetadataItem: ObjectMetadataItem; +}; + +export const useGetFieldMetadataItemByIdOrThrow = () => { + const getFieldMetadataItemById = useRecoilCallback( + ({ snapshot }) => + (fieldMetadataId: string): GetFieldMetadataItemByIdOrThrowResult => { + const objectMetadataItems = snapshot + .getLoadable(objectMetadataItemsState) + .getValue(); + + return getFieldMetadataItemByIdOrThrow({ + fieldMetadataId, + objectMetadataItems, + }); + }, + [], + ); + + return { getFieldMetadataItemByIdOrThrow: getFieldMetadataItemById }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemById.ts new file mode 100644 index 0000000000..5a03e3d19e --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemById.ts @@ -0,0 +1,24 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow'; + +type GetFieldMetadataItemByIdParams = { + fieldMetadataId: string; + objectMetadataItems: ObjectMetadataItem[]; +}; + +export const getFieldMetadataItemById = ({ + fieldMetadataId, + objectMetadataItems, +}: GetFieldMetadataItemByIdParams) => { + try { + return getFieldMetadataItemByIdOrThrow({ + fieldMetadataId, + objectMetadataItems, + }); + } catch { + return { + fieldMetadataItem: undefined, + objectMetadataItem: undefined, + }; + } +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemByIdOrThrow.ts b/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemByIdOrThrow.ts new file mode 100644 index 0000000000..e35226ea12 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getFieldMetadataItemByIdOrThrow.ts @@ -0,0 +1,43 @@ +import { CustomError } from '@/error-handler/CustomError'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; + +type GetFieldMetadataItemByIdParams = { + fieldMetadataId: string; + objectMetadataItems: ObjectMetadataItem[]; +}; + +const FIELD_METADATA_ITEM_NOT_FOUND_ERROR_CODE = + 'FIELD_METADATA_ITEM_NOT_FOUND'; + +export const getFieldMetadataItemByIdOrThrow = ({ + fieldMetadataId, + objectMetadataItems, +}: GetFieldMetadataItemByIdParams) => { + const objectMetadataItem = objectMetadataItems.find((objectMetadataItem) => + objectMetadataItem.fields.some((field) => field.id === fieldMetadataId), + ); + + if (!isDefined(objectMetadataItem)) { + throw new CustomError( + `Object metadata item not found for field id ${fieldMetadataId}`, + FIELD_METADATA_ITEM_NOT_FOUND_ERROR_CODE, + ); + } + + const fieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + throw new CustomError( + `Field metadata item not found for field id ${fieldMetadataId}`, + FIELD_METADATA_ITEM_NOT_FOUND_ERROR_CODE, + ); + } + + return { + fieldMetadataItem, + objectMetadataItem, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx index 8a49b2b5b3..f4c4a7b6a6 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx @@ -1,4 +1,3 @@ -import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { getAdvancedFilterInputPlaceholderText } from '@/object-record/advanced-filter/utils/getAdvancedFilterInputPlacedholderText'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { SelectControl } from '@/ui/input/components/SelectControl'; @@ -8,6 +7,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import styled from '@emotion/styled'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { isDefined } from 'twenty-shared/utils'; // TODO: factorize this with https://github.com/twentyhq/core-team-issues/issues/752 @@ -48,11 +48,9 @@ export const AdvancedFilterValueInputDropdownButtonClickableSelect = ({ const shouldUsePlaceholder = !isNonEmptyString(recordFilter?.value); - const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); - - const fieldMetadataItem = isNonEmptyString(recordFilter?.fieldMetadataId) - ? getFieldMetadataItemById(recordFilter?.fieldMetadataId) - : undefined; + const { fieldMetadataItem } = useFieldMetadataItemById( + recordFilter?.fieldMetadataId ?? '', + ); const placeholderText = isDefined(fieldMetadataItem) ? getAdvancedFilterInputPlaceholderText(fieldMetadataItem) diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useRecordFilterField.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useRecordFilterField.ts index 8c9587c32a..4c05056f63 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useRecordFilterField.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useRecordFilterField.ts @@ -1,4 +1,4 @@ -import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; @@ -17,11 +17,9 @@ export const useRecordFilterField = (recordFilterId: string) => { (recordFilter) => recordFilter.id === recordFilterId, ); - const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); - - const fieldMetadataItem = isNonEmptyString(recordFilter?.fieldMetadataId) - ? getFieldMetadataItemById(recordFilter?.fieldMetadataId) - : undefined; + const { fieldMetadataItem } = useFieldMetadataItemById( + recordFilter?.fieldMetadataId ?? '', + ); const { getIcon } = useIcons(); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts index df3018afd9..6581c1e507 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts @@ -1,4 +1,4 @@ -import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; +import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; @@ -45,7 +45,8 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); - const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); + const { getFieldMetadataItemByIdOrThrow } = + useGetFieldMetadataItemByIdOrThrow(); const setSubFieldNameUsedInDropdown = useSetRecoilComponentState( subFieldNameUsedInDropdownComponentState, @@ -64,7 +65,8 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { }: SelectFilterParams) => { setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId); - const fieldMetadataItem = getFieldMetadataItemById(fieldMetadataItemId); + const { fieldMetadataItem } = + getFieldMetadataItemByIdOrThrow(fieldMetadataItemId); if (!isDefined(fieldMetadataItem)) { return; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index 5967b97987..2d832c8456 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -1,4 +1,4 @@ -import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { useFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useFieldMetadataItemByIdOrThrow'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; @@ -21,7 +21,7 @@ export const EditableFilterChip = ({ }: EditableFilterChipProps) => { const { getIcon } = useIcons(); - const { fieldMetadataItem } = useFieldMetadataItemById( + const { fieldMetadataItem } = useFieldMetadataItemByIdOrThrow( recordFilter.fieldMetadataId, ); diff --git a/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx index b802af0735..6eb6701b77 100644 --- a/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx @@ -1,4 +1,4 @@ -import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { useFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useFieldMetadataItemByIdOrThrow'; import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemoveRecordSort'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort'; import { type RecordSort } from '@/object-record/record-sort/types/RecordSort'; @@ -18,7 +18,7 @@ export const EditableSortChip = ({ recordSort }: EditableSortChipProps) => { removeRecordSort(recordSort.fieldMetadataId); }; - const { fieldMetadataItem } = useFieldMetadataItemById( + const { fieldMetadataItem } = useFieldMetadataItemByIdOrThrow( recordSort.fieldMetadataId, ); diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx index e1caf1560d..411b2bf4a3 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx @@ -1,6 +1,6 @@ import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; -import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; +import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; @@ -32,7 +32,8 @@ export const ViewFieldsVisibleDropdownSection = () => { viewBarId: recordIndexId, }); - const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); + const { getFieldMetadataItemByIdOrThrow } = + useGetFieldMetadataItemByIdOrThrow(); const handleReorderFields = viewType === ViewType.Kanban @@ -93,7 +94,7 @@ export const ViewFieldsVisibleDropdownSection = () => { index + (isDefined(fieldMetadataItemLabelIdentifier) ? 1 : 0); - const fieldMetadataItem = getFieldMetadataItemById( + const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow( recordField.fieldMetadataItemId, ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx index 9e300880e9..8f1b2e7078 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx @@ -1,8 +1,9 @@ +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { SelectControl } from '@/ui/input/components/SelectControl'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useWorkflowVersionIdOrThrow } from '@/workflow/hooks/useWorkflowVersionIdOrThrow'; import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector'; -import { useFilterFieldMetadataItem } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterFieldMetadataItem'; import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings'; import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; import { getViewFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands'; @@ -25,6 +26,8 @@ type WorkflowStepFilterFieldSelectProps = { const NON_SELECTABLE_FIELD_TYPES = [ FieldMetadataType.ACTOR, FieldMetadataType.RICH_TEXT_V2, + FieldMetadataType.ARRAY, + FieldMetadataType.RATING, ]; export const WorkflowStepFilterFieldSelect = ({ @@ -53,10 +56,11 @@ export const WorkflowStepFilterFieldSelect = ({ const { getIcon } = useIcons(); - const { - fieldMetadataItem: filterFieldMetadataItem, - objectMetadataItem: filterObjectMetadataItem, - } = useFilterFieldMetadataItem(stepFilter.fieldMetadataId ?? ''); + const { fieldMetadataItem: filterFieldMetadataItem } = + useFieldMetadataItemById(stepFilter.fieldMetadataId ?? ''); + + const { getFieldMetadataItemByIdOrThrow } = + useGetFieldMetadataItemByIdOrThrow(); const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({ shouldDisplayRecordFields, @@ -92,6 +96,13 @@ export const WorkflowStepFilterFieldSelect = ({ isFullRecord: false, }); + const { + fieldMetadataItem: filterFieldMetadataItem, + objectMetadataItem: filterObjectMetadataItem, + } = isDefined(fieldMetadataId) + ? getFieldMetadataItemByIdOrThrow(fieldMetadataId) + : { fieldMetadataItem: undefined, objectMetadataItem: undefined }; + const filterType = isDefined(fieldMetadataId) ? (filterFieldMetadataItem?.type ?? 'unknown') : variableType; @@ -119,11 +130,10 @@ export const WorkflowStepFilterFieldSelect = ({ }); }, [ + workflowVersionId, + getFieldMetadataItemByIdOrThrow, upsertStepFilterSettings, stepFilter, - workflowVersionId, - filterFieldMetadataItem, - filterObjectMetadataItem, ], ); @@ -185,25 +195,27 @@ export const WorkflowStepFilterFieldSelect = ({ } return ( - - } - shouldDisplayRecordFields={shouldDisplayRecordFields} - shouldDisplayRecordObjects={shouldDisplayRecordObjects} - shouldEnableSelectRelationObject={true} - fieldTypesToExclude={NON_SELECTABLE_FIELD_TYPES} - /> + <> + + } + shouldDisplayRecordFields={shouldDisplayRecordFields} + shouldDisplayRecordObjects={shouldDisplayRecordObjects} + shouldEnableSelectRelationObject={true} + fieldTypesToExclude={NON_SELECTABLE_FIELD_TYPES} + /> + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx index c9a3dbe16f..01666b84e9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx @@ -1,3 +1,4 @@ +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands'; import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput'; @@ -7,7 +8,6 @@ import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/c import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { WorkflowStepFilterValueCompositeInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueCompositeInput'; -import { useFilterFieldMetadataItem } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterFieldMetadataItem'; import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings'; import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; @@ -91,7 +91,7 @@ export const WorkflowStepFilterValueInput = ({ } = stepFilter; const { fieldMetadataItem: selectedFieldMetadataItem, objectMetadataItem } = - useFilterFieldMetadataItem(fieldMetadataId ?? ''); + useFieldMetadataItemById(fieldMetadataId ?? ''); if (isDisabled || operandHasNoInput) { return null; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterFieldMetadataItem.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterFieldMetadataItem.ts deleted file mode 100644 index 8f3204ea22..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterFieldMetadataItem.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; - -export const useFilterFieldMetadataItem = (fieldMetadataId: string) => { - const { objectMetadataItems } = useObjectMetadataItems(); - - const objectMetadataItem = objectMetadataItems.find((objectMetadataItem) => - objectMetadataItem.fields.some((field) => field.id === fieldMetadataId), - ); - - const fieldMetadataItem = objectMetadataItem?.fields.find( - (field) => field.id === fieldMetadataId, - ); - - return { - objectMetadataItem, - fieldMetadataItem, - }; -}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts index dfb08da4d4..779b9fc9b0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts @@ -314,6 +314,80 @@ describe('evaluateFilterConditions', () => { }); }); + describe('Select filter operands', () => { + it('should return true when there are common values (SELECT)', () => { + const filter = createFilter( + ViewFilterOperand.Is, + ['John'], + ['John', 'Jane'], + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should return false when there are no common values (SELECT)', () => { + const filter = createFilter( + ViewFilterOperand.Is, + ['John'], + ['Jane'], + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); + }); + + it('should return true when there are no common values (IsNot)', () => { + const filter = createFilter( + ViewFilterOperand.IsNot, + ['John'], + ['Jane'], + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should return true when there are no values (IsEmpty)', () => { + const filter = createFilter( + ViewFilterOperand.IsEmpty, + [], + '', + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should return false when there is a value (IsEmpty)', () => { + const filter = createFilter( + ViewFilterOperand.IsEmpty, + ['John'], + '', + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); + }); + + it('should return true when there are values (IsNotEmpty)', () => { + const filter = createFilter( + ViewFilterOperand.IsNotEmpty, + ['John'], + '', + 'SELECT', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + }); + describe('Boolean filter operands', () => { it('should return true when boolean values are equal', () => { const filter = createFilter( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts index 8638372bd2..a8ca62c036 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts @@ -1,4 +1,9 @@ -import { isObject, isString } from '@sniptt/guards'; +import { + isNonEmptyArray, + isNonEmptyString, + isObject, + isString, +} from '@sniptt/guards'; import { type StepFilter, type StepFilterGroup, @@ -21,7 +26,6 @@ function evaluateFilter(filter: ResolvedFilter): boolean { case 'DATE_TIME': return evaluateDateFilter(filter); case 'TEXT': - case 'SELECT': case 'MULTI_SELECT': case 'FULL_NAME': case 'EMAILS': @@ -31,6 +35,8 @@ function evaluateFilter(filter: ResolvedFilter): boolean { case 'ARRAY': case 'RAW_JSON': return evaluateTextAndArrayFilter(filter); + case 'SELECT': + return evaluateSelectFilter(filter); case 'BOOLEAN': return evaluateBooleanFilter(filter); case 'UUID': @@ -121,20 +127,11 @@ function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean { case ViewFilterOperand.DoesNotContain: return !contains(filter.leftOperand, filter.rightOperand); case ViewFilterOperand.IsEmpty: - return ( - filter.leftOperand === null || - filter.leftOperand === undefined || - filter.leftOperand === '' || - (Array.isArray(filter.leftOperand) && filter.leftOperand.length === 0) - ); + return !isNotEmptyTextOrArray(filter.leftOperand); case ViewFilterOperand.IsNotEmpty: - return ( - filter.leftOperand !== null && - filter.leftOperand !== undefined && - filter.leftOperand !== '' && - (!Array.isArray(filter.leftOperand) || filter.leftOperand.length > 0) - ); + return isNotEmptyTextOrArray(filter.leftOperand); + default: throw new Error( `Operand ${filter.operand} not supported for this filter type`, @@ -142,6 +139,10 @@ function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean { } } +function isNotEmptyTextOrArray(value: unknown): boolean { + return isNonEmptyString(value) || isNonEmptyArray(value); +} + function evaluateBooleanFilter(filter: ResolvedFilter): boolean { switch (filter.operand) { case ViewFilterOperand.Is: @@ -255,17 +256,9 @@ function evaluateCurrencyFilter(filter: ResolvedFilter): boolean { case ViewFilterOperand.IsNot: return filter.leftOperand !== filter.rightOperand; case ViewFilterOperand.IsEmpty: - return ( - filter.leftOperand === null || - filter.leftOperand === undefined || - filter.leftOperand === '' - ); + return !isNonEmptyString(filter.leftOperand); case ViewFilterOperand.IsNotEmpty: - return ( - filter.leftOperand !== null && - filter.leftOperand !== undefined && - filter.leftOperand !== '' - ); + return isNonEmptyString(filter.leftOperand); default: throw new Error( `Operand ${filter.operand} not supported for currency filter`, @@ -288,10 +281,10 @@ function evaluateNumberFilter(filter: ResolvedFilter): boolean { return Number(leftValue) <= Number(rightValue); case ViewFilterOperand.IsEmpty: - return leftValue === null || leftValue === undefined || leftValue === ''; + return !isNonEmptyString(leftValue); case ViewFilterOperand.IsNotEmpty: - return leftValue !== null && leftValue !== undefined && leftValue !== ''; + return isNonEmptyString(leftValue); default: throw new Error( @@ -310,19 +303,9 @@ function evaluateDefaultFilter(filter: ResolvedFilter): boolean { case ViewFilterOperand.IsNot: return leftValue != rightValue; case ViewFilterOperand.IsEmpty: - return ( - leftValue === null || - leftValue === undefined || - leftValue === '' || - (Array.isArray(leftValue) && leftValue.length === 0) - ); + return !isNotEmptyTextOrArray(leftValue); case ViewFilterOperand.IsNotEmpty: - return ( - leftValue !== null && - leftValue !== undefined && - leftValue !== '' && - (!Array.isArray(leftValue) || leftValue.length > 0) - ); + return isNotEmptyTextOrArray(leftValue); case ViewFilterOperand.Contains: return contains(leftValue, rightValue); case ViewFilterOperand.DoesNotContain: @@ -338,6 +321,24 @@ function evaluateDefaultFilter(filter: ResolvedFilter): boolean { } } +function evaluateSelectFilter(filter: ResolvedFilter): boolean { + switch (filter.operand) { + case ViewFilterOperand.Is: + return contains(filter.leftOperand, filter.rightOperand); + case ViewFilterOperand.IsNot: + return !contains(filter.leftOperand, filter.rightOperand); + case ViewFilterOperand.IsEmpty: + return !isNotEmptyTextOrArray(filter.leftOperand); + + case ViewFilterOperand.IsNotEmpty: + return isNotEmptyTextOrArray(filter.leftOperand); + default: + throw new Error( + `Operand ${filter.operand} not supported for select filter`, + ); + } +} + export function evaluateFilterConditions({ filterGroups = [], filters = [], diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 4a6d5309bc..d36989f015 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -170,4 +170,4 @@ ] } } -} \ No newline at end of file +}