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

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
const StyledColumn = styled.div`
display: flex;
flex-direction: column;
width: 100%;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const WorkflowAdvancedFilterDropdownColumn = StyledColumn;

View File

@ -0,0 +1,149 @@
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
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 { 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 WorkflowAdvancedFilterValueFormInput = ({
recordFilterId,
}: {
recordFilterId: string;
}) => {
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
subFieldNameUsedInDropdownComponentState,
);
const recordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const isDisabled = !recordFilter?.fieldMetadataId || !recordFilter.operand;
const operandHasNoInput =
(recordFilter &&
!configurableViewFilterOperands.has(recordFilter.operand)) ??
true;
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const handleChange = (newValue: JsonValue) => {
if (typeof newValue === 'string') {
applyObjectFilterDropdownFilterValue(newValue);
} else if (Array.isArray(newValue) || isObject(newValue)) {
applyObjectFilterDropdownFilterValue(JSON.stringify(newValue));
} else {
applyObjectFilterDropdownFilterValue(String(newValue));
}
};
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const fieldDefinition = fieldMetadataItemUsedInDropdown
? formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItemUsedInDropdown,
objectMetadataItem: objectMetadataItem,
})
: null;
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 (
<WorkflowAdvancedFilterValueFormCompositeFieldInput
recordFilter={recordFilter}
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: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
};
return (
<FormFieldInput
field={field}
defaultValue={recordFilter.value}
onChange={handleChange}
// 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

@ -0,0 +1,60 @@
import { AdvancedFilterFieldSelectDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButton';
import { AdvancedFilterRecordFilterOperandSelect } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect';
import { AdvancedFilterRecordFilterOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown';
import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
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`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const WorkflowAdvancedFilterRecordFilterColumn = ({
recordFilterGroup,
recordFilter,
recordFilterIndex,
}: {
recordFilterGroup: RecordFilterGroup;
recordFilter: RecordFilter;
recordFilterIndex: number;
}) => {
return (
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{
instanceId: getAdvancedFilterObjectFilterDropdownComponentInstanceId(
recordFilter.id,
),
}}
>
<WorkflowAdvancedFilterDropdownColumn>
<StyledContainer>
<WorkflowAdvancedFilterLogicalOperatorCell
index={recordFilterIndex}
recordFilterGroup={recordFilterGroup}
/>
<AdvancedFilterRecordFilterOptionsDropdown
recordFilterId={recordFilter.id}
/>
</StyledContainer>
<AdvancedFilterFieldSelectDropdownButton
recordFilterId={recordFilter.id}
/>
<AdvancedFilterRecordFilterOperandSelect
recordFilterId={recordFilter.id}
widthFromProps="auto"
/>
<WorkflowAdvancedFilterValueFormInput
recordFilterId={recordFilter.id}
/>
</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

@ -0,0 +1,40 @@
import { AdvancedFilterRecordFilterGroupOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown';
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`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const WorkflowAdvancedFilterRecordFilterGroupColumn = ({
parentRecordFilterGroup,
recordFilterGroup,
recordFilterGroupIndex,
}: {
parentRecordFilterGroup: RecordFilterGroup;
recordFilterGroup: RecordFilterGroup;
recordFilterGroupIndex: number;
}) => {
return (
<WorkflowAdvancedFilterDropdownColumn>
<StyledContainer>
<WorkflowAdvancedFilterLogicalOperatorCell
index={recordFilterGroupIndex}
recordFilterGroup={parentRecordFilterGroup}
/>
<AdvancedFilterRecordFilterGroupOptionsDropdown
recordFilterGroupId={recordFilterGroup.id}
/>
</StyledContainer>
<WorkflowAdvancedFilterRecordFilterGroupChildren
recordFilterGroupId={recordFilterGroup.id}
/>
</WorkflowAdvancedFilterDropdownColumn>
);
};

View File

@ -0,0 +1,77 @@
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { FormCountryCodeSelectInput } from '@/object-record/record-field/form-types/components/FormCountryCodeSelectInput';
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 { 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 WorkflowAdvancedFilterValueFormCompositeFieldInput = ({
recordFilter,
onChange,
}: {
recordFilter: RecordFilter;
onChange: (newValue: JsonValue) => void;
}) => {
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
subFieldNameUsedInDropdownComponentState,
);
const filterType = recordFilter.type;
return (
<>
{filterType === 'ADDRESS' ? (
subFieldNameUsedInDropdown === 'addressCountry' ? (
<FormCountrySelectInput
selectedCountryName={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
)
) : filterType === 'CURRENCY' ? (
recordFilter.subFieldName === 'currencyCode' ? (
<FormCountryCodeSelectInput
selectedCountryCode={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
) : recordFilter.subFieldName === 'amountMicros' ? (
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
) : null
) : filterType === 'PHONES' ? (
recordFilter.subFieldName === 'primaryPhoneNumber' ? (
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
/>
)
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
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`);
}