Manage composite fields in step filters (#13407)
- add to step output schema the information that field is a composite sub field - from output schema, when selecting the variable, copy all info required to stepFilter - from stepFilter, when selecting a value, display a special component for composites
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
||||
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
|
||||
@ -38,6 +39,8 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
|
||||
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(variableName: string) => {
|
||||
@ -54,24 +57,39 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const { variableLabel, variableType } =
|
||||
searchVariableThroughOutputSchema({
|
||||
const {
|
||||
variableLabel,
|
||||
variableType,
|
||||
fieldMetadataId,
|
||||
compositeFieldSubFieldName,
|
||||
} = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: currentStepOutputSchema?.[0],
|
||||
rawVariableName: variableName,
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
const filterType = isDefined(fieldMetadataId)
|
||||
? getFieldMetadataItemById(fieldMetadataId).type
|
||||
: variableType;
|
||||
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
stepOutputKey: variableName,
|
||||
displayValue: variableLabel ?? '',
|
||||
type: variableType ?? 'unknown',
|
||||
type: filterType ?? 'unknown',
|
||||
value: '',
|
||||
fieldMetadataId,
|
||||
compositeFieldSubFieldName,
|
||||
},
|
||||
});
|
||||
},
|
||||
[upsertStepFilterSettings, stepFilter, workflowVersionId],
|
||||
[
|
||||
upsertStepFilterSettings,
|
||||
stepFilter,
|
||||
workflowVersionId,
|
||||
getFieldMetadataItemById,
|
||||
],
|
||||
);
|
||||
|
||||
if (!isDefined(stepId)) {
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import { FormCountryMultiSelectInput } from '@/object-record/record-field/form-types/components/FormCountryMultiSelectInput';
|
||||
import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
|
||||
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilter } from 'twenty-shared/types';
|
||||
import { JsonValue } from 'type-fest';
|
||||
|
||||
export const WorkflowStepFilterValueCompositeInput = ({
|
||||
stepFilter,
|
||||
onChange,
|
||||
}: {
|
||||
stepFilter: StepFilter;
|
||||
onChange: (newValue: JsonValue) => void;
|
||||
}) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
const { type: filterType, compositeFieldSubFieldName: subFieldName } =
|
||||
stepFilter;
|
||||
|
||||
return (
|
||||
<>
|
||||
{filterType === 'ADDRESS' ? (
|
||||
subFieldName === 'addressCountry' ? (
|
||||
<FormCountryMultiSelectInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
) : filterType === 'CURRENCY' ? (
|
||||
subFieldName === 'currencyCode' ? (
|
||||
<FormMultiSelectFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
options={CURRENCIES}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : subFieldName === 'amountMicros' ? (
|
||||
<FormNumberFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : null
|
||||
) : filterType === 'PHONES' ? (
|
||||
subFieldName === 'primaryPhoneNumber' ? (
|
||||
<FormNumberFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={onChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -3,17 +3,13 @@ import { configurableViewFilterOperands } from '@/object-record/object-filter-dr
|
||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
||||
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
|
||||
import { WorkflowStepFilterValueCompositeInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueCompositeInput';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isObject, isString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType, StepFilter } from 'twenty-shared/src/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { JsonValue } from 'type-fest';
|
||||
@ -22,6 +18,14 @@ type WorkflowStepFilterValueInputProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
const COMPOSITE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.EMAILS,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.CURRENCY,
|
||||
];
|
||||
|
||||
const isFilterableFieldMetadataType = (
|
||||
type: string,
|
||||
): type is FieldMetadataType => {
|
||||
@ -37,6 +41,8 @@ const isFilterableFieldMetadataType = (
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.UUID,
|
||||
...COMPOSITE_FIELD_METADATA_TYPES,
|
||||
].includes(type as FieldMetadataType);
|
||||
};
|
||||
|
||||
@ -47,24 +53,6 @@ export const WorkflowStepFilterValueInput = ({
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
const { workflowVersionId } = useWorkflowStepContextOrThrow();
|
||||
|
||||
const stepId = extractRawVariableNamePart({
|
||||
rawVariableName: stepFilter.stepOutputKey,
|
||||
part: 'stepId',
|
||||
});
|
||||
|
||||
const stepsOutputSchema = useRecoilValue(
|
||||
stepsOutputSchemaFamilySelector({
|
||||
workflowVersionId,
|
||||
stepIds: [stepId],
|
||||
}),
|
||||
);
|
||||
const { variableType, fieldMetadataId } = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: stepsOutputSchema?.[0],
|
||||
rawVariableName: stepFilter.stepOutputKey,
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
const handleValueChange = (value: JsonValue) => {
|
||||
const valueToUpsert = isString(value)
|
||||
@ -92,11 +80,44 @@ export const WorkflowStepFilterValueInput = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDefined(variableType) && isFilterableFieldMetadataType(variableType)) {
|
||||
const {
|
||||
fieldMetadataId,
|
||||
type: variableType,
|
||||
compositeFieldSubFieldName,
|
||||
} = stepFilter;
|
||||
|
||||
const selectedFieldMetadataItem = isDefined(fieldMetadataId)
|
||||
? getFieldMetadataItemById(fieldMetadataId)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
!isDefined(variableType) ||
|
||||
!isFilterableFieldMetadataType(variableType) ||
|
||||
!isDefined(selectedFieldMetadataItem)
|
||||
) {
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
placeholder={t`Enter value`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(compositeFieldSubFieldName) &&
|
||||
COMPOSITE_FIELD_METADATA_TYPES.includes(variableType as FieldMetadataType)
|
||||
) {
|
||||
return (
|
||||
<WorkflowStepFilterValueCompositeInput
|
||||
stepFilter={stepFilter}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const field = {
|
||||
type: variableType as FieldMetadataType,
|
||||
label: '',
|
||||
@ -116,15 +137,4 @@ export const WorkflowStepFilterValueInput = ({
|
||||
placeholder={t`Enter value`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
placeholder={t`Enter value`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ type Leaf = {
|
||||
description?: string;
|
||||
value: any;
|
||||
fieldMetadataId?: string;
|
||||
isCompositeSubField?: boolean;
|
||||
};
|
||||
|
||||
type Node = {
|
||||
@ -18,6 +19,7 @@ type Node = {
|
||||
value: OutputSchema;
|
||||
description?: string;
|
||||
fieldMetadataId?: string;
|
||||
isCompositeSubField?: boolean;
|
||||
};
|
||||
|
||||
type Link = {
|
||||
|
||||
@ -8,6 +8,14 @@ import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOu
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type VariableInfo = {
|
||||
variableLabel: string | undefined;
|
||||
variablePathLabel: string | undefined;
|
||||
variableType?: string | undefined;
|
||||
fieldMetadataId?: string | undefined;
|
||||
compositeFieldSubFieldName?: string | undefined;
|
||||
};
|
||||
|
||||
const getDisplayedSubStepObjectLabel = (outputSchema: OutputSchema) => {
|
||||
if (!isRecordOutputSchema(outputSchema)) {
|
||||
return;
|
||||
@ -54,6 +62,17 @@ const getFieldMetadataId = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isCompositeSubField = (
|
||||
key: string,
|
||||
outputSchema: OutputSchema,
|
||||
): boolean => {
|
||||
if (isBaseOutputSchema(outputSchema) && outputSchema[key]?.isLeaf) {
|
||||
return outputSchema[key]?.isCompositeSubField ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const searchCurrentStepOutputSchema = ({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
@ -70,6 +89,7 @@ const searchCurrentStepOutputSchema = ({
|
||||
let nextKey = path[nextKeyIndex];
|
||||
let variablePathLabel = stepOutputSchema.name;
|
||||
let isSelectedFieldInNextKey = false;
|
||||
let parentFieldMetadataId: string | undefined;
|
||||
|
||||
const handleFieldNotFound = () => {
|
||||
if (nextKeyIndex + 1 < path.length) {
|
||||
@ -94,6 +114,7 @@ const searchCurrentStepOutputSchema = ({
|
||||
currentSubStep = currentField.value;
|
||||
nextKey = path[nextKeyIndex + 1];
|
||||
variablePathLabel = `${variablePathLabel} > ${currentField.label}`;
|
||||
parentFieldMetadataId = currentField.fieldMetadataId;
|
||||
} else {
|
||||
handleFieldNotFound();
|
||||
}
|
||||
@ -103,6 +124,7 @@ const searchCurrentStepOutputSchema = ({
|
||||
currentSubStep = currentField.value;
|
||||
nextKey = path[nextKeyIndex + 1];
|
||||
variablePathLabel = `${variablePathLabel} > ${currentField.label}`;
|
||||
parentFieldMetadataId = currentField.fieldMetadataId;
|
||||
} else {
|
||||
handleFieldNotFound();
|
||||
}
|
||||
@ -118,23 +140,24 @@ const searchCurrentStepOutputSchema = ({
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel:
|
||||
const variableName = isSelectedFieldInNextKey ? nextKey : selectedField;
|
||||
const variableLabel =
|
||||
isFullRecord && isRecordOutputSchema(currentSubStep)
|
||||
? getDisplayedSubStepObjectLabel(currentSubStep)
|
||||
: getDisplayedSubStepFieldLabel(
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
: getDisplayedSubStepFieldLabel(variableName, currentSubStep);
|
||||
|
||||
return {
|
||||
variableLabel,
|
||||
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||
variableType: getVariableType(variableName, currentSubStep),
|
||||
fieldMetadataId:
|
||||
getFieldMetadataId(variableName, currentSubStep) ?? parentFieldMetadataId,
|
||||
compositeFieldSubFieldName: isCompositeSubField(
|
||||
variableName,
|
||||
currentSubStep,
|
||||
),
|
||||
variablePathLabel,
|
||||
variableType: getVariableType(
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
currentSubStep,
|
||||
),
|
||||
fieldMetadataId: getFieldMetadataId(
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
currentSubStep,
|
||||
),
|
||||
)
|
||||
? variableName
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@ -146,7 +169,7 @@ export const searchVariableThroughOutputSchema = ({
|
||||
stepOutputSchema: StepOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}) => {
|
||||
}): VariableInfo => {
|
||||
if (!isDefined(stepOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
@ -175,18 +198,10 @@ export const searchVariableThroughOutputSchema = ({
|
||||
};
|
||||
}
|
||||
|
||||
const { variableLabel, variablePathLabel, variableType, fieldMetadataId } =
|
||||
searchCurrentStepOutputSchema({
|
||||
return searchCurrentStepOutputSchema({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
isFullRecord,
|
||||
selectedField,
|
||||
});
|
||||
|
||||
return {
|
||||
variableLabel,
|
||||
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||
variableType,
|
||||
fieldMetadataId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ export type Leaf = {
|
||||
description?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any;
|
||||
isCompositeSubField?: boolean;
|
||||
};
|
||||
|
||||
export type Node = {
|
||||
@ -29,7 +30,9 @@ type Link = {
|
||||
export type BaseOutputSchema = Record<string, Leaf | Node>;
|
||||
|
||||
export type FieldOutputSchema =
|
||||
| ((Leaf | Node) & { fieldMetadataId?: string })
|
||||
| ((Leaf | Node) & {
|
||||
fieldMetadataId?: string;
|
||||
})
|
||||
| RecordOutputSchema;
|
||||
|
||||
export type RecordOutputSchema = {
|
||||
|
||||
@ -158,12 +158,16 @@ describe('generateFakeField', () => {
|
||||
type: FieldMetadataType.LINKS,
|
||||
value: {
|
||||
label: {
|
||||
fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Label',
|
||||
value: 'Fake Label',
|
||||
},
|
||||
url: {
|
||||
fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Url',
|
||||
@ -199,12 +203,16 @@ describe('generateFakeField', () => {
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
value: {
|
||||
amount: {
|
||||
fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Amount',
|
||||
value: 100,
|
||||
},
|
||||
currencyCode: {
|
||||
fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Currency Code',
|
||||
|
||||
@ -80,18 +80,24 @@ describe('generateFakeFormResponse', () => {
|
||||
"type": "LINKS",
|
||||
"value": {
|
||||
"primaryLinkLabel": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Primary Link Label",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
},
|
||||
"primaryLinkUrl": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Primary Link Url",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
},
|
||||
"secondaryLinks": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Secondary Links",
|
||||
"type": "RAW_JSON",
|
||||
|
||||
@ -37,6 +37,8 @@ export const generateFakeField = ({
|
||||
type: property.type,
|
||||
label: camelToTitleCase(property.name),
|
||||
value: value || generateFakeValue(property.type, 'FieldMetadataType'),
|
||||
fieldMetadataId,
|
||||
isCompositeSubField: true,
|
||||
};
|
||||
|
||||
return acc;
|
||||
|
||||
@ -16,10 +16,12 @@ export type StepFilter = {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
stepOutputKey: string;
|
||||
operand: ViewFilterOperand;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
stepFilterGroupId: string;
|
||||
stepOutputKey: string;
|
||||
positionInStepFilterGroup?: number;
|
||||
fieldMetadataId?: string;
|
||||
compositeFieldSubFieldName?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user