Handle relations for filters (#13654)
- Refacto variable dropdown so it can display both objects and fields - we do not filter on object name anymore to simplify the code - add relation handler in filters https://github.com/user-attachments/assets/e4f03f11-45cb-4d3f-b628-e996129dd996
This commit is contained in:
+2
-1
@@ -222,7 +222,8 @@ export const FormSingleRecordPicker = ({
|
||||
instanceId={variablesDropdownId}
|
||||
disabled={disabled}
|
||||
onVariableSelect={handleVariableTagInsert}
|
||||
objectNameSingularToSelect={objectNameSingular}
|
||||
shouldDisplayRecordObjects={true}
|
||||
shouldDisplayRecordFields={false}
|
||||
/>
|
||||
)}
|
||||
</FormFieldInputRowContainer>
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@ export type VariablePickerComponent = React.FC<{
|
||||
disabled?: boolean;
|
||||
multiline?: boolean;
|
||||
onVariableSelect: (variableName: string) => void;
|
||||
objectNameSingularToSelect?: string;
|
||||
shouldDisplayRecordObjects?: boolean;
|
||||
shouldDisplayRecordFields?: boolean;
|
||||
}>;
|
||||
|
||||
+9
-3
@@ -24,6 +24,8 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterFieldSelectProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
const shouldDisplayRecordFields = true;
|
||||
const shouldDisplayRecordObjects = true;
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
@@ -44,9 +46,10 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
|
||||
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
|
||||
|
||||
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep(
|
||||
{},
|
||||
);
|
||||
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
});
|
||||
|
||||
const noAvailableVariables = availableVariablesInWorkflowStep.length === 0;
|
||||
|
||||
@@ -157,6 +160,9 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
textAccent={isSelectedFieldNotFound ? 'placeholder' : 'default'}
|
||||
/>
|
||||
}
|
||||
shouldDisplayRecordFields={shouldDisplayRecordFields}
|
||||
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
|
||||
shouldEnableSelectRelationObject={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -43,6 +43,7 @@ const isFilterableFieldMetadataType = (
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
...COMPOSITE_FIELD_METADATA_TYPES,
|
||||
].includes(type as FieldMetadataType);
|
||||
};
|
||||
@@ -142,6 +143,9 @@ export const WorkflowStepFilterValueInput = ({
|
||||
metadata: {
|
||||
fieldName: selectedFieldMetadataItem?.name ?? '',
|
||||
options: selectedFieldMetadataItem?.options ?? [],
|
||||
relationObjectMetadataNameSingular:
|
||||
selectedFieldMetadataItem?.relation?.targetObjectMetadata?.nameSingular,
|
||||
relationType: selectedFieldMetadataItem?.relation?.type,
|
||||
} as FieldMetadata,
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -41,7 +41,8 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
|
||||
disabled,
|
||||
multiline,
|
||||
onVariableSelect,
|
||||
objectNameSingularToSelect,
|
||||
shouldDisplayRecordObjects = false,
|
||||
shouldDisplayRecordFields = true,
|
||||
}) => {
|
||||
return (
|
||||
<StyledSearchVariablesDropdownContainer
|
||||
@@ -52,7 +53,8 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
|
||||
instanceId={instanceId}
|
||||
onVariableSelect={onVariableSelect}
|
||||
disabled={disabled}
|
||||
objectNameSingularToSelect={objectNameSingularToSelect}
|
||||
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
|
||||
shouldDisplayRecordFields={shouldDisplayRecordFields}
|
||||
multiline={multiline}
|
||||
/>
|
||||
</StyledSearchVariablesDropdownContainer>
|
||||
|
||||
+12
-6
@@ -3,8 +3,8 @@ import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/S
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { WorkflowVariablesDropdownAllItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownAllItems';
|
||||
import { WorkflowVariablesDropdownFieldItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems';
|
||||
import { WorkflowVariablesDropdownObjectItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownObjectItems';
|
||||
import { WorkflowVariablesDropdownWorkflowStepItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownWorkflowStepItems';
|
||||
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/workflow-variables/constants/SearchVariablesDropdownId';
|
||||
|
||||
@@ -35,14 +35,18 @@ export const WorkflowVariablesDropdown = ({
|
||||
instanceId,
|
||||
onVariableSelect,
|
||||
disabled,
|
||||
objectNameSingularToSelect,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
shouldEnableSelectRelationObject,
|
||||
multiline,
|
||||
clickableComponent,
|
||||
}: {
|
||||
instanceId: string;
|
||||
onVariableSelect: (variableName: string) => void;
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
shouldEnableSelectRelationObject?: boolean;
|
||||
disabled?: boolean;
|
||||
objectNameSingularToSelect?: string;
|
||||
multiline?: boolean;
|
||||
clickableComponent?: React.ReactNode;
|
||||
}) => {
|
||||
@@ -55,7 +59,8 @@ export const WorkflowVariablesDropdown = ({
|
||||
);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
|
||||
objectNameSingularToSelect,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
});
|
||||
|
||||
const noAvailableVariables = availableVariablesInWorkflowStep.length === 0;
|
||||
@@ -120,11 +125,12 @@ export const WorkflowVariablesDropdown = ({
|
||||
steps={availableVariablesInWorkflowStep}
|
||||
onSelect={handleStepSelect}
|
||||
/>
|
||||
) : isDefined(objectNameSingularToSelect) ? (
|
||||
<WorkflowVariablesDropdownObjectItems
|
||||
) : shouldDisplayRecordObjects ? (
|
||||
<WorkflowVariablesDropdownAllItems
|
||||
step={selectedStep}
|
||||
onSelect={handleSubItemSelect}
|
||||
onBack={handleBack}
|
||||
shouldEnableSelectRelationObject={shouldEnableSelectRelationObject}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowVariablesDropdownFieldItems
|
||||
|
||||
+37
-17
@@ -3,14 +3,15 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
|
||||
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
|
||||
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
|
||||
import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath';
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
OverflowingTextWithTooltip,
|
||||
@@ -19,25 +20,28 @@ import {
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { useVariableDropdown } from '../hooks/useVariableDropdown';
|
||||
|
||||
type WorkflowVariablesDropdownObjectItemsProps = {
|
||||
type WorkflowVariablesDropdownAllItemsProps = {
|
||||
step: StepOutputSchema;
|
||||
onSelect: (value: string) => void;
|
||||
onBack: () => void;
|
||||
shouldEnableSelectRelationObject?: boolean;
|
||||
};
|
||||
|
||||
export const WorkflowVariablesDropdownObjectItems = ({
|
||||
export const WorkflowVariablesDropdownAllItems = ({
|
||||
step,
|
||||
onSelect,
|
||||
onBack,
|
||||
}: WorkflowVariablesDropdownObjectItemsProps) => {
|
||||
shouldEnableSelectRelationObject,
|
||||
}: WorkflowVariablesDropdownAllItemsProps) => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const {
|
||||
currentPath,
|
||||
filteredOptions,
|
||||
searchInputValue,
|
||||
setSearchInputValue,
|
||||
handleSelectField,
|
||||
goBack,
|
||||
filteredOptions,
|
||||
currentPath,
|
||||
} = useVariableDropdown({
|
||||
step,
|
||||
onSelect,
|
||||
@@ -61,9 +65,25 @@ export const WorkflowVariablesDropdownObjectItems = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(
|
||||
`{{${step.id}.${[...currentPath, currentSubStep.object.fieldIdName].join('.')}}}`,
|
||||
);
|
||||
const isRelationField = currentSubStep.object.isRelationField ?? false;
|
||||
const isRelationObjectSelectable =
|
||||
shouldEnableSelectRelationObject ?? false;
|
||||
|
||||
if (isRelationField && isRelationObjectSelectable) {
|
||||
onSelect(
|
||||
getVariableTemplateFromPath({
|
||||
stepId: step.id,
|
||||
path: currentPath,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
onSelect(
|
||||
getVariableTemplateFromPath({
|
||||
stepId: step.id,
|
||||
path: [...currentPath, currentSubStep.object.fieldIdName],
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const displayedSubStepObject = getDisplayedSubStepObject();
|
||||
@@ -118,17 +138,17 @@ export const WorkflowVariablesDropdownObjectItems = ({
|
||||
{filteredOptions.length > 0 && shouldDisplayObject && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{filteredOptions.map(([key, option]) => (
|
||||
{filteredOptions.map(([key, subStep]) => (
|
||||
<MenuItemSelect
|
||||
key={key}
|
||||
selected={false}
|
||||
focused={false}
|
||||
onClick={() => handleSelectField(key)}
|
||||
text={option.label || key}
|
||||
hasSubMenu={!option.isLeaf}
|
||||
LeftIcon={option.icon ? getIcon(option.icon) : undefined}
|
||||
text={subStep.label || key}
|
||||
hasSubMenu={!subStep.isLeaf}
|
||||
LeftIcon={subStep.icon ? getIcon(subStep.icon) : undefined}
|
||||
contextualText={
|
||||
option.isLeaf ? option?.value?.toString() : undefined
|
||||
subStep.isLeaf ? subStep?.value?.toString() : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
-1
@@ -50,7 +50,6 @@ export const WorkflowVariablesDropdownFieldItems = ({
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
style={{ position: 'fixed' }}
|
||||
>
|
||||
<OverflowingTextWithTooltip
|
||||
text={getStepHeaderLabel(step, currentPath)}
|
||||
|
||||
+9
-6
@@ -13,9 +13,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
export const useAvailableVariablesInWorkflowStep = ({
|
||||
objectNameSingularToSelect,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
}: {
|
||||
objectNameSingularToSelect?: string;
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
}): StepOutputSchema[] => {
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
const flow = useFlowOrThrow();
|
||||
@@ -35,10 +37,11 @@ export const useAvailableVariablesInWorkflowStep = ({
|
||||
|
||||
const availableVariablesInWorkflowStep = availableStepsOutputSchema
|
||||
.map((stepOutputSchema) => {
|
||||
const outputSchema = filterOutputSchema(
|
||||
stepOutputSchema.outputSchema,
|
||||
objectNameSingularToSelect,
|
||||
) as OutputSchema;
|
||||
const outputSchema = filterOutputSchema({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
outputSchema: stepOutputSchema.outputSchema,
|
||||
}) as OutputSchema;
|
||||
|
||||
if (!isDefined(outputSchema) || isEmptyObject(outputSchema)) {
|
||||
return undefined;
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -70,7 +71,12 @@ export const useVariableDropdown = ({
|
||||
setCurrentPath([...currentPath, key]);
|
||||
setSearchInputValue('');
|
||||
} else {
|
||||
onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`);
|
||||
onSelect(
|
||||
getVariableTemplateFromPath({
|
||||
stepId: step.id,
|
||||
path: [...currentPath, key],
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ export type RecordOutputSchema = {
|
||||
nameSingular: string;
|
||||
fieldIdName: string;
|
||||
objectMetadataId: string;
|
||||
isRelationField?: boolean;
|
||||
} & Leaf;
|
||||
fields: BaseOutputSchema;
|
||||
_outputSchemaType: 'RECORD';
|
||||
|
||||
+190
-153
@@ -3,189 +3,226 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { filterOutputSchema } from '../filterOutputSchema';
|
||||
|
||||
describe('filterOutputSchema', () => {
|
||||
describe('edge cases', () => {
|
||||
it('should return the input schema when objectNameSingularToSelect is undefined', () => {
|
||||
const inputSchema: OutputSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: {
|
||||
nameSingular: 'person',
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields: {},
|
||||
};
|
||||
const createRecordSchema = (
|
||||
nameSingular: string,
|
||||
fields = {},
|
||||
): OutputSchema => ({
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: {
|
||||
nameSingular,
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, undefined)).toBe(inputSchema);
|
||||
const createBaseSchema = (fields = {}): OutputSchema => ({
|
||||
...fields,
|
||||
});
|
||||
|
||||
describe('shouldDisplayRecordFields only (true, false)', () => {
|
||||
describe('record schema', () => {
|
||||
it('should return the input schema unchanged', () => {
|
||||
const inputSchema = createRecordSchema('person', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
id: { isLeaf: true, type: FieldMetadataType.UUID },
|
||||
});
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: false,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBe(inputSchema);
|
||||
});
|
||||
|
||||
it('should return undefined when input schema is undefined', () => {
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: false,
|
||||
outputSchema: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when input schema is undefined', () => {
|
||||
expect(filterOutputSchema(undefined, 'person')).toBeUndefined();
|
||||
describe('base schema', () => {
|
||||
it('should return the input schema unchanged', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: { isLeaf: true, value: 'string' },
|
||||
field2: { isLeaf: true, type: FieldMetadataType.NUMBER },
|
||||
});
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: false,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBe(inputSchema);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('record output schema', () => {
|
||||
const createRecordSchema = (
|
||||
nameSingular: string,
|
||||
fields = {},
|
||||
): OutputSchema => ({
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: {
|
||||
nameSingular,
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
it('should keep a matching record schema', () => {
|
||||
const inputSchema = createRecordSchema('person');
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(inputSchema);
|
||||
});
|
||||
|
||||
it('should filter out a non-matching record schema with no valid fields', () => {
|
||||
const inputSchema = createRecordSchema('company');
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep valid nested records while filtering out invalid ones', () => {
|
||||
const inputSchema = createRecordSchema('company', {
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person', {
|
||||
manager: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
}),
|
||||
},
|
||||
department: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('department'),
|
||||
},
|
||||
});
|
||||
|
||||
const expectedSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
describe('shouldDisplayRecordObjects only (false, true)', () => {
|
||||
describe('record schema', () => {
|
||||
it('should keep record schema with object and filter compatible fields', () => {
|
||||
const inputSchema = createRecordSchema('person', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
id: { isLeaf: true, type: FieldMetadataType.UUID },
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person', {
|
||||
manager: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
}),
|
||||
value: createRecordSchema('employee'),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema);
|
||||
});
|
||||
|
||||
it('should ignore leaf fields that are field metadata types', () => {
|
||||
const inputSchema = createRecordSchema('company', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
id: { isLeaf: true, type: FieldMetadataType.UUID },
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
});
|
||||
|
||||
const expectedSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
const expectedSchema = createRecordSchema('person', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('employee'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toEqual(expectedSchema);
|
||||
});
|
||||
|
||||
it('should return undefined for record schema without object and no valid fields', () => {
|
||||
const inputSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
invalidField: { isLeaf: true, type: FieldMetadataType.NUMBER },
|
||||
},
|
||||
} as any;
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('base schema', () => {
|
||||
it('should keep base schema with valid nested records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
},
|
||||
};
|
||||
field2: { isLeaf: true, type: FieldMetadataType.NUMBER },
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema);
|
||||
const expectedSchema = {
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toEqual(expectedSchema);
|
||||
});
|
||||
|
||||
it('should return undefined for base schema with no valid records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: { isLeaf: true, type: FieldMetadataType.NUMBER },
|
||||
field2: { isLeaf: true, type: FieldMetadataType.BOOLEAN },
|
||||
});
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('base output schema', () => {
|
||||
const createBaseSchema = (fields = {}): OutputSchema => ({
|
||||
...fields,
|
||||
describe('both shouldDisplayRecordFields and shouldDisplayRecordObjects (true, true)', () => {
|
||||
it('should return the input schema unchanged for record schema', () => {
|
||||
const inputSchema = createRecordSchema('person', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
id: { isLeaf: true, type: FieldMetadataType.UUID },
|
||||
});
|
||||
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBe(inputSchema);
|
||||
});
|
||||
|
||||
it('should filter out base schema with no valid records', () => {
|
||||
it('should return the input schema unchanged for base schema', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
value: 'string',
|
||||
},
|
||||
field1: { isLeaf: true, value: 'string' },
|
||||
field2: { isLeaf: true, type: FieldMetadataType.NUMBER },
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined();
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBe(inputSchema);
|
||||
});
|
||||
|
||||
it('should keep base schema with valid nested records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
it('should return undefined when input schema is undefined', () => {
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: true,
|
||||
shouldDisplayRecordObjects: true,
|
||||
outputSchema: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('both shouldDisplayRecordFields and shouldDisplayRecordObjects false (false, false)', () => {
|
||||
it('should return the input schema unchanged', () => {
|
||||
const inputSchema = createRecordSchema('person', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual({
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: false,
|
||||
outputSchema: inputSchema,
|
||||
}),
|
||||
).toBe(inputSchema);
|
||||
});
|
||||
|
||||
it('should handle deeply nested valid records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
level1: {
|
||||
isLeaf: false,
|
||||
value: createBaseSchema({
|
||||
level2: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual({
|
||||
level1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
level2: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
it('should return undefined when input schema is undefined', () => {
|
||||
expect(
|
||||
filterOutputSchema({
|
||||
shouldDisplayRecordFields: false,
|
||||
shouldDisplayRecordObjects: false,
|
||||
outputSchema: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath';
|
||||
|
||||
describe('getVariableTemplateFromPath', () => {
|
||||
it('should return stepId template when path is empty', () => {
|
||||
const result = getVariableTemplateFromPath({
|
||||
stepId: 'step-1',
|
||||
path: [],
|
||||
});
|
||||
|
||||
expect(result).toBe('{{step-1}}');
|
||||
});
|
||||
|
||||
it('should return stepId with path', () => {
|
||||
const result = getVariableTemplateFromPath({
|
||||
stepId: 'step-2',
|
||||
path: ['company', 'name'],
|
||||
});
|
||||
|
||||
expect(result).toBe('{{step-2.company.name}}');
|
||||
});
|
||||
});
|
||||
+71
-33
@@ -9,24 +9,31 @@ import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOu
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const isValidRecordOutputSchema = (
|
||||
outputSchema: RecordOutputSchema,
|
||||
objectNameSingularToSelect?: string,
|
||||
): boolean => {
|
||||
if (isDefined(objectNameSingularToSelect)) {
|
||||
return (
|
||||
isDefined(outputSchema.object) &&
|
||||
outputSchema.object.nameSingular === objectNameSingularToSelect
|
||||
);
|
||||
const isValidRecordOutputSchema = ({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
outputSchema,
|
||||
}: {
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
outputSchema: RecordOutputSchema;
|
||||
}): boolean => {
|
||||
if (shouldDisplayRecordObjects && !shouldDisplayRecordFields) {
|
||||
return isDefined(outputSchema.object);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterRecordOutputSchema = (
|
||||
outputSchema: RecordOutputSchema,
|
||||
objectNameSingularToSelect: string,
|
||||
): RecordOutputSchema | undefined => {
|
||||
const filterRecordOutputSchema = ({
|
||||
outputSchema,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
}: {
|
||||
outputSchema: RecordOutputSchema;
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
}): RecordOutputSchema | undefined => {
|
||||
const filteredFields: BaseOutputSchema = {};
|
||||
let hasValidFields = false;
|
||||
|
||||
@@ -41,10 +48,12 @@ const filterRecordOutputSchema = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const validSubSchema = filterOutputSchema(
|
||||
field.value,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
const validSubSchema = filterOutputSchema({
|
||||
outputSchema: field.value,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
});
|
||||
|
||||
if (isDefined(validSubSchema)) {
|
||||
filteredFields[key] = {
|
||||
...field,
|
||||
@@ -54,7 +63,13 @@ const filterRecordOutputSchema = (
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidRecordOutputSchema(outputSchema, objectNameSingularToSelect)) {
|
||||
if (
|
||||
isValidRecordOutputSchema({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
outputSchema,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
...outputSchema,
|
||||
fields: filteredFields,
|
||||
@@ -69,10 +84,15 @@ const filterRecordOutputSchema = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const filterBaseOutputSchema = (
|
||||
outputSchema: BaseOutputSchema,
|
||||
objectNameSingularToSelect: string,
|
||||
): BaseOutputSchema | undefined => {
|
||||
const filterBaseOutputSchema = ({
|
||||
outputSchema,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
}: {
|
||||
outputSchema: BaseOutputSchema;
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
}): BaseOutputSchema | undefined => {
|
||||
const filteredSchema: BaseOutputSchema = {};
|
||||
let hasValidFields = false;
|
||||
|
||||
@@ -87,10 +107,11 @@ const filterBaseOutputSchema = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const validSubSchema = filterOutputSchema(
|
||||
field.value,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
const validSubSchema = filterOutputSchema({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
outputSchema: field.value,
|
||||
});
|
||||
if (isDefined(validSubSchema)) {
|
||||
filteredSchema[key] = {
|
||||
...field,
|
||||
@@ -107,20 +128,37 @@ const filterBaseOutputSchema = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const filterOutputSchema = (
|
||||
outputSchema?: OutputSchema,
|
||||
objectNameSingularToSelect?: string,
|
||||
): OutputSchema | undefined => {
|
||||
if (!objectNameSingularToSelect || !outputSchema) {
|
||||
export const filterOutputSchema = ({
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
outputSchema,
|
||||
}: {
|
||||
shouldDisplayRecordFields: boolean;
|
||||
shouldDisplayRecordObjects: boolean;
|
||||
outputSchema?: OutputSchema;
|
||||
}): OutputSchema | undefined => {
|
||||
if (
|
||||
!shouldDisplayRecordObjects ||
|
||||
shouldDisplayRecordFields ||
|
||||
!outputSchema
|
||||
) {
|
||||
return outputSchema;
|
||||
}
|
||||
|
||||
if (isLinkOutputSchema(outputSchema)) {
|
||||
return outputSchema;
|
||||
} else if (isRecordOutputSchema(outputSchema)) {
|
||||
return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect);
|
||||
return filterRecordOutputSchema({
|
||||
outputSchema,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
});
|
||||
} else if (isBaseOutputSchema(outputSchema)) {
|
||||
return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect);
|
||||
return filterBaseOutputSchema({
|
||||
outputSchema,
|
||||
shouldDisplayRecordFields,
|
||||
shouldDisplayRecordObjects,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export const getVariableTemplateFromPath = ({
|
||||
stepId,
|
||||
path,
|
||||
}: {
|
||||
stepId: string;
|
||||
path: string[];
|
||||
}) => {
|
||||
if (path.length === 0) {
|
||||
return `{{${stepId}}}`;
|
||||
}
|
||||
|
||||
return `{{${stepId}.${path.join('.')}}}`;
|
||||
};
|
||||
+1
@@ -40,6 +40,7 @@ export type RecordOutputSchema = {
|
||||
nameSingular: string;
|
||||
fieldIdName: string;
|
||||
objectMetadataId: string;
|
||||
isRelationField?: boolean;
|
||||
} & Leaf;
|
||||
fields: Record<string, FieldOutputSchema>;
|
||||
_outputSchemaType: 'RECORD';
|
||||
|
||||
+73
-81
@@ -56,92 +56,84 @@ describe('generateFakeFormResponse', () => {
|
||||
objectMetadataMaps: mockObjectMetadataMaps,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"age": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Age",
|
||||
"type": "NUMBER",
|
||||
"value": 20,
|
||||
},
|
||||
"company": {
|
||||
"isLeaf": false,
|
||||
"label": "Company",
|
||||
"value": {
|
||||
"_outputSchemaType": "RECORD",
|
||||
"fields": {
|
||||
"domainName": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"icon": "test-field-icon",
|
||||
"isLeaf": false,
|
||||
"label": "Domain Name",
|
||||
"type": "LINKS",
|
||||
"value": {
|
||||
"primaryLinkLabel": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Primary Link Label",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
expect(result).toEqual({
|
||||
age: {
|
||||
isLeaf: true,
|
||||
label: 'Age',
|
||||
type: 'NUMBER',
|
||||
value: 20,
|
||||
},
|
||||
company: {
|
||||
isLeaf: false,
|
||||
label: 'Company',
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
domainName: {
|
||||
fieldMetadataId: 'domainNameFieldMetadataId',
|
||||
icon: 'test-field-icon',
|
||||
isLeaf: false,
|
||||
label: 'Domain Name',
|
||||
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',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"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",
|
||||
"value": null,
|
||||
name: {
|
||||
fieldMetadataId: 'nameFieldMetadataId',
|
||||
icon: 'test-field-icon',
|
||||
isLeaf: true,
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
"fieldMetadataId": "nameFieldMetadataId",
|
||||
"icon": "test-field-icon",
|
||||
"isLeaf": true,
|
||||
"label": "Name",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
object: {
|
||||
fieldIdName: 'id',
|
||||
icon: 'test-company-icon',
|
||||
isLeaf: true,
|
||||
label: 'Company',
|
||||
nameSingular: 'company',
|
||||
objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c',
|
||||
value: 'A company',
|
||||
},
|
||||
},
|
||||
},
|
||||
"object": {
|
||||
"fieldIdName": "id",
|
||||
"icon": "test-company-icon",
|
||||
"isLeaf": true,
|
||||
"label": "Company",
|
||||
"nameSingular": "company",
|
||||
"objectMetadataId": "20202020-c03c-45d6-a4b0-04afe1357c5c",
|
||||
"value": "A company",
|
||||
date: {
|
||||
isLeaf: true,
|
||||
label: 'Date',
|
||||
type: 'DATE',
|
||||
value: 'mm/dd/yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
"date": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Date",
|
||||
"type": "DATE",
|
||||
"value": "mm/dd/yyyy",
|
||||
},
|
||||
"name": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Name",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
},
|
||||
}
|
||||
`);
|
||||
name: {
|
||||
isLeaf: true,
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
@@ -5,9 +5,11 @@ import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builde
|
||||
export const generateFakeObjectRecord = ({
|
||||
objectMetadataInfo,
|
||||
depth = 0,
|
||||
isRelationField,
|
||||
}: {
|
||||
objectMetadataInfo: ObjectMetadataInfo;
|
||||
depth?: number;
|
||||
isRelationField?: boolean;
|
||||
}): RecordOutputSchema => {
|
||||
return {
|
||||
object: {
|
||||
@@ -20,6 +22,7 @@ export const generateFakeObjectRecord = ({
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
fieldIdName: 'id',
|
||||
objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id,
|
||||
isRelationField,
|
||||
},
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo,
|
||||
|
||||
+2
@@ -52,6 +52,7 @@ export const generateObjectRecordFields = ({
|
||||
isLeaf: false,
|
||||
icon: field.icon ?? undefined,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
fieldMetadataId: field.id,
|
||||
value: generateFakeObjectRecord({
|
||||
objectMetadataInfo: {
|
||||
@@ -59,6 +60,7 @@ export const generateObjectRecordFields = ({
|
||||
objectMetadataMaps: objectMetadataInfo.objectMetadataMaps,
|
||||
},
|
||||
depth: depth + 1,
|
||||
isRelationField: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+217
@@ -95,6 +95,223 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
// Enhanced relation filter tests with object id extraction
|
||||
it('should extract id from left operand object for relation comparison', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const leftObject = { id: uuid1, name: 'John Doe' };
|
||||
const rightValue = uuid1;
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
leftObject,
|
||||
rightValue,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should extract id from right operand object for relation comparison', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const leftValue = uuid1;
|
||||
const rightObject = { id: uuid1, name: 'John Doe' };
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
leftValue,
|
||||
rightObject,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should extract id from both operands when they are objects for relation comparison', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const leftObject = { id: uuid1, name: 'John Doe' };
|
||||
const rightObject = { id: uuid1, title: 'Admin' };
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
leftObject,
|
||||
rightObject,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when extracted ids do not match for relation comparison', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
const leftObject = { id: uuid1, name: 'John Doe' };
|
||||
const rightObject = { id: uuid2, name: 'Jane Smith' };
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
leftObject,
|
||||
rightObject,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle IsNot with object id extraction for relation comparison', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
const leftObject = { id: uuid1, name: 'John Doe' };
|
||||
const rightObject = { id: uuid2, name: 'Jane Smith' };
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
leftObject,
|
||||
rightObject,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle objects without id property for relation comparison', () => {
|
||||
const leftObject = { name: 'John Doe' };
|
||||
const rightObject = { name: 'John Doe' };
|
||||
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
leftObject,
|
||||
rightObject,
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false); // Objects are different references
|
||||
});
|
||||
|
||||
it('should throw error for unsupported relation filter operand', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
uuid1,
|
||||
uuid2,
|
||||
'RELATION',
|
||||
);
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||
'Operand contains not supported for relation filter',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UUID filter operands', () => {
|
||||
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
|
||||
it('should return true when UUIDs are equal (Is)', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid1, 'UUID');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when UUIDs are not equal (Is)', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid2, 'UUID');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when UUIDs are equal (IsNot)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
uuid1,
|
||||
uuid1,
|
||||
'UUID',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when UUIDs are not equal (IsNot)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
uuid1,
|
||||
uuid2,
|
||||
'UUID',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle null/undefined UUIDs with Is operand', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.Is, null, null, 'UUID');
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
undefined,
|
||||
undefined,
|
||||
'UUID',
|
||||
);
|
||||
const filter3 = createFilter(ViewFilterOperand.Is, uuid1, null, 'UUID');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null/undefined UUIDs with IsNot operand', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
null,
|
||||
null,
|
||||
'UUID',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
undefined,
|
||||
undefined,
|
||||
'UUID',
|
||||
);
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
uuid1,
|
||||
null,
|
||||
'UUID',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty string UUIDs', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.Is, '', '', 'UUID');
|
||||
const filter2 = createFilter(ViewFilterOperand.Is, uuid1, '', 'UUID');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw error for unsupported UUID filter operand', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
uuid1,
|
||||
uuid2,
|
||||
'UUID',
|
||||
);
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||
'Operand contains not supported for uuid filter',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Boolean filter operands', () => {
|
||||
|
||||
+27
-2
@@ -1,4 +1,4 @@
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { isObject, isString } from '@sniptt/guards';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
@@ -32,6 +32,7 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||
case 'BOOLEAN':
|
||||
return evaluateBooleanFilter(filter);
|
||||
case 'UUID':
|
||||
return evaluateUuidFilter(filter);
|
||||
case 'RELATION':
|
||||
return evaluateRelationFilter(filter);
|
||||
case 'CURRENCY':
|
||||
@@ -193,12 +194,36 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateRelationFilter(filter: ResolvedFilter): boolean {
|
||||
function evaluateUuidFilter(filter: ResolvedFilter): boolean {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return filter.leftOperand === filter.rightOperand;
|
||||
case ViewFilterOperand.IsNot:
|
||||
return filter.leftOperand !== filter.rightOperand;
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for uuid filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateRelationFilter(filter: ResolvedFilter): boolean {
|
||||
// compare only the ids. If the left operand is the relation object, get the id
|
||||
const leftValue =
|
||||
isObject(filter.leftOperand) && 'id' in filter.leftOperand
|
||||
? filter.leftOperand.id
|
||||
: filter.leftOperand;
|
||||
|
||||
const rightValue =
|
||||
isObject(filter.rightOperand) && 'id' in filter.rightOperand
|
||||
? filter.rightOperand.id
|
||||
: filter.rightOperand;
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return leftValue === rightValue;
|
||||
case ViewFilterOperand.IsNot:
|
||||
return leftValue !== rightValue;
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for relation filter`,
|
||||
|
||||
Reference in New Issue
Block a user