feat(twenty-front): relation traversal in filter dropdown (stacked) (#20533)

**Stacked on #20527** 




https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd





## Summary

Surfaces the one-hop relation traversal added in #20527 through the
existing **composite sub-field dropdown pattern**. Clicking a
MANY_TO_ONE relation field in the "+ Filter" picker now opens the same
second-level dropdown that composite fields (FULL_NAME, ADDRESS,
CURRENCY, etc.) already use — populated with the target object's
filterable fields. Picking one (e.g. `Company → Name`) builds a filter
that serializes to the nested GraphQL filter the backend now accepts: `{
company: { name: { ilike: "%X%" } } }`.

No new components. The whole feature reuses
`AdvancedFilterSubFieldSelectMenu` + the existing
`subFieldNameUsedInDropdownComponentState` + the existing `MenuItem
hasSubMenu` indicator. Only the conditions that gate the sub-menu (and
the sub-menu's content for relations) were broadened.

## What landed

| File | Change |
|---|---|
| `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now
shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). |
| `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu
alongside composite clicks. |
| `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu
type is `'RELATION'`, render the target object's filterable fields via
`useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite
logic untouched. |
| `objectFilterDropdownSubMenuFieldType` state | Widened to accept a
`'RELATION'` sentinel. Role-permissions sub-field menu narrows it back
out (it doesn't traverse relations). |
| `useSelectFieldUsedInAdvancedFilterDropdown` | New optional
`targetFieldMetadataItem` arg. When present, the stored RecordFilter's
`type` is the target field's type so the operand picker and value input
render the target's operands (`'TEXT'` operators when filtering
`company.name`, etc.). |
| `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter
targets a `RELATION` field with a `subFieldName`, synthesize a
field-metadata for the target, recurse to build the inner filter, then
wrap it under the relation field's name → `{ relationName: {
targetFieldName: { ...operator } } }`. |

`RecordFilter.subFieldName` stays narrowly typed as
`CompositeFieldSubFieldName` so the wide downstream consumers
(`shouldShowFilterTextInput`, composite handlers in the serializer,
etc.) don't change. The relation target field's name is stored through a
narrowly-scoped cast at the dropdown's storage point — the serializer
checks `filter.type === 'RELATION'` before interpreting it as a target
field name, so the cast can't be mis-read by composite-only code paths.

## Test plan

- [ ] Open a table view on People, click "+ Filter", click "Company" →
sub-menu opens with Company's filterable fields
- [ ] Pick "Name" → operand picker shows TEXT operators (Contains,
Equals, …)
- [ ] Type "Airbnb" → filter applies, table shows people whose company
name contains "Airbnb"
- [ ] Verify network tab: the GraphQL filter variable is `{ company: {
name: { ilike: "%Airbnb%" } } }`
- [ ] Same flow with a composite target field (e.g. `Company →
annualRecurringRevenue → amountMicros`) — should work end-to-end
(backend supports composite-within-relation; #20527 has an integration
test covering this)
- [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal
sub-menu and filter correctly — no regression
- [ ] Role-permissions field-select sub-field menu is unaffected (it
bails out early on the RELATION sentinel)

## Out of scope

- ONE_TO_MANY traversal (no backend support yet)
- Aggregates (`people.count > 5`)
- Persisting relation-traversal filters into a saved view (ViewFilter
has no `relationPath` column yet; that's a separate slice)
- REST API DSL changes
- AI Tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-05-15 16:42:47 +02:00
committed by GitHub
parent eca92ca559
commit c938fbf4d6
88 changed files with 1809 additions and 414 deletions
@@ -919,4 +919,92 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
expect(result).toHaveProperty('recordId.in');
});
});
describe('relation traversal', () => {
// The dispatcher should wrap the inner filter under the relation source
// field's GraphQL key and build it against the target field's type
// (not the relation FK's type).
it('should nest filter under source field name when target field is set', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: {
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
relationTargetFieldMetadataId: 'f-text',
} as RecordFilter,
fieldMetadataItems: fields,
});
expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } });
});
// If the target field is no longer in fieldMetadataItems (e.g. it was
// deleted from the workspace), dropping the filter is the safe path —
// the alternative would silently interpret the text value as a UUID
// list against the relation FK.
it('should return undefined when target field is not found', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: {
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
relationTargetFieldMetadataId: 'nonexistent-target',
} as RecordFilter,
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
});
// Emptiness operands must check the target column on the related
// object rather than the FK column on the source record.
it('should apply IS_EMPTY against the target field, not the relation FK', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: {
...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''),
relationTargetFieldMetadataId: 'f-text',
} as RecordFilter,
fieldMetadataItems: fields,
});
// Without traversal, the RELATION case would have produced a filter
// on `companyId`; here we expect the emptiness check to fall on
// `company.name` instead.
expect(result).toEqual({
company: { or: [{ name: { ilike: '' } }, { name: { is: 'NULL' } }] },
});
});
// The target field's per-type switch must run, so number/select/etc.
// targets behave like a direct filter on that field — verified here
// with a SELECT target.
it('should dispatch to target type switch (SELECT target)', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: {
...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'),
relationTargetFieldMetadataId: 'f-select',
} as RecordFilter,
fieldMetadataItems: fields,
});
expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } });
});
// Without a relationTargetFieldMetadataId the dispatcher must fall
// through to the direct builder — preserving the filter-by-record-id
// behaviour on the relation FK.
it('should keep relation filter-by-id behaviour when no target field is set', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-relation',
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('companyId.in');
});
});
});
@@ -19,6 +19,7 @@ export type RecordFilter = {
recordFilterGroupId?: string | null;
operand: ViewFilterOperand;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
relationTargetFieldMetadataId?: string | null | undefined;
};
export type RecordFilterGroup = {
@@ -75,11 +75,11 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}: TurnRecordFilterIntoRecordGqlOperationFilterParams):
| RecordGqlOperationFilter
| undefined => {
const correspondingFieldMetadataItem = fieldMetadataItems.find(
const sourceFieldMetadataItem = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
if (!isDefined(correspondingFieldMetadataItem)) {
if (!isDefined(sourceFieldMetadataItem)) {
return;
}
@@ -87,15 +87,66 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return;
}
if (
sourceFieldMetadataItem.type === FieldMetadataType.RELATION &&
isDefined(recordFilter.relationTargetFieldMetadataId)
) {
const targetFieldMetadataItem = fieldMetadataItems.find(
(field) => field.id === recordFilter.relationTargetFieldMetadataId,
);
if (!isDefined(targetFieldMetadataItem)) {
return;
}
const innerFilter = buildDirectFieldGqlOperationFilter({
recordFilter: {
...recordFilter,
fieldMetadataId: targetFieldMetadataItem.id,
relationTargetFieldMetadataId: null,
},
fieldMetadataItem: targetFieldMetadataItem,
filterValueDependencies,
});
if (!isDefined(innerFilter)) {
return;
}
return {
[sourceFieldMetadataItem.name]: innerFilter,
} as RecordGqlOperationFilter;
}
return buildDirectFieldGqlOperationFilter({
recordFilter,
fieldMetadataItem: sourceFieldMetadataItem,
filterValueDependencies,
});
};
type BuildDirectFieldGqlOperationFilterParams = {
filterValueDependencies: RecordFilterValueDependencies;
recordFilter: Omit<RecordFilter, 'id'>;
fieldMetadataItem: FieldShared;
};
const buildDirectFieldGqlOperationFilter = ({
recordFilter,
fieldMetadataItem,
filterValueDependencies,
}: BuildDirectFieldGqlOperationFilterParams):
| RecordGqlOperationFilter
| undefined => {
const shouldComputeEmptinessFilter = checkIfShouldComputeEmptinessFilter({
recordFilterOperand: recordFilter.operand,
correspondingFieldMetadataItem,
correspondingFieldMetadataItem: fieldMetadataItem,
});
if (shouldComputeEmptinessFilter) {
const emptinessFilter = getEmptyRecordGqlOperationFilter({
operand: recordFilter.operand,
correspondingField: correspondingFieldMetadataItem,
correspondingField: fieldMetadataItem,
recordFilter: recordFilter,
});
@@ -106,23 +157,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
const isSubFieldFilter = isNonEmptyString(subFieldName);
const filterType = getFilterTypeFromFieldType(
correspondingFieldMetadataItem.type,
);
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
switch (filterType) {
case 'TEXT':
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
ilike: `%${recordFilter.value}%`,
} as StringFilter,
};
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
ilike: `%${recordFilter.value}%`,
} as StringFilter,
},
@@ -137,7 +186,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.VECTOR_SEARCH:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
search: recordFilter.value,
} as TSVectorFilter,
};
@@ -150,14 +199,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
like: `%${recordFilter.value}%`,
} as RawJsonFilter,
};
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
like: `%${recordFilter.value}%`,
} as RawJsonFilter,
},
@@ -171,14 +220,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
like: `%${recordFilter.value}%`,
} as FilesFilter,
};
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
like: `%${recordFilter.value}%`,
} as FilesFilter,
},
@@ -215,12 +264,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: start,
} as DateFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: end,
} as DateFilter,
},
@@ -241,19 +290,19 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_TODAY: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: nowAsPlainDate,
} as DateFilter,
};
@@ -265,14 +314,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: plainDateFilter,
} as DateFilter,
};
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: plainDateFilter,
} as DateFilter,
};
@@ -280,7 +329,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
case RecordFilterOperand.IS: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: plainDateFilter,
} as DateFilter,
};
@@ -331,12 +380,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: start.toInstant().toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: end.toInstant().toString(),
} as DateTimeFilter,
},
@@ -357,13 +406,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
@@ -371,12 +420,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
@@ -413,12 +462,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: start.toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: end.toString(),
} as DateTimeFilter,
},
@@ -431,14 +480,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: resolvedDateTime.toString(),
} as DateTimeFilter,
};
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lt: resolvedDateTime.toString(),
} as DateTimeFilter,
};
@@ -454,13 +503,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: convertRatingToRatingValue(parseFloat(recordFilter.value)),
} as RatingFilter,
};
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
in: convertGreaterThanOrEqualRatingToArrayOfRatingValues(
parseFloat(recordFilter.value),
),
@@ -468,7 +517,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
in: convertLessThanOrEqualRatingToArrayOfRatingValues(
parseFloat(recordFilter.value),
),
@@ -483,26 +532,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
gte: parseFloat(recordFilter.value),
} as FloatFilter,
};
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
lte: parseFloat(recordFilter.value),
} as FloatFilter,
};
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: parseFloat(recordFilter.value),
} as FloatFilter,
};
case RecordFilterOperand.IS_NOT:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: parseFloat(recordFilter.value),
} as FloatFilter,
},
@@ -535,7 +584,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name + 'Id']: {
[fieldMetadataItem.name + 'Id']: {
in: recordIds,
} as RelationFilter,
};
@@ -545,13 +594,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name + 'Id']: {
[fieldMetadataItem.name + 'Id']: {
in: recordIds,
} as RelationFilter,
},
},
{
[correspondingFieldMetadataItem.name + 'Id']: {
[fieldMetadataItem.name + 'Id']: {
is: 'NULL',
} as RelationFilter,
},
@@ -579,7 +628,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (parsedCurrencyCodes.length === 0) return undefined;
const gqlFilter: RecordGqlOperationFilter = {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
currencyCode: { in: parsedCurrencyCodes },
} as CurrencyFilter,
};
@@ -607,26 +656,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
amountMicros: { gte: parseFloat(recordFilter.value) * 1000000 },
} as CurrencyFilter,
};
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
amountMicros: { lte: parseFloat(recordFilter.value) * 1000000 },
} as CurrencyFilter,
};
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
amountMicros: { eq: parseFloat(recordFilter.value) * 1000000 },
} as CurrencyFilter,
};
case RecordFilterOperand.IS_NOT:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
amountMicros: {
eq: parseFloat(recordFilter.value) * 1000000,
},
@@ -646,7 +695,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}
case 'LINKS': {
return computeGqlOperationFilterForLinks({
correspondingFieldMetadataItem,
correspondingFieldMetadataItem: fieldMetadataItem,
recordFilter,
subFieldName,
});
@@ -654,7 +703,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
case 'FULL_NAME': {
const fullNameFilters = generateILikeFiltersForCompositeFields(
recordFilter.value,
correspondingFieldMetadataItem.name,
fieldMetadataItem.name,
['firstName', 'lastName'],
);
switch (recordFilter.operand) {
@@ -665,7 +714,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
} else {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
ilike: `%${recordFilter.value}%`,
},
@@ -684,7 +733,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
} else {
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
ilike: `%${recordFilter.value}%`,
},
@@ -705,42 +754,42 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
or: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet1: {
ilike: `%${recordFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet2: {
ilike: `%${recordFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCity: {
ilike: `%${recordFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressState: {
ilike: `%${recordFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCountry: {
ilike: `%${recordFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressPostcode: {
ilike: `%${recordFilter.value}%`,
},
@@ -759,7 +808,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
in: parsedCountryCodes,
} as AddressFilter,
@@ -768,7 +817,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
ilike: `%${recordFilter.value}%`,
} as AddressFilter,
@@ -783,7 +832,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet1: {
ilike: `%${recordFilter.value}%`,
},
@@ -791,7 +840,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet1: {
is: 'NULL',
},
@@ -803,7 +852,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet2: {
ilike: `%${recordFilter.value}%`,
},
@@ -811,7 +860,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressStreet2: {
is: 'NULL',
},
@@ -823,7 +872,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCity: {
ilike: `%${recordFilter.value}%`,
},
@@ -831,7 +880,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCity: {
is: 'NULL',
},
@@ -843,7 +892,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressState: {
ilike: `%${recordFilter.value}%`,
},
@@ -851,7 +900,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressState: {
is: 'NULL',
},
@@ -863,7 +912,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressPostcode: {
ilike: `%${recordFilter.value}%`,
},
@@ -871,7 +920,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressPostcode: {
is: 'NULL',
},
@@ -883,7 +932,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCountry: {
ilike: `%${recordFilter.value}%`,
},
@@ -891,7 +940,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCountry: {
is: 'NULL',
},
@@ -918,7 +967,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCountry: {
in: JSON.parse(recordFilter.value),
} as AddressFilter,
@@ -926,7 +975,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
addressCountry: {
is: 'NULL',
} as AddressFilter,
@@ -940,7 +989,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
ilike: `%${recordFilter.value}%`,
} as AddressFilter,
@@ -948,7 +997,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
[subFieldName]: {
is: 'NULL',
} as AddressFilter,
@@ -976,7 +1025,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (nonEmptyOptions.length > 0) {
conditions.push({
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
containsAny: nonEmptyOptions,
} as MultiSelectFilter,
});
@@ -984,7 +1033,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (emptyOptions.length > 0) {
conditions.push({
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
isEmptyArray: true,
} as MultiSelectFilter,
});
@@ -997,18 +1046,18 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
containsAny: nonEmptyOptions,
} as MultiSelectFilter,
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
isEmptyArray: true,
} as MultiSelectFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
is: 'NULL',
} as MultiSelectFilter,
},
@@ -1034,7 +1083,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (nonEmptyOptions.length > 0) {
conditions.push({
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
in: nonEmptyOptions,
} as SelectFilter,
});
@@ -1042,7 +1091,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (emptyOptions.length > 0) {
conditions.push({
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
is: 'NULL',
} as SelectFilter,
});
@@ -1056,7 +1105,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (nonEmptyOptions.length > 0) {
conditions.push({
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
in: nonEmptyOptions,
} as SelectFilter,
},
@@ -1066,7 +1115,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
if (emptyOptions.length > 0) {
conditions.push({
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
is: 'NULL',
} as SelectFilter,
},
@@ -1085,14 +1134,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
containsIlike: `%${recordFilter.value}%`,
} as ArrayFilter,
};
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
containsIlike: `%${recordFilter.value}%`,
} as ArrayFilter,
},
@@ -1114,7 +1163,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
const parsedSources = JSON.parse(recordFilter.value) as string[];
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
source: {
in: parsedSources,
} satisfies RelationFilter,
@@ -1132,7 +1181,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
source: {
in: parsedSources,
} satisfies RelationFilter,
@@ -1141,12 +1190,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
}
default: {
const fieldForRecordFilter = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
throw new Error(
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
);
}
}
@@ -1177,7 +1222,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
workspaceMemberId: {
in: workspaceMemberIds,
} satisfies UUIDFilter,
@@ -1188,7 +1233,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
workspaceMemberId: {
in: workspaceMemberIds,
} satisfies UUIDFilter,
@@ -1196,7 +1241,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
workspaceMemberId: {
is: 'NULL',
} satisfies UUIDFilter,
@@ -1206,12 +1251,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
}
default: {
const fieldForRecordFilter = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
throw new Error(
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
);
}
}
@@ -1227,7 +1268,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
or: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
name: {
ilike: `%${recordFilter.value}%`,
},
@@ -1236,7 +1277,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
...(matchingSourceValues.length > 0
? [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
source: {
in: matchingSourceValues,
},
@@ -1252,7 +1293,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
and: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
name: {
ilike: `%${recordFilter.value}%`,
},
@@ -1263,7 +1304,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
? [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
source: {
in: matchingSourceValues,
},
@@ -1276,19 +1317,15 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
}
default: {
const fieldForRecordFilter = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
throw new Error(
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
);
}
}
}
case 'EMAILS': {
return computeGqlOperationFilterForEmails({
correspondingFieldMetadataItem,
correspondingFieldMetadataItem: fieldMetadataItem,
recordFilter,
subFieldName,
});
@@ -1306,21 +1343,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
or: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneNumber: {
ilike: `%${filterValue}%`,
},
} as PhonesFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneCallingCode: {
ilike: `%${filterValue}%`,
},
} as PhonesFilter,
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
like: `%${filterValue}%`,
},
@@ -1333,7 +1370,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
and: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneNumber: {
ilike: `%${filterValue}%`,
},
@@ -1342,7 +1379,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneCallingCode: {
ilike: `%${filterValue}%`,
},
@@ -1353,7 +1390,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
like: `%${filterValue}%`,
},
@@ -1361,7 +1398,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
is: 'NULL',
} as PhonesFilter,
@@ -1387,7 +1424,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return {
or: [
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
like: `%${filterValue}%`,
},
@@ -1400,7 +1437,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
or: [
{
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
like: `%${filterValue}%`,
},
@@ -1408,7 +1445,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
},
},
{
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
additionalPhones: {
is: 'NULL',
} as PhonesFilter,
@@ -1426,7 +1463,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneNumber: {
ilike: `%${filterValue}%`,
},
@@ -1435,7 +1472,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneNumber: {
ilike: `%${filterValue}%`,
},
@@ -1452,7 +1489,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.CONTAINS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneCallingCode: {
ilike: `%${filterValue}%`,
},
@@ -1461,7 +1498,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
case RecordFilterOperand.DOES_NOT_CONTAIN:
return {
not: {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
primaryPhoneCallingCode: {
ilike: `%${filterValue}%`,
},
@@ -1482,7 +1519,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
}
case 'BOOLEAN': {
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
eq: recordFilter.value === 'true',
} as BooleanFilter,
};
@@ -1495,7 +1532,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[correspondingFieldMetadataItem.name]: {
[fieldMetadataItem.name]: {
in: recordIds,
} as UUIDFilter,
};