diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx index 0365885d2..f7dc8896a 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx @@ -1,18 +1,15 @@ import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown'; -import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import styled from '@emotion/styled'; -import { useContext } from 'react'; import { capitalize } from 'twenty-shared/utils'; -const StyledText = styled.div<{ noPadding?: boolean }>` +const StyledText = styled.div` height: ${({ theme }) => theme.spacing(8)}; display: flex; align-items: center; - padding-left: ${({ theme, noPadding }) => - noPadding ? 0 : theme.spacing(2.25)}; + padding-left: ${({ theme }) => theme.spacing(2.25)}; `; const StyledContainer = styled.div` @@ -31,18 +28,16 @@ export const AdvancedFilterLogicalOperatorCell = ({ index, recordFilterGroup, }: AdvancedFilterLogicalOperatorCellProps) => { - const { isColumn } = useContext(AdvancedFilterContext); - return ( {index === 0 ? ( - Where + Where ) : index === 1 ? ( ) : ( - + {capitalize(recordFilterGroup.logicalOperator.toLowerCase())} )} diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilter.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilter.tsx deleted file mode 100644 index 3f39e08fd..000000000 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilter.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { AdvancedFilterRecordFilterColumn } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterColumn'; -import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow'; -import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; -import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; -import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useContext } from 'react'; - -export const AdvancedFilterRecordFilter = ({ - recordFilterGroup, - recordFilter, - recordFilterIndex, - VariablePicker, -}: { - recordFilterGroup: RecordFilterGroup; - recordFilter: RecordFilter; - recordFilterIndex: number; - VariablePicker?: VariablePickerComponent; -}) => { - const { isColumn } = useContext(AdvancedFilterContext); - - return isColumn ? ( - - ) : ( - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup.tsx deleted file mode 100644 index 972879509..000000000 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { AdvancedFilterRecordFilterGroupColumn } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupColumn'; -import { AdvancedFilterRecordFilterGroupRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupRow'; -import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; -import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; -import { useContext } from 'react'; - -export const AdvancedFilterRecordFilterGroup = ({ - parentRecordFilterGroup, - recordFilterGroup, - recordFilterGroupIndex, - VariablePicker, -}: { - parentRecordFilterGroup: RecordFilterGroup; - recordFilterGroup: RecordFilterGroup; - recordFilterGroupIndex: number; - VariablePicker?: VariablePickerComponent; -}) => { - const { isColumn } = useContext(AdvancedFilterContext); - - return isColumn ? ( - - ) : ( - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildren.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildren.tsx index 58f49956a..0544386b2 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildren.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildren.tsx @@ -1,8 +1,7 @@ import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; -import { AdvancedFilterRecordFilter } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilter'; +import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared/utils'; @@ -17,17 +16,14 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>` flex-direction: column; gap: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)}; - overflow: hidden; `; type AdvancedFilterRecordFilterGroupChildrenProps = { recordFilterGroupId: string; - VariablePicker?: VariablePickerComponent; }; export const AdvancedFilterRecordFilterGroupChildren = ({ recordFilterGroupId, - VariablePicker, }: AdvancedFilterRecordFilterGroupChildrenProps) => { const { currentRecordFilterGroup, childRecordFilters } = useChildRecordFiltersAndRecordFilterGroups({ @@ -45,12 +41,11 @@ export const AdvancedFilterRecordFilterGroupChildren = ({ return ( {childRecordFilters.map((childRecordFilter, childRecordFilterIndex) => ( - ))} { return ( @@ -24,7 +21,6 @@ export const AdvancedFilterRecordFilterGroupRow = ({ /> ` - width: ${({ isColumn }) => (isColumn ? 'auto' : '100px')}; +const StyledContainer = styled.div<{ width?: string }>` + width: ${({ width }) => width ?? '100px'}; `; type AdvancedFilterRecordFilterOperandSelectProps = { recordFilterId: string; + widthFromProps?: string; }; export const AdvancedFilterRecordFilterOperandSelect = ({ recordFilterId, + widthFromProps, }: AdvancedFilterRecordFilterOperandSelectProps) => { const dropdownId = `advanced-filter-view-filter-operand-${recordFilterId}`; @@ -38,8 +38,6 @@ export const AdvancedFilterRecordFilterOperandSelect = ({ currentRecordFiltersComponentState, ); - const { isColumn } = useContext(AdvancedFilterContext); - const filter = currentRecordFilters.find( (recordFilter) => recordFilter.id === recordFilterId, ); @@ -86,7 +84,7 @@ export const AdvancedFilterRecordFilterOperandSelect = ({ } return ( - + { isRecordFilterGroupChildARecordFilterGroup( recordFilterGroupChild, ) ? ( - ) : ( - diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/states/context/AdvancedFilterContext.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/states/context/AdvancedFilterContext.ts index 21cae4c93..c72176191 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/states/context/AdvancedFilterContext.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/states/context/AdvancedFilterContext.ts @@ -2,7 +2,7 @@ import { createContext } from 'react'; type AdvancedFilterContextType = { onUpdate?: () => void; - isColumn?: boolean; + isWorkflowFindRecords?: boolean; }; export const AdvancedFilterContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/utils/shouldShowFilterTextInput.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/utils/shouldShowFilterTextInput.ts new file mode 100644 index 000000000..cbcbfb029 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/utils/shouldShowFilterTextInput.ts @@ -0,0 +1,42 @@ +import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes'; +import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes'; +import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { RecordFilter } from '../../record-filter/types/RecordFilter'; + +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; + +export const shouldShowFilterTextInput = ({ + recordFilter, + subFieldNameUsedInDropdown, +}: { + recordFilter: RecordFilter; + subFieldNameUsedInDropdown: CompositeFieldSubFieldName | null | undefined; +}) => { + const isFilterableByTextValue = + TEXT_FILTER_TYPES.includes(recordFilter.type) || + NUMBER_FILTER_TYPES.includes(recordFilter.type); + + const isCurrencyAmountMicrosFilter = isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'amountMicros', + recordFilter.subFieldName, + ); + + const isAddressFilterOnSubFieldOtherThanCountry = + recordFilter.type === 'ADDRESS' && + subFieldNameUsedInDropdown !== 'addressCountry'; + + const isActorNameFilter = isExpectedSubFieldName( + FieldMetadataType.ACTOR, + 'name', + recordFilter.subFieldName, + ); + + return ( + isFilterableByTextValue || + isCurrencyAmountMicrosFilter || + isAddressFilterOnSubFieldOtherThanCountry || + isActorNameFilter + ); +}; 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 14baaa92d..46c7e4bcf 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 @@ -15,8 +15,8 @@ import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue'; +import { arrayOfUuidOrVariableSchema } from '@/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema'; import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema'; -import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema'; import { isDefined } from 'twenty-shared/utils'; import { IconUserCircle } from 'twenty-ui/display'; @@ -59,7 +59,7 @@ export const ObjectFilterDropdownRecordSelect = ({ const { isCurrentWorkspaceMemberSelected } = jsonRelationFilterValueSchema .catch({ isCurrentWorkspaceMemberSelected: false, - selectedRecordIds: simpleRelationFilterValueSchema.parse( + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( objectFilterDropdownFilterValue, ), }) @@ -105,7 +105,7 @@ export const ObjectFilterDropdownRecordSelect = ({ const { selectedRecordIds } = jsonRelationFilterValueSchema .catch({ isCurrentWorkspaceMemberSelected: false, - selectedRecordIds: simpleRelationFilterValueSchema.parse( + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( recordFilterUsedInDropdown?.value, ), }) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx index a5bd5b7d2..d612eb306 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx @@ -12,11 +12,13 @@ export type FormCountryCodeSelectInputUpdatedValue = CountryCode | ''; export const FormCountryCodeSelectInput = ({ selectedCountryCode, onChange, + label, readonly = false, VariablePicker, }: { selectedCountryCode: string; onChange: (countryCode: FormCountryCodeSelectInputUpdatedValue) => void; + label?: string; readonly?: boolean; VariablePicker?: VariablePickerComponent; }) => { @@ -55,7 +57,7 @@ export const FormCountryCodeSelectInput = ({ return ( {label}} = { title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput', component: FormCountryCodeSelectInput, - args: {}, + args: { + label: 'Country Code', + }, argTypes: {}, }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext.ts index 7475f6197..43c1309d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext.ts @@ -1,12 +1,25 @@ +import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; +import { useContext } from 'react'; export const useFilterableFieldMetadataItemsInRecordIndexContext = () => { const { objectMetadataItem } = useRecordIndexContextOrThrow(); + const { isWorkflowFindRecords } = useContext(AdvancedFilterContext); - const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems( - objectMetadataItem.id, - ); + const { + filterableFieldMetadataItems: filterableFieldMetadataItemsForRecordIndex, + } = useFilterableFieldMetadataItems(objectMetadataItem.id); + + const filterableFieldMetadataItems = isWorkflowFindRecords + ? filterableFieldMetadataItemsForRecordIndex.filter((fieldMetadataItem) => + shouldDisplayFormField({ + fieldMetadataItem, + actionType: 'FIND_RECORDS', + }), + ) + : filterableFieldMetadataItemsForRecordIndex; return { filterableFieldMetadataItems }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts index d7993c42d..88628f587 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -33,9 +33,7 @@ import { RecordFilterValueDependencies } from '@/object-record/record-filter/typ import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter'; import { resolveDateViewFilterValue } from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; -import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue'; import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema'; -import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema'; import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; @@ -44,6 +42,8 @@ import { checkIfShouldComputeEmptinessFilter } from '@/object-record/record-filt import { checkIfShouldSkipFiltering } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering'; import { computeGqlOperationFilterForEmails } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails'; import { computeGqlOperationFilterForLinks } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks'; +import { arrayOfStringsOrVariablesSchema } from '@/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema'; +import { arrayOfUuidOrVariableSchema } from '@/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -312,7 +312,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ jsonRelationFilterValueSchema .catch({ isCurrentWorkspaceMemberSelected: false, - selectedRecordIds: simpleRelationFilterValueSchema.parse( + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( recordFilter.value, ), }) @@ -759,7 +759,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ ); } case 'MULTI_SELECT': { - const options = resolveSelectViewFilterValue(recordFilter); + const options = arrayOfStringsOrVariablesSchema.parse(recordFilter.value); if (options.length === 0) return; @@ -817,7 +817,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } } case 'SELECT': { - const options = resolveSelectViewFilterValue(recordFilter); + const options = arrayOfStringsOrVariablesSchema.parse(recordFilter.value); if (options.length === 0) return; 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 deleted file mode 100644 index c5253fb2e..000000000 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveSelectViewFilterValue.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ViewFilter } from '@/views/types/ViewFilter'; -import { z } from 'zod'; - -const selectViewFilterValueSchema = z - .string() - .transform((val) => (val === '' ? [] : JSON.parse(val))) - .refine( - (parsed) => - Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'), - { - message: 'Expected an array of strings', - }, - ); - -export const resolveSelectViewFilterValue = ( - viewFilter: Pick, -) => { - return selectViewFilterValueSchema.parse(viewFilter.value); -}; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfStringsOrVariablesSchema.test.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfStringsOrVariablesSchema.test.ts new file mode 100644 index 000000000..c37a2e909 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfStringsOrVariablesSchema.test.ts @@ -0,0 +1,91 @@ +import { arrayOfStringsOrVariablesSchema } from '../arrayOfStringsOrVariablesSchema'; + +describe('arrayOfStringsOrVariablesSchema', () => { + describe('Empty value handling', () => { + it('should return empty array for empty string', () => { + const result = arrayOfStringsOrVariablesSchema.safeParse(''); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([]); + } + }); + }); + + describe('Variable syntax validation', () => { + it('should accept valid variable syntax', () => { + const validVariables = [ + '{{variable}}', + '{{user.id}}', + '{{company.name}}', + ]; + + validVariables.forEach((variable) => { + const result = arrayOfStringsOrVariablesSchema.safeParse(variable); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([variable]); + } + }); + }); + }); + + describe('JSON array handling', () => { + it('should accept valid JSON array of strings', () => { + const validArrays = [ + JSON.stringify(['value1', 'value2']), + JSON.stringify(['{{variable1}}', '{{variable2}}']), + JSON.stringify(['value1', '{{variable2}}']), + ]; + + validArrays.forEach((array) => { + const result = arrayOfStringsOrVariablesSchema.safeParse(array); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(JSON.parse(array)); + } + }); + }); + it('should reject JSON array with non-string values', () => { + const invalidArrays = [ + JSON.stringify([1, 2, 3]), + JSON.stringify([true, false]), + JSON.stringify([null]), + JSON.stringify([{}]), + JSON.stringify([[]]), + ]; + + invalidArrays.forEach((array) => { + const result = arrayOfStringsOrVariablesSchema.safeParse(array); + expect(result.success).toBe(false); + }); + }); + }); + + describe('Edge cases', () => { + it('should handle whitespace in variable syntax', () => { + const result = + arrayOfStringsOrVariablesSchema.safeParse('{{ variable }}'); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(['{{ variable }}']); + } + }); + + it('should handle nested variables in JSON array', () => { + const input = JSON.stringify(['{{outer.{{inner}}}}']); + const result = arrayOfStringsOrVariablesSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(['{{outer.{{inner}}}}']); + } + }); + + it('should handle empty array in JSON', () => { + const result = arrayOfStringsOrVariablesSchema.safeParse('[]'); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([]); + } + }); + }); +}); diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfUuidOrVariableSchema.test.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfUuidOrVariableSchema.test.ts new file mode 100644 index 000000000..f635b5edf --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/__tests__/arrayOfUuidOrVariableSchema.test.ts @@ -0,0 +1,169 @@ +import { arrayOfUuidOrVariableSchema } from '../arrayOfUuidsOrVariablesSchema'; + +describe('arrayOfUuidOrVariableSchema', () => { + describe('UUID validation', () => { + it('should accept valid UUIDs', () => { + const validUuids = [ + '123e4567-e89b-12d3-a456-426614174000', + '550e8400-e29b-41d4-a716-446655440000', + ]; + + validUuids.forEach((uuid) => { + // Test as single value + const singleResult = arrayOfUuidOrVariableSchema.safeParse(uuid); + expect(singleResult.success).toBe(true); + if (singleResult.success) { + expect(singleResult.data).toEqual([uuid]); + } + + // Test as array + const arrayResult = arrayOfUuidOrVariableSchema.safeParse([uuid]); + expect(arrayResult.success).toBe(true); + if (arrayResult.success) { + expect(arrayResult.data).toEqual([uuid]); + } + }); + }); + + it('should return empty array for invalid UUIDs', () => { + const invalidUuids = [ + 'invalid-uuid', + '12345', + '550e8400e29b41d4a716446655440000', + '', + '123e4567-e89b-12d3-a456-42661417400-', + ]; + + invalidUuids.forEach((uuid) => { + // Test as single value + const singleResult = arrayOfUuidOrVariableSchema.safeParse(uuid); + expect(singleResult.success).toBe(true); + if (singleResult.success) { + expect(singleResult.data).toEqual([]); + } + + // Test as array + const arrayResult = arrayOfUuidOrVariableSchema.safeParse([uuid]); + expect(arrayResult.success).toBe(true); + if (arrayResult.success) { + expect(arrayResult.data).toEqual([]); + } + }); + }); + }); + + describe('Variable syntax validation', () => { + it('should accept valid variable syntax', () => { + const validVariables = [ + '{{variable}}', + '{{user.id}}', + '{{company.name}}', + ]; + + validVariables.forEach((variable) => { + // Test as single value + const singleResult = arrayOfUuidOrVariableSchema.safeParse(variable); + expect(singleResult.success).toBe(true); + if (singleResult.success) { + expect(singleResult.data).toEqual([variable]); + } + + // Test as array + const arrayResult = arrayOfUuidOrVariableSchema.safeParse([variable]); + expect(arrayResult.success).toBe(true); + if (arrayResult.success) { + expect(arrayResult.data).toEqual([variable]); + } + }); + }); + + it('should return empty array for invalid variable syntax', () => { + const invalidVariables = ['{{variable', 'variable}}', '{{}}', '{{', '}}']; + + invalidVariables.forEach((variable) => { + // Test as single value + const singleResult = arrayOfUuidOrVariableSchema.safeParse(variable); + expect(singleResult.success).toBe(true); + if (singleResult.success) { + expect(singleResult.data).toEqual([]); + } + + // Test as array + const arrayResult = arrayOfUuidOrVariableSchema.safeParse([variable]); + expect(arrayResult.success).toBe(true); + if (arrayResult.success) { + expect(arrayResult.data).toEqual([]); + } + }); + }); + }); + + describe('Input type handling', () => { + it('should handle string input with valid JSON', () => { + const input = JSON.stringify(['123e4567-e89b-12d3-a456-426614174000']); + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(['123e4567-e89b-12d3-a456-426614174000']); + } + }); + + it('should handle string input with variables', () => { + const input = '{{variable}}'; + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(['{{variable}}']); + } + }); + + it('should handle array input directly', () => { + const input = ['123e4567-e89b-12d3-a456-426614174000', '{{variable}}']; + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(input); + } + }); + + it('should handle single value input', () => { + const input = '20202020-0687-4c41-b707-ed1bfca972a7'; + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([input]); + } + }); + }); + + describe('Error handling', () => { + it('should return empty array for invalid JSON string', () => { + const input = 'invalid-json'; + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([]); + } + }); + + it('should return empty array for non-string, non-array input', () => { + const inputs = [null, undefined, 123, true, {}]; + inputs.forEach((input) => { + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([]); + } + }); + }); + + it('should return empty array for array with invalid values', () => { + const input = ['invalid-uuid', 'not-a-variable']; + const result = arrayOfUuidOrVariableSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual([]); + } + }); + }); +}); diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts new file mode 100644 index 000000000..f248ae255 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts @@ -0,0 +1,19 @@ +import { isValidVariable } from 'twenty-shared/utils'; +import { z } from 'zod'; + +export const arrayOfStringsOrVariablesSchema = z + .string() + .transform((val) => { + if (val === '') return []; + if (isValidVariable(val) as boolean) { + return [val]; + } + return JSON.parse(val); + }) + .refine( + (parsed) => + Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'), + { + message: 'Expected an array of strings', + }, + ); diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema.ts new file mode 100644 index 000000000..64b573f19 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema.ts @@ -0,0 +1,30 @@ +import { isValidUuid, isValidVariable } from 'twenty-shared/utils'; +import { z } from 'zod'; + +export const arrayOfUuidOrVariableSchema = z + .preprocess( + (value) => { + try { + if (typeof value === 'string') { + if (isValidVariable(value) as boolean) { + return [value]; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch { + return [value]; + } + } + return Array.isArray(value) ? value : [value]; + } catch { + return []; + } + }, + z.array( + z.string().refine((val) => { + return isValidUuid(val) || isValidVariable(val); + }, 'Must be a valid UUID or a variable with {{ }} syntax'), + ), + ) + .catch([]); diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema.ts deleted file mode 100644 index 93a2ec867..000000000 --- a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -export const simpleRelationFilterValueSchema = z - .preprocess((value) => { - try { - return typeof value === 'string' ? JSON.parse(value) : []; - } catch { - return []; - } - }, z.array(z.string().uuid())) - .catch([]); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownColumn.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn.tsx similarity index 72% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownColumn.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn.tsx index 9970de5d2..08457589f 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownColumn.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn.tsx @@ -7,4 +7,4 @@ const StyledColumn = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export const AdvancedFilterDropdownColumn = StyledColumn; +export const WorkflowAdvancedFilterDropdownColumn = StyledColumn; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterFormInput.tsx similarity index 54% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormInput.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterFormInput.tsx index bdcbaa102..401cbb916 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterFormInput.tsx @@ -1,25 +1,31 @@ import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; -import { AdvancedFilterValueFormCompositeFieldInput } from '@/object-record/advanced-filter/components/AdvancedFilterValueFormCompositeFieldInput'; +import { shouldShowFilterTextInput } from '@/object-record/advanced-filter/utils/shouldShowFilterTextInput'; import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; +import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; +import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; +import { + FieldMetadata, + FieldMultiSelectMetadata, + FieldSelectMetadata, +} from '@/object-record/record-field/types/FieldMetadata'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { WorkflowAdvancedFilterValueFormCompositeFieldInput } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterValueFormCompositeFieldInput'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { isObject } from '@sniptt/guards'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { JsonValue } from 'type-fest'; -export const AdvancedFilterValueFormInput = ({ +export const WorkflowAdvancedFilterValueFormInput = ({ recordFilterId, - VariablePicker, }: { recordFilterId: string; - VariablePicker?: VariablePickerComponent; }) => { const currentRecordFilters = useRecoilComponentValueV2( currentRecordFiltersComponentState, @@ -37,6 +43,11 @@ export const AdvancedFilterValueFormInput = ({ const isDisabled = !recordFilter?.fieldMetadataId || !recordFilter.operand; + const operandHasNoInput = + (recordFilter && + !configurableViewFilterOperands.has(recordFilter.operand)) ?? + true; + const { applyObjectFilterDropdownFilterValue } = useApplyObjectFilterDropdownFilterValue(); @@ -61,20 +72,63 @@ export const AdvancedFilterValueFormInput = ({ }) : null; - if (isDisabled) { + if (!isDefined(recordFilter)) { return null; } + const isFilterableByTextValue = shouldShowFilterTextInput({ + recordFilter, + subFieldNameUsedInDropdown, + }); + + const isFilterableByMultiSelectValue = + recordFilter.type === FieldMetadataType.MULTI_SELECT || + recordFilter.type === FieldMetadataType.SELECT; + + const isFilterableByDateValue = + recordFilter.type === FieldMetadataType.DATE || + recordFilter.type === FieldMetadataType.DATE_TIME; + + if (isDisabled || operandHasNoInput) { + return null; + } + + if (isFilterableByTextValue) { + return ( + + ); + } + if (isDefined(subFieldNameUsedInDropdown)) { return ( - ); } + if (isFilterableByMultiSelectValue) { + const metadata = fieldDefinition?.metadata as + | FieldMultiSelectMetadata + | FieldSelectMetadata + | undefined; + return ( + + ); + } + const field = { type: recordFilter.type as FieldMetadataType, label: '', @@ -86,7 +140,10 @@ export const AdvancedFilterValueFormInput = ({ field={field} defaultValue={recordFilter.value} onChange={handleChange} - VariablePicker={VariablePicker} + // VariablePicker is not supported for date filters yet + VariablePicker={ + isFilterableByDateValue ? undefined : WorkflowVariablePicker + } /> ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterLogicalOperatorCell.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterLogicalOperatorCell.tsx new file mode 100644 index 000000000..459f573bb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterLogicalOperatorCell.tsx @@ -0,0 +1,44 @@ +import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; + +import styled from '@emotion/styled'; +import { capitalize } from 'twenty-shared/utils'; + +const StyledText = styled.div` + height: ${({ theme }) => theme.spacing(8)}; + display: flex; + align-items: center; +`; + +const StyledContainer = styled.div` + align-items: start; + display: flex; + min-width: ${({ theme }) => theme.spacing(20)}; + color: ${({ theme }) => theme.font.color.tertiary}; +`; + +type WorkflowAdvancedFilterLogicalOperatorCellProps = { + index: number; + recordFilterGroup: RecordFilterGroup; +}; + +export const WorkflowAdvancedFilterLogicalOperatorCell = ({ + index, + recordFilterGroup, +}: WorkflowAdvancedFilterLogicalOperatorCellProps) => { + return ( + + {index === 0 ? ( + Where + ) : index === 1 ? ( + + ) : ( + + {capitalize(recordFilterGroup.logicalOperator.toLowerCase())} + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterColumn.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterColumn.tsx similarity index 72% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterColumn.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterColumn.tsx index 3e7128d09..17ca45ca6 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterColumn.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterColumn.tsx @@ -1,14 +1,13 @@ -import { AdvancedFilterDropdownColumn } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownColumn'; import { AdvancedFilterFieldSelectDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButton'; -import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell'; import { AdvancedFilterRecordFilterOperandSelect } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect'; import { AdvancedFilterRecordFilterOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown'; -import { AdvancedFilterValueFormInput } from '@/object-record/advanced-filter/components/AdvancedFilterValueFormInput'; import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { WorkflowAdvancedFilterDropdownColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn'; +import { WorkflowAdvancedFilterValueFormInput } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterFormInput'; +import { WorkflowAdvancedFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterLogicalOperatorCell'; import styled from '@emotion/styled'; const StyledContainer = styled.div` @@ -18,16 +17,14 @@ const StyledContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export const AdvancedFilterRecordFilterColumn = ({ +export const WorkflowAdvancedFilterRecordFilterColumn = ({ recordFilterGroup, recordFilter, recordFilterIndex, - VariablePicker, }: { recordFilterGroup: RecordFilterGroup; recordFilter: RecordFilter; recordFilterIndex: number; - VariablePicker?: VariablePickerComponent; }) => { return ( - + - @@ -52,12 +49,12 @@ export const AdvancedFilterRecordFilterColumn = ({ /> - - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupChildren.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupChildren.tsx new file mode 100644 index 000000000..f7758512e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupChildren.tsx @@ -0,0 +1,56 @@ +import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; + +import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; +import { WorkflowAdvancedFilterRecordFilterColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterColumn'; +import styled from '@emotion/styled'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledContainer = styled.div<{ isGrayBackground?: boolean }>` + align-items: start; + background-color: ${({ theme, isGrayBackground }) => + isGrayBackground ? theme.background.transparent.lighter : 'transparent'}; + border: ${({ theme }) => `1px solid ${theme.border.color.medium}`}; + border-radius: ${({ theme }) => theme.border.radius.md}; + display: flex; + flex: 1; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +type WorkflowAdvancedFilterRecordFilterGroupChildrenProps = { + recordFilterGroupId: string; +}; + +export const WorkflowAdvancedFilterRecordFilterGroupChildren = ({ + recordFilterGroupId, +}: WorkflowAdvancedFilterRecordFilterGroupChildrenProps) => { + const { currentRecordFilterGroup, childRecordFilters } = + useChildRecordFiltersAndRecordFilterGroups({ + recordFilterGroupId, + }); + + if (!currentRecordFilterGroup) { + return null; + } + + const hasParentRecordFilterGroup = isDefined( + currentRecordFilterGroup.parentRecordFilterGroupId, + ); + + return ( + + {childRecordFilters.map((childRecordFilter, childRecordFilterIndex) => ( + + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupColumn.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupColumn.tsx similarity index 55% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupColumn.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupColumn.tsx index 0ea900f86..9cabfc6d9 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupColumn.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupColumn.tsx @@ -1,9 +1,8 @@ -import { AdvancedFilterDropdownColumn } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownColumn'; -import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell'; -import { AdvancedFilterRecordFilterGroupChildren } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildren'; import { AdvancedFilterRecordFilterGroupOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { WorkflowAdvancedFilterDropdownColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn'; +import { WorkflowAdvancedFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterLogicalOperatorCell'; +import { WorkflowAdvancedFilterRecordFilterGroupChildren } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupChildren'; import styled from '@emotion/styled'; const StyledContainer = styled.div` @@ -13,21 +12,19 @@ const StyledContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export const AdvancedFilterRecordFilterGroupColumn = ({ +export const WorkflowAdvancedFilterRecordFilterGroupColumn = ({ parentRecordFilterGroup, recordFilterGroup, recordFilterGroupIndex, - VariablePicker, }: { parentRecordFilterGroup: RecordFilterGroup; recordFilterGroup: RecordFilterGroup; recordFilterGroupIndex: number; - VariablePicker?: VariablePickerComponent; }) => { return ( - + - @@ -35,10 +32,9 @@ export const AdvancedFilterRecordFilterGroupColumn = ({ recordFilterGroupId={recordFilterGroup.id} /> - - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormCompositeFieldInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterValueFormCompositeFieldInput.tsx similarity index 81% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormCompositeFieldInput.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterValueFormCompositeFieldInput.tsx index 0ca288d1d..3542ee53f 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueFormCompositeFieldInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterValueFormCompositeFieldInput.tsx @@ -3,19 +3,17 @@ import { FormCountryCodeSelectInput } from '@/object-record/record-field/form-ty import { FormCountrySelectInput } from '@/object-record/record-field/form-types/components/FormCountrySelectInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; -import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { JsonValue } from 'type-fest'; -export const AdvancedFilterValueFormCompositeFieldInput = ({ +export const WorkflowAdvancedFilterValueFormCompositeFieldInput = ({ recordFilter, onChange, - VariablePicker, }: { recordFilter: RecordFilter; onChange: (newValue: JsonValue) => void; - VariablePicker?: VariablePickerComponent; }) => { const subFieldNameUsedInDropdown = useRecoilComponentValueV2( subFieldNameUsedInDropdownComponentState, @@ -30,13 +28,13 @@ export const AdvancedFilterValueFormCompositeFieldInput = ({ ) : ( ) ) : filterType === 'CURRENCY' ? ( @@ -44,13 +42,13 @@ export const AdvancedFilterValueFormCompositeFieldInput = ({ ) : recordFilter.subFieldName === 'amountMicros' ? ( ) : null ) : filterType === 'PHONES' ? ( @@ -58,20 +56,20 @@ export const AdvancedFilterValueFormCompositeFieldInput = ({ ) : ( ) ) : ( )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters.tsx index 51cc73cf1..c07108f29 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters.tsx @@ -1,8 +1,6 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; -import { AdvancedFilterRecordFilterColumn } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterColumn'; -import { AdvancedFilterRecordFilterGroupColumn } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupColumn'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector'; @@ -13,9 +11,10 @@ import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/u import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { WorkflowAdvancedFilterRecordFilterColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterColumn'; +import { WorkflowAdvancedFilterRecordFilterGroupColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterRecordFilterGroupColumn'; import { FindRecordsActionFilter } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; import { WorkflowFindRecordsAddFilterButton } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsAddFilterButton'; -import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import styled from '@emotion/styled'; import { useRecoilCallback, useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -110,7 +109,7 @@ export const WorkflowFindRecordsFilters = ({ {isDefined(rootRecordFilterGroup) ? ( @@ -121,20 +120,18 @@ export const WorkflowFindRecordsFilters = ({ isRecordFilterGroupChildARecordFilterGroup( recordFilterGroupChild, ) ? ( - ) : ( - ), )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts index 93f6f6f92..45f428dae 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts @@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { WorkflowActionType } from '@/workflow/types/Workflow'; import { FieldMetadataType } from '~/generated/graphql'; -const DISPLAYABLE_FIELD_TYPES_FOR_UPDATE = [ +const COMMON_DISPLAYABLE_FIELD_TYPES = [ FieldMetadataType.TEXT, FieldMetadataType.NUMBER, FieldMetadataType.DATE, @@ -20,6 +20,12 @@ const DISPLAYABLE_FIELD_TYPES_FOR_UPDATE = [ FieldMetadataType.UUID, ]; +const FIND_RECORDS_DISPLAYABLE_FIELD_TYPES = [ + ...COMMON_DISPLAYABLE_FIELD_TYPES, + FieldMetadataType.ARRAY, + FieldMetadataType.RELATION, +]; + export const shouldDisplayFormField = ({ fieldMetadataItem, actionType, @@ -37,9 +43,14 @@ export const shouldDisplayFormField = ({ break; case 'UPDATE_RECORD': isTypeAllowedForAction = - DISPLAYABLE_FIELD_TYPES_FOR_UPDATE.includes(fieldMetadataItem.type) || + COMMON_DISPLAYABLE_FIELD_TYPES.includes(fieldMetadataItem.type) || fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE'; break; + case 'FIND_RECORDS': + isTypeAllowedForAction = FIND_RECORDS_DISPLAYABLE_FIELD_TYPES.includes( + fieldMetadataItem.type, + ); + break; default: throw new Error(`Action "${actionType}" is not supported`); } diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index b9a50fe88..593dff7b7 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -26,4 +26,5 @@ export { isValidUrl } from './url/isValidUrl'; export { isDefined } from './validation/isDefined'; export { isValidLocale } from './validation/isValidLocale'; export { isValidUuid } from './validation/isValidUuid'; +export { isValidVariable } from './validation/isValidVariable'; export { normalizeLocale } from './validation/normalizeLocale'; diff --git a/packages/twenty-shared/src/utils/validation/isValidVariable.ts b/packages/twenty-shared/src/utils/validation/isValidVariable.ts new file mode 100644 index 000000000..948e0cd95 --- /dev/null +++ b/packages/twenty-shared/src/utils/validation/isValidVariable.ts @@ -0,0 +1,3 @@ +export const isValidVariable = (variable: string): boolean => { + return /^{{[^{}]+}}$/.test(variable); +};