Automatically Apply Values on Filtered Views (#11717)

Issue : When I create a task in the "Assigned to me" task view, it will
disappear from the view because the Assignee field isn't automatically
populated.

Solution :
We created a "buildRecordInputFromFilters" funciron that will convert
filtered into their corresponding values for the input.


Fixes https://github.com/twentyhq/core-team-issues/issues/708

---------

Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
This commit is contained in:
Guillim
2025-04-24 19:02:03 +02:00
committed by GitHub
parent 4d7dbb1991
commit 9fb7ef5d47
6 changed files with 991 additions and 63 deletions

View File

@ -1,4 +1,5 @@
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
export type RecordFilter = {
@ -14,3 +15,6 @@ export type RecordFilter = {
label: string;
subFieldName?: string | null | undefined;
};
export type RecordFilterToRecordInputOperand<T extends FilterableFieldType> =
(typeof FILTER_OPERANDS_MAP)[T][number];

View File

@ -7,17 +7,116 @@ export type GetRecordFilterOperandsParams = {
subFieldName?: string | null | undefined;
};
const emptyOperands = [
RecordFilterOperand.IsEmpty,
RecordFilterOperand.IsNotEmpty,
] as const;
const relationOperands = [
RecordFilterOperand.Is,
RecordFilterOperand.IsNot,
] as const;
type FilterOperandMap = {
[K in FilterableFieldType]: readonly RecordFilterOperand[];
};
export const FILTER_OPERANDS_MAP = {
TEXT: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
EMAILS: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
FULL_NAME: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
ADDRESS: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
LINKS: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
PHONES: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
CURRENCY: [
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
],
NUMBER: [
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
],
RAW_JSON: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
DATE_TIME: [
RecordFilterOperand.Is,
RecordFilterOperand.IsRelative,
RecordFilterOperand.IsInPast,
RecordFilterOperand.IsInFuture,
RecordFilterOperand.IsToday,
RecordFilterOperand.IsBefore,
RecordFilterOperand.IsAfter,
...emptyOperands,
],
DATE: [
RecordFilterOperand.Is,
RecordFilterOperand.IsRelative,
RecordFilterOperand.IsInPast,
RecordFilterOperand.IsInFuture,
RecordFilterOperand.IsToday,
RecordFilterOperand.IsBefore,
RecordFilterOperand.IsAfter,
...emptyOperands,
],
RATING: [
RecordFilterOperand.Is,
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
],
RELATION: [...relationOperands, ...emptyOperands],
MULTI_SELECT: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
SELECT: [RecordFilterOperand.Is, RecordFilterOperand.IsNot, ...emptyOperands],
ACTOR: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
ARRAY: [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
],
BOOLEAN: [RecordFilterOperand.Is],
} as const satisfies FilterOperandMap;
export const getRecordFilterOperands = ({
filterType,
subFieldName,
}: GetRecordFilterOperandsParams): RecordFilterOperand[] => {
const emptyOperands = [
RecordFilterOperand.IsEmpty,
RecordFilterOperand.IsNotEmpty,
];
const relationOperands = [RecordFilterOperand.Is, RecordFilterOperand.IsNot];
}: GetRecordFilterOperandsParams) => {
switch (filterType) {
case 'TEXT':
case 'EMAILS':
@ -25,57 +124,23 @@ export const getRecordFilterOperands = ({
case 'ADDRESS':
case 'LINKS':
case 'PHONES':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.TEXT;
case 'CURRENCY':
case 'NUMBER':
return [
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.NUMBER;
case 'RAW_JSON':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.RAW_JSON;
case 'DATE_TIME':
case 'DATE':
return [
RecordFilterOperand.Is,
RecordFilterOperand.IsRelative,
RecordFilterOperand.IsInPast,
RecordFilterOperand.IsInFuture,
RecordFilterOperand.IsToday,
RecordFilterOperand.IsBefore,
RecordFilterOperand.IsAfter,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.DATE_TIME;
case 'RATING':
return [
RecordFilterOperand.Is,
RecordFilterOperand.GreaterThan,
RecordFilterOperand.LessThan,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.RATING;
case 'RELATION':
return [...relationOperands, ...emptyOperands];
return FILTER_OPERANDS_MAP.RELATION;
case 'MULTI_SELECT':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.MULTI_SELECT;
case 'SELECT':
return [
RecordFilterOperand.Is,
RecordFilterOperand.IsNot,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.SELECT;
case 'ACTOR': {
if (isFilterOnActorSourceSubField(subFieldName)) {
return [
@ -85,20 +150,12 @@ export const getRecordFilterOperands = ({
];
}
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.ACTOR;
}
case 'ARRAY':
return [
RecordFilterOperand.Contains,
RecordFilterOperand.DoesNotContain,
...emptyOperands,
];
return FILTER_OPERANDS_MAP.ARRAY;
case 'BOOLEAN':
return [RecordFilterOperand.Is];
return FILTER_OPERANDS_MAP.BOOLEAN;
default:
return [];
}

View File

@ -0,0 +1,58 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { buildValueFromFilter } from '@/object-record/record-table/utils/buildRecordInputFromFilter';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useBuildRecordInputFromFilters = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
// we might need to build a recoil callback for better performance
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const buildRecordInputFromFilters = (): Partial<ObjectRecord> => {
const recordInput: Partial<ObjectRecord> = {};
currentRecordFilters.forEach((filter) => {
const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === filter.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return;
}
if (fieldMetadataItem.type === 'RELATION') {
const value = buildValueFromFilter({
filter,
options: fieldMetadataItem.options ?? undefined,
relationType: fieldMetadataItem.relationDefinition?.direction,
currentWorkspaceMember: currentWorkspaceMember ?? undefined,
label: filter.label,
});
if (!isDefined(value)) {
return;
}
recordInput[`${fieldMetadataItem.name}Id`] = value;
} else {
recordInput[fieldMetadataItem.name] = buildValueFromFilter({
filter,
options: fieldMetadataItem.options ?? undefined,
});
}
});
return recordInput;
};
return { buildRecordInputFromFilters };
};

View File

@ -2,6 +2,7 @@ import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordIn
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useBuildRecordInputFromFilters } from '@/object-record/record-table/hooks/useBuildRecordInputFromFilters';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -27,17 +28,25 @@ export const useCreateNewIndexRecord = ({
const { openRecordTitleCell } = useRecordTitleCell();
const { buildRecordInputFromFilters } = useBuildRecordInputFromFilters({
objectMetadataItem,
});
const createNewIndexRecord = useRecoilCallback(
({ snapshot }) =>
async (recordInput?: Partial<ObjectRecord>) => {
const recordId = v4();
const recordInputFromFilters = buildRecordInputFromFilters();
const recordIndexOpenRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInState)
.getValue();
await createOneRecord({ id: recordId, ...recordInput });
await createOneRecord({
id: recordId,
...recordInputFromFilters,
...recordInput,
});
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
openRecordInCommandMenu({
recordId,
@ -58,6 +67,7 @@ export const useCreateNewIndexRecord = ({
}
},
[
buildRecordInputFromFilters,
createOneRecord,
navigate,
objectMetadataItem.labelIdentifierFieldMetadataId,

View File

@ -0,0 +1,500 @@
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { buildValueFromFilter } from './buildRecordInputFromFilter';
// TODO: fix the dates, and test the not supported types
const mockDate = new Date('2024-03-20T12:00:00Z');
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(mockDate);
});
afterAll(() => {
jest.useRealTimers();
});
describe('buildValueFromFilter', () => {
const createTestFilter = (
operand: ViewFilterOperand,
value: string,
type: FilterableFieldType,
): RecordFilter => ({
id: 'test-id',
fieldMetadataId: 'test-field-id',
value,
displayValue: value,
type,
operand,
label: 'Test Label',
});
describe('TEXT field type', () => {
const testCases = [
{
operand: ViewFilterOperand.Contains,
value: 'test',
expected: 'test',
},
{
operand: ViewFilterOperand.DoesNotContain,
value: 'test',
expected: undefined,
},
{
operand: ViewFilterOperand.IsNotEmpty,
value: 'test',
expected: 'test',
},
{
operand: ViewFilterOperand.IsEmpty,
value: 'test',
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'TEXT');
expect(buildValueFromFilter({ filter })).toBe(expected);
},
);
});
describe('DATE_TIME field type', () => {
const testCases = [
{
operand: ViewFilterOperand.Is,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsAfter,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsBefore,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsInPast,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsInFuture,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsToday,
value: '',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsRelative,
value: '',
expected: mockDate,
},
{
operand: ViewFilterOperand.IsEmpty,
value: '',
expected: undefined,
},
{
operand: ViewFilterOperand.IsNotEmpty,
value: '2024-03-20T12:00:00Z',
expected: mockDate,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'DATE_TIME');
const result = buildValueFromFilter({ filter });
if (expected instanceof Date) {
expect(result).toBeInstanceOf(Date);
expect(result).toEqual(expected);
} else {
expect(result).toBe(expected);
}
},
);
});
describe('NUMBER field type', () => {
const testCases = [
{
operand: ViewFilterOperand.GreaterThan,
value: '5',
expected: 6,
},
{
operand: ViewFilterOperand.LessThan,
value: '5',
expected: 4,
},
{
operand: ViewFilterOperand.IsNotEmpty,
value: '5',
expected: 5,
},
{
operand: ViewFilterOperand.IsEmpty,
value: '5',
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'NUMBER');
expect(buildValueFromFilter({ filter })).toBe(expected);
},
);
});
describe('BOOLEAN field type', () => {
const testCases = [
{
operand: ViewFilterOperand.Is,
value: 'true',
expected: true,
},
{
operand: ViewFilterOperand.Is,
value: 'false',
expected: false,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'BOOLEAN');
expect(buildValueFromFilter({ filter })).toBe(expected);
},
);
});
describe('ARRAY field type', () => {
const testCases = [
{
operand: ViewFilterOperand.Contains,
value: 'test',
expected: 'test',
},
{
operand: ViewFilterOperand.DoesNotContain,
value: 'test',
expected: undefined,
},
{
operand: ViewFilterOperand.IsNotEmpty,
value: 'test',
expected: 'test',
},
{
operand: ViewFilterOperand.IsEmpty,
value: 'test',
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'ARRAY');
expect(buildValueFromFilter({ filter })).toBe(expected);
},
);
});
describe('RELATION field type', () => {
const mockCurrentWorkspaceMember = {
id: 'current-workspace-member-id',
name: { firstName: 'John', lastName: 'Doe' },
locale: 'en',
colorScheme: 'Light' as ColorScheme,
avatarUrl: '',
dateFormat: null,
timeFormat: null,
timeZone: null,
};
const testCases = [
{
operand: ViewFilterOperand.Is,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: ['record-1'],
}),
relationType: RelationDefinitionType.MANY_TO_ONE,
label: 'belongs to one',
expected: 'record-1',
},
{
operand: ViewFilterOperand.Is,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: true,
selectedRecordIds: ['record-1'],
}),
relationType: RelationDefinitionType.MANY_TO_ONE,
label: 'Assignee',
expected: 'current-workspace-member-id',
},
{
operand: ViewFilterOperand.Is,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: ['record-1', 'record-2'],
}),
relationType: RelationDefinitionType.MANY_TO_MANY,
label: 'hasmany',
expected: undefined,
},
{
operand: ViewFilterOperand.IsNot,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: ['record-1'],
}),
relationType: RelationDefinitionType.MANY_TO_ONE,
label: 'Assignee',
expected: undefined,
},
{
operand: ViewFilterOperand.IsEmpty,
value: JSON.stringify({
isCurrentWorkspaceMemberSelected: false,
selectedRecordIds: ['record-1'],
}),
relationType: RelationDefinitionType.MANY_TO_ONE,
label: 'Assignee',
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value" for $relationType relation',
({ operand, value, relationType, label, expected }) => {
const filter = createTestFilter(operand, value, 'RELATION');
expect(
buildValueFromFilter({
filter,
relationType,
currentWorkspaceMember: mockCurrentWorkspaceMember,
label,
}),
).toEqual(expected);
},
);
});
describe('Composite field types', () => {
const compositeTypes: FilterableFieldType[] = ['ACTOR', 'FULL_NAME'];
it.each(compositeTypes)(
'should return undefined for composite type %s',
(type) => {
const filter = createTestFilter(ViewFilterOperand.Is, 'test', type);
expect(buildValueFromFilter({ filter })).toBeUndefined();
},
);
});
describe('RAW_JSON field type', () => {
it('should return undefined', () => {
const filter = createTestFilter(ViewFilterOperand.Is, 'test', 'RAW_JSON');
expect(buildValueFromFilter({ filter })).toBeUndefined();
});
});
describe('RATING field type', () => {
const mockOptions = [
{
label: 'Rating 1',
value: 'RATING_1',
id: '1',
position: 1,
},
{
label: 'Rating 2',
value: 'RATING_2',
id: '2',
position: 2,
},
{
label: 'Rating 3',
value: 'RATING_3',
id: '3',
position: 3,
},
];
const testCases = [
{
operand: ViewFilterOperand.Is,
value: 'Rating 1',
expected: 'RATING_1',
},
{
operand: ViewFilterOperand.IsNotEmpty,
value: 'Rating 2',
expected: 'RATING_2',
},
{
operand: ViewFilterOperand.IsEmpty,
value: 'Rating 1',
expected: undefined,
},
{
operand: ViewFilterOperand.GreaterThan,
value: 'Rating 1',
expected: 'RATING_2',
},
{
operand: ViewFilterOperand.LessThan,
value: 'Rating 2',
expected: 'RATING_1',
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'RATING');
expect(
buildValueFromFilter({
filter,
options: mockOptions as FieldMetadataItemOption[],
}),
).toBe(expected);
},
);
it('should return undefined when option is not found', () => {
const filter = createTestFilter(
ViewFilterOperand.Is,
'Rating 4',
'RATING',
);
expect(
buildValueFromFilter({
filter,
options: mockOptions as FieldMetadataItemOption[],
}),
).toBeUndefined();
});
});
describe('SELECT field type', () => {
const mockOptions = [
{
label: 'Option 1',
value: 'OPTION_1',
color: 'red',
id: '1',
position: 1,
},
{
label: 'Option 2',
value: 'OPTION_2',
color: 'blue',
id: '2',
position: 2,
},
];
const testCases = [
{
operand: ViewFilterOperand.Is,
value: JSON.stringify(['OPTION_1']),
expected: 'OPTION_1',
},
{
operand: ViewFilterOperand.IsNot,
value: JSON.stringify(['OPTION_1']),
expected: undefined,
},
{
operand: ViewFilterOperand.IsEmpty,
value: JSON.stringify(['OPTION_1']),
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'SELECT');
expect(
buildValueFromFilter({
filter,
options: mockOptions as FieldMetadataItemOption[],
}),
).toBe(expected);
},
);
it('should handle invalid JSON', () => {
const filter = createTestFilter(
ViewFilterOperand.Is,
'invalid-json',
'SELECT',
);
expect(
buildValueFromFilter({
filter,
options: mockOptions as FieldMetadataItemOption[],
}),
).toBeUndefined();
});
});
describe('MULTI_SELECT field type', () => {
const testCases = [
{
operand: ViewFilterOperand.Contains,
value: JSON.stringify(['OPTION_1', 'OPTION_2']),
expected: ['OPTION_1', 'OPTION_2'],
},
{
operand: ViewFilterOperand.DoesNotContain,
value: JSON.stringify(['OPTION_1']),
expected: undefined,
},
{
operand: ViewFilterOperand.IsEmpty,
value: JSON.stringify(['OPTION_1']),
expected: undefined,
},
];
it.each(testCases)(
'should handle $operand with value "$value"',
({ operand, value, expected }) => {
const filter = createTestFilter(operand, value, 'MULTI_SELECT');
expect(buildValueFromFilter({ filter })).toEqual(expected);
},
);
it('should handle invalid JSON', () => {
const filter = createTestFilter(
ViewFilterOperand.Contains,
'invalid-json',
'MULTI_SELECT',
);
expect(buildValueFromFilter({ filter })).toBeUndefined();
});
});
});

View File

@ -0,0 +1,299 @@
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
import {
RecordFilter,
RecordFilterToRecordInputOperand,
} from '@/object-record/record-filter/types/RecordFilter';
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { assertUnreachable } from 'twenty-shared/utils';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { parseJson } from '~/utils/parseJson';
export const buildValueFromFilter = ({
filter,
options,
relationType,
currentWorkspaceMember,
label,
}: {
filter: RecordFilter;
options?: FieldMetadataItemOption[];
relationType?: RelationDefinitionType;
currentWorkspaceMember?: CurrentWorkspaceMember;
label?: string;
}) => {
if (isCompositeField(filter.type)) {
return;
}
if (filter.type === 'RAW_JSON') {
return;
}
const operands = FILTER_OPERANDS_MAP[filter.type];
if (!operands.some((operand) => operand === filter.operand)) {
throw new Error('Operand not supported for this field type');
}
switch (filter.type) {
case 'TEXT': {
return computeValueFromFilterText(
filter.operand as (typeof FILTER_OPERANDS_MAP)['TEXT'][number],
filter.value,
);
}
case 'RATING':
return computeValueFromFilterRating(
filter.operand as (typeof FILTER_OPERANDS_MAP)['RATING'][number],
filter.value,
options,
);
case 'DATE_TIME':
case 'DATE':
return computeValueFromFilterDate(
filter.operand as (typeof FILTER_OPERANDS_MAP)['DATE_TIME'][number],
filter.value,
);
case 'NUMBER':
return computeValueFromFilterNumber(
filter.operand as (typeof FILTER_OPERANDS_MAP)['NUMBER'][number],
filter.value,
);
case 'BOOLEAN':
return computeValueFromFilterBoolean(
filter.operand as (typeof FILTER_OPERANDS_MAP)['BOOLEAN'][number],
filter.value,
);
case 'ARRAY':
return computeValueFromFilterArray(
filter.operand as (typeof FILTER_OPERANDS_MAP)['ARRAY'][number],
filter.value,
);
case 'SELECT':
return computeValueFromFilterSelect(
filter.operand as (typeof FILTER_OPERANDS_MAP)['SELECT'][number],
filter.value,
options,
);
case 'MULTI_SELECT':
return computeValueFromFilterMultiSelect(
filter.operand as (typeof FILTER_OPERANDS_MAP)['MULTI_SELECT'][number],
filter.value,
);
case 'RELATION': {
return computeValueFromFilterRelation(
filter.operand as (typeof FILTER_OPERANDS_MAP)['RELATION'][number],
filter.value,
relationType,
currentWorkspaceMember,
label,
);
}
default:
assertUnreachable(filter.type);
}
};
const computeValueFromFilterText = (
operand: RecordFilterToRecordInputOperand<'TEXT'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.Contains:
return value;
case ViewFilterOperand.IsNotEmpty:
return value;
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.DoesNotContain:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterDate = (
operand: RecordFilterToRecordInputOperand<'DATE_TIME'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.Is:
case ViewFilterOperand.IsAfter:
case ViewFilterOperand.IsBefore:
return new Date(value);
case ViewFilterOperand.IsToday:
case ViewFilterOperand.IsNotEmpty:
case ViewFilterOperand.IsInPast:
case ViewFilterOperand.IsInFuture:
case ViewFilterOperand.IsRelative:
return new Date();
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterNumber = (
operand: RecordFilterToRecordInputOperand<'NUMBER'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.GreaterThan:
return Number(value) + 1;
case ViewFilterOperand.LessThan:
return Number(value) - 1;
case ViewFilterOperand.IsNotEmpty:
return Number(value);
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterBoolean = (
operand: RecordFilterToRecordInputOperand<'BOOLEAN'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.Is:
return value === 'true';
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterArray = (
operand: RecordFilterToRecordInputOperand<'ARRAY'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.Contains:
case ViewFilterOperand.IsNotEmpty:
return value;
case ViewFilterOperand.DoesNotContain:
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterRating = (
operand: RecordFilterToRecordInputOperand<'RATING'>,
value: string,
options?: FieldMetadataItemOption[],
) => {
const option = options?.find((option) => option.label === value);
if (!option) {
return undefined;
}
switch (operand) {
case ViewFilterOperand.Is:
case ViewFilterOperand.IsNotEmpty:
return option.value;
case ViewFilterOperand.GreaterThan: {
const plusOne = options?.find(
(opt) => opt.position === option.position + 1,
)?.value;
return plusOne ? plusOne : option.value;
}
case ViewFilterOperand.LessThan: {
const minusOne = options?.find(
(opt) => opt.position === option.position - 1,
)?.value;
return minusOne ? minusOne : option.value;
}
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterSelect = (
operand: RecordFilterToRecordInputOperand<'SELECT'>,
value: string,
options?: FieldMetadataItemOption[],
) => {
switch (operand) {
case ViewFilterOperand.Is:
case ViewFilterOperand.IsNotEmpty:
try {
const valueParsed = parseJson<string[]>(value)?.[0];
const option = options?.find((option) => option.value === valueParsed);
if (!option) {
return undefined;
}
return option.value;
} catch (error) {
return undefined;
}
case ViewFilterOperand.IsNot:
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterMultiSelect = (
operand: RecordFilterToRecordInputOperand<'MULTI_SELECT'>,
value: string,
) => {
switch (operand) {
case ViewFilterOperand.Contains:
case ViewFilterOperand.IsNotEmpty:
try {
const parsedValue = parseJson<string[]>(value);
return parsedValue ? parsedValue : undefined;
} catch (error) {
return undefined;
}
case ViewFilterOperand.DoesNotContain:
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};
const computeValueFromFilterRelation = (
operand: RecordFilterToRecordInputOperand<'RELATION'>,
value: string,
relationType?: RelationDefinitionType,
currentWorkspaceMember?: CurrentWorkspaceMember,
label?: string,
) => {
switch (operand) {
case ViewFilterOperand.Is: {
const parsedValue = parseJson<{
isCurrentWorkspaceMemberSelected: boolean;
selectedRecordIds: string[];
}>(value);
if (
relationType === RelationDefinitionType.MANY_TO_ONE ||
relationType === RelationDefinitionType.ONE_TO_ONE
) {
if (label === 'Assignee') {
return parsedValue?.isCurrentWorkspaceMemberSelected
? currentWorkspaceMember?.id
: undefined;
} else {
return parsedValue?.selectedRecordIds?.[0];
}
}
return undefined; //todo
}
case ViewFilterOperand.IsNot:
case ViewFilterOperand.IsNotEmpty: // todo
case ViewFilterOperand.IsEmpty:
return undefined;
default:
assertUnreachable(operand);
}
};