Search action - Add variables to select and relations + other fixes (#12604)

- Variables can now be handled for select/multiselect/relations
- Hide field not supported in forms (source, rating)
- Add tests for schemas

Remaning issues:
- country/currency pickers not working
- stories for components
- variable picker hidden for dates
This commit is contained in:
Thomas Trompette
2025-06-16 13:45:28 +02:00
committed by GitHub
parent e0cb53af48
commit ae57e67c77
33 changed files with 621 additions and 234 deletions

View File

@ -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 (
<StyledContainer>
{index === 0 ? (
<StyledText noPadding={isColumn}>Where</StyledText>
<StyledText>Where</StyledText>
) : index === 1 ? (
<AdvancedFilterLogicalOperatorDropdown
recordFilterGroup={recordFilterGroup}
/>
) : (
<StyledText noPadding={isColumn}>
<StyledText>
{capitalize(recordFilterGroup.logicalOperator.toLowerCase())}
</StyledText>
)}

View File

@ -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 ? (
<AdvancedFilterRecordFilterColumn
recordFilterGroup={recordFilterGroup}
recordFilter={recordFilter}
recordFilterIndex={recordFilterIndex}
VariablePicker={VariablePicker}
/>
) : (
<AdvancedFilterRecordFilterRow
recordFilterGroup={recordFilterGroup}
recordFilter={recordFilter}
recordFilterIndex={recordFilterIndex}
/>
);
};

View File

@ -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 ? (
<AdvancedFilterRecordFilterGroupColumn
parentRecordFilterGroup={parentRecordFilterGroup}
recordFilterGroup={recordFilterGroup}
recordFilterGroupIndex={recordFilterGroupIndex}
VariablePicker={VariablePicker}
/>
) : (
<AdvancedFilterRecordFilterGroupRow
parentRecordFilterGroup={parentRecordFilterGroup}
recordFilterGroup={recordFilterGroup}
recordFilterGroupIndex={recordFilterGroupIndex}
VariablePicker={VariablePicker}
/>
);
};

View File

@ -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 (
<StyledContainer isGrayBackground={hasParentRecordFilterGroup}>
{childRecordFilters.map((childRecordFilter, childRecordFilterIndex) => (
<AdvancedFilterRecordFilter
<AdvancedFilterRecordFilterRow
key={childRecordFilter.id}
recordFilter={childRecordFilter}
recordFilterIndex={childRecordFilterIndex}
recordFilterGroup={currentRecordFilterGroup}
VariablePicker={VariablePicker}
/>
))}
<AdvancedFilterAddFilterRuleSelect

View File

@ -2,19 +2,16 @@ import { AdvancedFilterDropdownRow } from '@/object-record/advanced-filter/compo
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';
export const AdvancedFilterRecordFilterGroupRow = ({
parentRecordFilterGroup,
recordFilterGroup,
recordFilterGroupIndex,
VariablePicker,
}: {
parentRecordFilterGroup: RecordFilterGroup;
recordFilterGroup: RecordFilterGroup;
recordFilterGroupIndex: number;
VariablePicker?: VariablePickerComponent;
}) => {
return (
<AdvancedFilterDropdownRow>
@ -24,7 +21,6 @@ export const AdvancedFilterRecordFilterGroupRow = ({
/>
<AdvancedFilterRecordFilterGroupChildren
recordFilterGroupId={recordFilterGroup.id}
VariablePicker={VariablePicker}
/>
<AdvancedFilterRecordFilterGroupOptionsDropdown
recordFilterGroupId={recordFilterGroup.id}

View File

@ -1,5 +1,4 @@
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
@ -17,20 +16,21 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { MenuItem } from 'twenty-ui/navigation';
const StyledContainer = styled.div<{ isColumn?: boolean }>`
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 (
<StyledContainer isColumn={isColumn}>
<StyledContainer width={widthFromProps}>
<Dropdown
dropdownId={dropdownId}
clickableComponent={

View File

@ -1,6 +1,6 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterRecordFilter } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilter';
import { AdvancedFilterRecordFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup';
import { AdvancedFilterRecordFilterGroupRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupRow';
import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow';
import { ADVANCED_FILTER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/advanced-filter/constants/AdvancedFilterDropdownContentWidth';
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
@ -45,14 +45,14 @@ export const AdvancedFilterRootRecordFilterGroup = () => {
isRecordFilterGroupChildARecordFilterGroup(
recordFilterGroupChild,
) ? (
<AdvancedFilterRecordFilterGroup
<AdvancedFilterRecordFilterGroupRow
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterRecordFilter
<AdvancedFilterRecordFilterRow
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}

View File

@ -2,14 +2,12 @@ import { AdvancedFilterDropdownFilterInput } from '@/object-record/advanced-filt
import { AdvancedFilterDropdownTextInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput';
import { AdvancedFilterValueInputDropdownButtonClickableSelect } from '@/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect';
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
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 { shouldShowFilterTextInput } from '@/object-record/advanced-filter/utils/shouldShowFilterTextInput';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
@ -17,7 +15,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
const StyledValueDropdownContainer = styled.div`
@ -83,31 +80,10 @@ export const AdvancedFilterValueInput = ({
? ({ y: -33, x: 0 } satisfies DropdownOffset)
: DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET;
const isFilterableByTextValue =
isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType));
const isCurrencyAmountMicrosFilter = isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'amountMicros',
recordFilter.subFieldName,
);
const isAddressFilterOnSubFieldOtherThanCountry =
filterType === 'ADDRESS' && subFieldNameUsedInDropdown !== 'addressCountry';
const isActorNameFilter = isExpectedSubFieldName(
FieldMetadataType.ACTOR,
'name',
recordFilter.subFieldName,
);
const showFilterTextInputInsteadOfDropdown =
isFilterableByTextValue ||
isCurrencyAmountMicrosFilter ||
isAddressFilterOnSubFieldOtherThanCountry ||
isActorNameFilter;
const showFilterTextInputInsteadOfDropdown = shouldShowFilterTextInput({
recordFilter,
subFieldNameUsedInDropdown,
});
return (
<StyledValueDropdownContainer>

View File

@ -2,7 +2,7 @@ import { createContext } from 'react';
type AdvancedFilterContextType = {
onUpdate?: () => void;
isColumn?: boolean;
isWorkflowFindRecords?: boolean;
};
export const AdvancedFilterContext = createContext<AdvancedFilterContextType>(

View File

@ -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
);
};

View File

@ -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,
),
})

View File

@ -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 (
<FormSelectFieldInput
label="Country Code"
label={label}
onChange={onCountryCodeChange}
options={options}
defaultValue={selectedCountryCode}

View File

@ -55,13 +55,14 @@ export const FormPhoneFieldInput = ({
{label && <InputLabel>{label}</InputLabel>}
<FormNestedFieldInputContainer>
<FormCountryCodeSelectInput
selectedCountryCode={defaultValue?.primaryPhoneCountryCode || ''}
label="Country Code"
selectedCountryCode={defaultValue?.primaryPhoneCountryCode ?? ''}
onChange={handleCountryChange}
readonly={readonly}
/>
<FormNumberFieldInput
label="Phone Number"
defaultValue={defaultValue?.primaryPhoneNumber || ''}
defaultValue={defaultValue?.primaryPhoneNumber ?? ''}
onChange={handleNumberChange}
VariablePicker={VariablePicker}
placeholder="Enter phone number"

View File

@ -6,7 +6,9 @@ import { FormCountryCodeSelectInput } from '../FormCountryCodeSelectInput';
const meta: Meta<typeof FormCountryCodeSelectInput> = {
title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput',
component: FormCountryCodeSelectInput,
args: {},
args: {
label: 'Country Code',
},
argTypes: {},
};

View File

@ -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 };
};

View File

@ -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;

View File

@ -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<ViewFilter, 'value'>,
) => {
return selectViewFilterValueSchema.parse(viewFilter.value);
};

View File

@ -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([]);
}
});
});
});

View File

@ -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([]);
}
});
});
});

View File

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

View File

@ -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([]);

View File

@ -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([]);

View File

@ -7,4 +7,4 @@ const StyledColumn = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
export const AdvancedFilterDropdownColumn = StyledColumn;
export const WorkflowAdvancedFilterDropdownColumn = StyledColumn;

View File

@ -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 (
<FormTextFieldInput
label={''}
defaultValue={recordFilter.value}
onChange={handleChange}
VariablePicker={WorkflowVariablePicker}
/>
);
}
if (isDefined(subFieldNameUsedInDropdown)) {
return (
<AdvancedFilterValueFormCompositeFieldInput
<WorkflowAdvancedFilterValueFormCompositeFieldInput
recordFilter={recordFilter}
VariablePicker={VariablePicker}
onChange={handleChange}
/>
);
}
if (isFilterableByMultiSelectValue) {
const metadata = fieldDefinition?.metadata as
| FieldMultiSelectMetadata
| FieldSelectMetadata
| undefined;
return (
<FormMultiSelectFieldInput
label={''}
defaultValue={recordFilter.value}
onChange={handleChange}
VariablePicker={WorkflowVariablePicker}
options={metadata?.options ?? []}
/>
);
}
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
}
/>
);
};

View File

@ -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 (
<StyledContainer>
{index === 0 ? (
<StyledText>Where</StyledText>
) : index === 1 ? (
<AdvancedFilterLogicalOperatorDropdown
recordFilterGroup={recordFilterGroup}
/>
) : (
<StyledText>
{capitalize(recordFilterGroup.logicalOperator.toLowerCase())}
</StyledText>
)}
</StyledContainer>
);
};

View File

@ -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 (
<ObjectFilterDropdownComponentInstanceContext.Provider
@ -37,9 +34,9 @@ export const AdvancedFilterRecordFilterColumn = ({
),
}}
>
<AdvancedFilterDropdownColumn>
<WorkflowAdvancedFilterDropdownColumn>
<StyledContainer>
<AdvancedFilterLogicalOperatorCell
<WorkflowAdvancedFilterLogicalOperatorCell
index={recordFilterIndex}
recordFilterGroup={recordFilterGroup}
/>
@ -52,12 +49,12 @@ export const AdvancedFilterRecordFilterColumn = ({
/>
<AdvancedFilterRecordFilterOperandSelect
recordFilterId={recordFilter.id}
widthFromProps="auto"
/>
<AdvancedFilterValueFormInput
<WorkflowAdvancedFilterValueFormInput
recordFilterId={recordFilter.id}
VariablePicker={VariablePicker}
/>
</AdvancedFilterDropdownColumn>
</WorkflowAdvancedFilterDropdownColumn>
</ObjectFilterDropdownComponentInstanceContext.Provider>
);
};

View File

@ -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 (
<StyledContainer isGrayBackground={hasParentRecordFilterGroup}>
{childRecordFilters.map((childRecordFilter, childRecordFilterIndex) => (
<WorkflowAdvancedFilterRecordFilterColumn
key={childRecordFilter.id}
recordFilter={childRecordFilter}
recordFilterIndex={childRecordFilterIndex}
recordFilterGroup={currentRecordFilterGroup}
/>
))}
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={currentRecordFilterGroup}
/>
</StyledContainer>
);
};

View File

@ -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 (
<AdvancedFilterDropdownColumn>
<WorkflowAdvancedFilterDropdownColumn>
<StyledContainer>
<AdvancedFilterLogicalOperatorCell
<WorkflowAdvancedFilterLogicalOperatorCell
index={recordFilterGroupIndex}
recordFilterGroup={parentRecordFilterGroup}
/>
@ -35,10 +32,9 @@ export const AdvancedFilterRecordFilterGroupColumn = ({
recordFilterGroupId={recordFilterGroup.id}
/>
</StyledContainer>
<AdvancedFilterRecordFilterGroupChildren
<WorkflowAdvancedFilterRecordFilterGroupChildren
recordFilterGroupId={recordFilterGroup.id}
VariablePicker={VariablePicker}
/>
</AdvancedFilterDropdownColumn>
</WorkflowAdvancedFilterDropdownColumn>
);
};

View File

@ -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 = ({
<FormCountrySelectInput
selectedCountryName={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
)
) : filterType === 'CURRENCY' ? (
@ -44,13 +42,13 @@ export const AdvancedFilterValueFormCompositeFieldInput = ({
<FormCountryCodeSelectInput
selectedCountryCode={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
) : recordFilter.subFieldName === 'amountMicros' ? (
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
) : null
) : filterType === 'PHONES' ? (
@ -58,20 +56,20 @@ export const AdvancedFilterValueFormCompositeFieldInput = ({
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
)
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
VariablePicker={WorkflowVariablePicker}
/>
)}
</>

View File

@ -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 = ({
<AdvancedFilterContext.Provider
value={{
onUpdate,
isColumn: true,
isWorkflowFindRecords: true,
}}
>
{isDefined(rootRecordFilterGroup) ? (
@ -121,20 +120,18 @@ export const WorkflowFindRecordsFilters = ({
isRecordFilterGroupChildARecordFilterGroup(
recordFilterGroupChild,
) ? (
<AdvancedFilterRecordFilterGroupColumn
<WorkflowAdvancedFilterRecordFilterGroupColumn
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<AdvancedFilterRecordFilterColumn
<WorkflowAdvancedFilterRecordFilterColumn
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
VariablePicker={WorkflowVariablePicker}
/>
),
)}

View File

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

View File

@ -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';

View File

@ -0,0 +1,3 @@
export const isValidVariable = (variable: string): boolean => {
return /^{{[^{}]+}}$/.test(variable);
};