Support primitive types in filters (#15402)
When using primitive types such as array, number and boolean, we display a text field in filters because fieldmetadataId is empty. We should instead support these as we would do for our own fields. Adding also a fix for https://github.com/twentyhq/twenty/issues/15282 --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
+40
-8
@@ -2,7 +2,9 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada
|
||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
|
||||
import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput';
|
||||
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
|
||||
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
|
||||
import { FormRelativeDatePicker } from '@/object-record/record-field/ui/form-types/components/FormRelativeDatePicker';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
type StepFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { parseBooleanFromStringValue } from 'twenty-shared/workflow';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
type WorkflowStepFilterValueInputProps = {
|
||||
@@ -35,9 +38,9 @@ const COMPOSITE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.CURRENCY,
|
||||
];
|
||||
|
||||
const isFilterableFieldMetadataType = (
|
||||
const isFilterableFieldType = (
|
||||
type: string,
|
||||
): type is FieldMetadataType => {
|
||||
): type is FieldMetadataType | 'array' | 'boolean' | 'number' => {
|
||||
return [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.NUMBER,
|
||||
@@ -53,6 +56,9 @@ const isFilterableFieldMetadataType = (
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
...COMPOSITE_FIELD_METADATA_TYPES,
|
||||
'array',
|
||||
'boolean',
|
||||
'number',
|
||||
].includes(type as FieldMetadataType);
|
||||
};
|
||||
|
||||
@@ -123,11 +129,7 @@ export const WorkflowStepFilterValueInput = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(variableType) ||
|
||||
!isFilterableFieldMetadataType(variableType) ||
|
||||
!isDefined(selectedFieldMetadataItem)
|
||||
) {
|
||||
if (!isDefined(variableType) || !isFilterableFieldType(variableType)) {
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
@@ -174,7 +176,7 @@ export const WorkflowStepFilterValueInput = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (variableType === FieldMetadataType.ARRAY) {
|
||||
if (variableType === FieldMetadataType.ARRAY || variableType === 'array') {
|
||||
const arrayValue = parseJson<string[]>(stepFilter.value) ?? [];
|
||||
|
||||
return (
|
||||
@@ -186,6 +188,36 @@ export const WorkflowStepFilterValueInput = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
variableType === FieldMetadataType.BOOLEAN ||
|
||||
variableType === 'boolean'
|
||||
) {
|
||||
const parsedValue = parseBooleanFromStringValue(stepFilter.value) as
|
||||
| boolean
|
||||
| undefined
|
||||
| string;
|
||||
|
||||
return (
|
||||
<FormBooleanFieldInput
|
||||
defaultValue={parsedValue}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (variableType === 'number') {
|
||||
return (
|
||||
<FormNumberFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const field = {
|
||||
type: variableType as FieldMetadataType,
|
||||
label: '',
|
||||
|
||||
+1
-1
@@ -67,6 +67,6 @@ export const NumberInput: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('100')).toBeVisible();
|
||||
await expect(await canvas.findByDisplayValue(100)).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
+4
-1
@@ -60,7 +60,7 @@ export const FILTER_OPERANDS_MAP = {
|
||||
...emptyOperands,
|
||||
],
|
||||
BOOLEAN: [ViewFilterOperand.IS],
|
||||
UUID: [ViewFilterOperand.IS],
|
||||
UUID: [ViewFilterOperand.IS, ViewFilterOperand.IS_NOT],
|
||||
NUMERIC: [
|
||||
ViewFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
ViewFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
@@ -108,6 +108,7 @@ export const getStepFilterOperands = ({
|
||||
}
|
||||
}
|
||||
case 'NUMBER':
|
||||
case 'number':
|
||||
return FILTER_OPERANDS_MAP.NUMBER;
|
||||
case 'RAW_JSON':
|
||||
return FILTER_OPERANDS_MAP.RAW_JSON;
|
||||
@@ -123,8 +124,10 @@ export const getStepFilterOperands = ({
|
||||
case 'SELECT':
|
||||
return FILTER_OPERANDS_MAP.SELECT;
|
||||
case 'ARRAY':
|
||||
case 'array':
|
||||
return FILTER_OPERANDS_MAP.ARRAY;
|
||||
case 'BOOLEAN':
|
||||
case 'boolean':
|
||||
return FILTER_OPERANDS_MAP.BOOLEAN;
|
||||
case 'UUID':
|
||||
return FILTER_OPERANDS_MAP.UUID;
|
||||
|
||||
+6
-1
@@ -412,7 +412,12 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle truthy/falsy conversion', () => {
|
||||
const filter = createFilter(ViewFilterOperand.IS, 1, true, 'BOOLEAN');
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IS,
|
||||
'true',
|
||||
true,
|
||||
'BOOLEAN',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
+12
-1
@@ -11,6 +11,7 @@ import {
|
||||
type ViewFilterOperandDeprecated,
|
||||
} from 'twenty-shared/types';
|
||||
import { convertViewFilterOperandToCoreOperand as convertViewFilterOperandDeprecated } from 'twenty-shared/utils';
|
||||
import { parseBooleanFromStringValue } from 'twenty-shared/workflow';
|
||||
|
||||
import { parseAndEvaluateRelativeDateFilter } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util';
|
||||
|
||||
@@ -43,6 +44,7 @@ function evaluateFilter(
|
||||
switch (filter.type) {
|
||||
case 'NUMBER':
|
||||
case 'NUMERIC':
|
||||
case 'number':
|
||||
return evaluateNumberFilter(filterWithConvertedOperand);
|
||||
case 'DATE':
|
||||
case 'DATE_TIME':
|
||||
@@ -55,11 +57,13 @@ function evaluateFilter(
|
||||
case 'ADDRESS':
|
||||
case 'LINKS':
|
||||
case 'ARRAY':
|
||||
case 'array':
|
||||
case 'RAW_JSON':
|
||||
return evaluateTextAndArrayFilter(filterWithConvertedOperand);
|
||||
case 'SELECT':
|
||||
return evaluateSelectFilter(filterWithConvertedOperand);
|
||||
case 'BOOLEAN':
|
||||
case 'boolean':
|
||||
return evaluateBooleanFilter(filterWithConvertedOperand);
|
||||
case 'UUID':
|
||||
return evaluateUuidFilter(filterWithConvertedOperand);
|
||||
@@ -168,7 +172,10 @@ function isNotEmptyTextOrArray(value: unknown): boolean {
|
||||
function evaluateBooleanFilter(filter: ResolvedFilter): boolean {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.IS:
|
||||
return Boolean(filter.leftOperand) === Boolean(filter.rightOperand);
|
||||
return (
|
||||
parseBooleanFromStringValue(filter.leftOperand) ===
|
||||
parseBooleanFromStringValue(filter.rightOperand)
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for boolean filter`,
|
||||
@@ -263,6 +270,10 @@ function evaluateRelationFilter(filter: ResolvedFilter): boolean {
|
||||
return leftValue === rightValue;
|
||||
case ViewFilterOperand.IS_NOT:
|
||||
return leftValue !== rightValue;
|
||||
case ViewFilterOperand.IS_EMPTY:
|
||||
return !isNonEmptyString(leftValue);
|
||||
case ViewFilterOperand.IS_NOT_EMPTY:
|
||||
return isNonEmptyString(leftValue);
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for relation filter`,
|
||||
|
||||
@@ -64,6 +64,7 @@ export { StepStatus } from './types/WorkflowRunStateStepInfos';
|
||||
export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow';
|
||||
export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts';
|
||||
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
|
||||
export { parseBooleanFromStringValue } from './utils/parseBooleanFromStringValue';
|
||||
export { parseDataFromContentType } from './utils/parseDataFromContentType';
|
||||
export type {
|
||||
LeafType,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const parseBooleanFromStringValue = (value: unknown): boolean | unknown => {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
Reference in New Issue
Block a user