Tt filter step input per variable type (#13371)
- add fieldMetadataId to step output schema - use it to display FormFieldInput in Filter input - few fixes for a few fields Next step: - Handle composite fields - Design review
This commit is contained in:
@ -47,6 +47,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { JsonValue } from 'type-fest';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
@ -70,7 +71,7 @@ export const FormFieldInput = ({
|
||||
error,
|
||||
onError,
|
||||
}: FormFieldInputProps) => {
|
||||
return isFieldNumber(field) ? (
|
||||
return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? (
|
||||
<FormNumberFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | number | undefined}
|
||||
|
||||
@ -48,7 +48,7 @@ export const FormBooleanFieldInput = ({
|
||||
}
|
||||
: {
|
||||
type: 'static',
|
||||
value: defaultValue ?? false,
|
||||
value: Boolean(defaultValue),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ChangeEvent, KeyboardEvent, useId, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -106,9 +107,10 @@ export const FormDateTimeFieldInput = ({
|
||||
},
|
||||
);
|
||||
|
||||
const draftValueAsDate = isDefined(draftValue.value)
|
||||
? new Date(draftValue.value)
|
||||
: null;
|
||||
const draftValueAsDate =
|
||||
isDefined(draftValue.value) && isNonEmptyString(draftValue.value)
|
||||
? new Date(draftValue.value)
|
||||
: null;
|
||||
|
||||
const [pickerDate, setPickerDate] =
|
||||
useState<Nullable<Date>>(draftValueAsDate);
|
||||
|
||||
@ -10,7 +10,9 @@ import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/rootLevelStepFilterGroupComponentSelector';
|
||||
import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/isStepFilterGroupChildAStepFilterGroup';
|
||||
import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -27,6 +29,10 @@ const StyledChildContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDangerContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.danger};
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFilterBodyProps = {
|
||||
action: WorkflowFilterAction;
|
||||
actionOptions:
|
||||
@ -43,6 +49,8 @@ export const WorkflowEditActionFilterBody = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFilterBodyProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const rootStepFilterGroup = useRecoilComponentValueV2(
|
||||
rootLevelStepFilterGroupComponentSelector,
|
||||
);
|
||||
@ -69,6 +77,22 @@ export const WorkflowEditActionFilterBody = ({
|
||||
});
|
||||
};
|
||||
|
||||
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep(
|
||||
{},
|
||||
);
|
||||
|
||||
const noAvailableVariables = availableVariablesInWorkflowStep.length === 0;
|
||||
|
||||
if (noAvailableVariables) {
|
||||
return (
|
||||
<WorkflowStepBody>
|
||||
<StyledDangerContainer>
|
||||
{t`No Available Step Outputs`}
|
||||
</StyledDangerContainer>
|
||||
</WorkflowStepBody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepFilterContext.Provider
|
||||
value={{
|
||||
|
||||
@ -67,6 +67,7 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
stepOutputKey: variableName,
|
||||
displayValue: variableLabel ?? '',
|
||||
type: variableType ?? 'unknown',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -85,10 +86,11 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
|
||||
const isSelectedFieldNotFound = !isDefined(variableLabel);
|
||||
const label = isSelectedFieldNotFound ? t`No Field Selected` : variableLabel;
|
||||
const dropdownId = `step-filter-field-${stepFilter.id}`;
|
||||
|
||||
return (
|
||||
<WorkflowVariablesDropdown
|
||||
instanceId={`step-filter-field-${stepFilter.id}`}
|
||||
instanceId={dropdownId}
|
||||
onVariableSelect={handleChange}
|
||||
disabled={readonly}
|
||||
clickableComponent={
|
||||
|
||||
@ -1,15 +1,45 @@
|
||||
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||
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 { 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 { StepFilter } from 'twenty-shared/src/types';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType, StepFilter } from 'twenty-shared/src/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { JsonValue } from 'type-fest';
|
||||
|
||||
type WorkflowStepFilterValueInputProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
const isFilterableFieldMetadataType = (
|
||||
type: string,
|
||||
): type is FieldMetadataType => {
|
||||
return [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.BOOLEAN,
|
||||
FieldMetadataType.DATE_TIME,
|
||||
FieldMetadataType.DATE,
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.ARRAY,
|
||||
].includes(type as FieldMetadataType);
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterValueInput = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterValueInputProps) => {
|
||||
@ -17,15 +47,76 @@ 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)
|
||||
? value
|
||||
: Array.isArray(value) || isObject(value)
|
||||
? JSON.stringify(value)
|
||||
: String(value);
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
value,
|
||||
value: valueToUpsert,
|
||||
},
|
||||
});
|
||||
};
|
||||
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
|
||||
|
||||
const isDisabled = !stepFilter.operand;
|
||||
|
||||
const operandHasNoInput =
|
||||
(stepFilter && !configurableViewFilterOperands.has(stepFilter.operand)) ??
|
||||
true;
|
||||
|
||||
if (isDisabled || operandHasNoInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDefined(variableType) && isFilterableFieldMetadataType(variableType)) {
|
||||
const selectedFieldMetadataItem = isDefined(fieldMetadataId)
|
||||
? getFieldMetadataItemById(fieldMetadataId)
|
||||
: undefined;
|
||||
|
||||
const field = {
|
||||
type: variableType as FieldMetadataType,
|
||||
label: '',
|
||||
metadata: {
|
||||
fieldName: selectedFieldMetadataItem?.name ?? '',
|
||||
options: selectedFieldMetadataItem?.options ?? [],
|
||||
} as FieldMetadata,
|
||||
};
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
field={field}
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
placeholder={t`Enter value`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
|
||||
@ -38,7 +38,6 @@ export const FILTER_OPERANDS_MAP = {
|
||||
],
|
||||
DATE_TIME: [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
@ -48,7 +47,6 @@ export const FILTER_OPERANDS_MAP = {
|
||||
],
|
||||
DATE: [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
|
||||
@ -18,7 +18,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
|
||||
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 { isObject, isString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -56,7 +56,7 @@ export const WorkflowAdvancedFilterValueFormInput = ({
|
||||
useApplyObjectFilterDropdownFilterValue();
|
||||
|
||||
const handleChange = (newValue: JsonValue) => {
|
||||
if (typeof newValue === 'string') {
|
||||
if (isString(newValue)) {
|
||||
applyObjectFilterDropdownFilterValue(newValue);
|
||||
} else if (Array.isArray(newValue) || isObject(newValue)) {
|
||||
applyObjectFilterDropdownFilterValue(JSON.stringify(newValue));
|
||||
|
||||
@ -7,6 +7,7 @@ type Leaf = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
value: any;
|
||||
fieldMetadataId?: string;
|
||||
};
|
||||
|
||||
type Node = {
|
||||
@ -16,6 +17,7 @@ type Node = {
|
||||
label?: string;
|
||||
value: OutputSchema;
|
||||
description?: string;
|
||||
fieldMetadataId?: string;
|
||||
};
|
||||
|
||||
type Link = {
|
||||
@ -28,7 +30,11 @@ type Link = {
|
||||
export type BaseOutputSchema = Record<string, Leaf | Node>;
|
||||
|
||||
export type RecordOutputSchema = {
|
||||
object: { nameSingular: string; fieldIdName: string } & Leaf;
|
||||
object: {
|
||||
nameSingular: string;
|
||||
fieldIdName: string;
|
||||
objectMetadataId: string;
|
||||
} & Leaf;
|
||||
fields: BaseOutputSchema;
|
||||
_outputSchemaType: 'RECORD';
|
||||
};
|
||||
|
||||
@ -12,6 +12,7 @@ describe('filterOutputSchema', () => {
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {},
|
||||
};
|
||||
@ -35,6 +36,7 @@ describe('filterOutputSchema', () => {
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
@ -16,6 +16,7 @@ const mockStep = {
|
||||
label: 'Company',
|
||||
value: 'John',
|
||||
isLeaf: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {
|
||||
name: { label: 'Name', value: 'Twenty', isLeaf: true },
|
||||
|
||||
@ -16,6 +16,7 @@ const mockStep = {
|
||||
label: 'Company',
|
||||
value: 'John',
|
||||
isLeaf: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {
|
||||
name: { label: 'Name', value: 'Twenty', isLeaf: true },
|
||||
|
||||
@ -19,6 +19,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
label: 'Company',
|
||||
value: 'John',
|
||||
isLeaf: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {
|
||||
name: { label: 'Name', value: 'Twenty', isLeaf: true },
|
||||
@ -38,6 +39,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
label: 'Person',
|
||||
value: 'Jane',
|
||||
isLeaf: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {
|
||||
firstName: { label: 'First Name', value: 'Jane', isLeaf: true },
|
||||
@ -270,6 +272,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
isLeaf: true,
|
||||
fieldIdName: 'properties.after.id',
|
||||
nameSingular: 'company',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
},
|
||||
|
||||
@ -43,6 +43,17 @@ const getVariableType = (key: string, outputSchema: OutputSchema): string => {
|
||||
return outputSchema[key]?.type ?? 'unknown';
|
||||
};
|
||||
|
||||
const getFieldMetadataId = (
|
||||
key: string,
|
||||
outputSchema: OutputSchema,
|
||||
): string | undefined => {
|
||||
if (isRecordOutputSchema(outputSchema)) {
|
||||
return outputSchema.fields[key]?.fieldMetadataId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const searchCurrentStepOutputSchema = ({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
@ -120,6 +131,10 @@ const searchCurrentStepOutputSchema = ({
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
currentSubStep,
|
||||
),
|
||||
fieldMetadataId: getFieldMetadataId(
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
currentSubStep,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -160,7 +175,7 @@ export const searchVariableThroughOutputSchema = ({
|
||||
};
|
||||
}
|
||||
|
||||
const { variableLabel, variablePathLabel, variableType } =
|
||||
const { variableLabel, variablePathLabel, variableType, fieldMetadataId } =
|
||||
searchCurrentStepOutputSchema({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
@ -172,5 +187,6 @@ export const searchVariableThroughOutputSchema = ({
|
||||
variableLabel,
|
||||
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||
variableType,
|
||||
fieldMetadataId,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user