mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-25 01:04:58 +03:00
Allow filtering by multi-select fields. <img width="1053" alt="Screenshot 2024-11-11 at 11 54 45" src="https://github.com/user-attachments/assets/a79b2251-94e3-48f8-abda-e808103a6c39"> --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
parent
ac1197afe1
commit
badebc513f
@ -61,7 +61,7 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
|
||||
result.current;
|
||||
|
||||
expect(columnDefinitions.length).toBe(21);
|
||||
expect(filterDefinitions.length).toBe(14);
|
||||
expect(filterDefinitions.length).toBe(15);
|
||||
expect(sortDefinitions.length).toBe(14);
|
||||
});
|
||||
});
|
||||
|
@ -35,6 +35,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
||||
FieldMetadataType.Address,
|
||||
FieldMetadataType.Relation,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.MultiSelect,
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.Rating,
|
||||
FieldMetadataType.Actor,
|
||||
|
@ -21,11 +21,7 @@ export type BooleanFilter = {
|
||||
|
||||
export type StringFilter = {
|
||||
eq?: string;
|
||||
gt?: string;
|
||||
gte?: string;
|
||||
in?: string[];
|
||||
lt?: string;
|
||||
lte?: string;
|
||||
neq?: string;
|
||||
startsWith?: string;
|
||||
like?: string;
|
||||
@ -35,6 +31,12 @@ export type StringFilter = {
|
||||
is?: IsFilter;
|
||||
};
|
||||
|
||||
export type RatingFilter = {
|
||||
eq?: string;
|
||||
in?: string[];
|
||||
is?: IsFilter;
|
||||
};
|
||||
|
||||
export type FloatFilter = {
|
||||
eq?: number;
|
||||
gt?: number;
|
||||
@ -104,10 +106,21 @@ export type PhonesFilter = {
|
||||
primaryPhoneCountryCode?: StringFilter;
|
||||
};
|
||||
|
||||
export type ArrayFilter = {
|
||||
contains?: string[];
|
||||
not_contains?: string[];
|
||||
export type SelectFilter = {
|
||||
is?: IsFilter;
|
||||
in?: string[];
|
||||
};
|
||||
|
||||
export type MultiSelectFilter = {
|
||||
is?: IsFilter;
|
||||
isEmptyArray?: boolean;
|
||||
containsAny?: string[];
|
||||
};
|
||||
|
||||
export type ArrayFilter = {
|
||||
is?: IsFilter;
|
||||
isEmptyArray?: boolean;
|
||||
containsIlike?: string;
|
||||
};
|
||||
|
||||
export type RawJsonFilter = {
|
||||
|
@ -93,7 +93,9 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
<ObjectFilterDropdownSourceSelect />
|
||||
</>
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
|
||||
{['SELECT', 'MULTI_SELECT'].includes(
|
||||
filterDefinitionUsedInDropdown.type,
|
||||
) && (
|
||||
<>
|
||||
<ObjectFilterDropdownSearchInput />
|
||||
<ObjectFilterDropdownOptionSelect />
|
||||
|
@ -58,8 +58,14 @@ export const getOperandsForFilterDefinition = (
|
||||
];
|
||||
case 'RELATION':
|
||||
return [...relationOperands, ...emptyOperands];
|
||||
case 'MULTI_SELECT':
|
||||
return [
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
...emptyOperands,
|
||||
];
|
||||
case 'SELECT':
|
||||
return [...relationOperands];
|
||||
return [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands];
|
||||
case 'ACTOR': {
|
||||
if (isActorSourceCompositeFilter(filterDefinition)) {
|
||||
return [
|
||||
|
@ -158,62 +158,6 @@ describe('isMatchingStringFilter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('gt', () => {
|
||||
it('value is greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gt: 'a' }, value: 'b' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gt: 'b' }, value: 'a' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gte', () => {
|
||||
it('value is greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gte: 'a' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gte: 'b' }, value: 'a' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lt', () => {
|
||||
it('value is less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lt: 'b' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lt: 'a' }, value: 'b' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lte', () => {
|
||||
it('value is less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'b' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startsWith', () => {
|
||||
it('value starts with the startsWith filter', () => {
|
||||
expect(
|
||||
|
@ -3,15 +3,18 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
ActorFilter,
|
||||
AddressFilter,
|
||||
ArrayFilter,
|
||||
CurrencyFilter,
|
||||
DateFilter,
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
MultiSelectFilter,
|
||||
RatingFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
RelationFilter,
|
||||
SelectFilter,
|
||||
StringFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
@ -241,7 +244,7 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
eq: convertRatingToRatingValue(parseFloat(filter.value)),
|
||||
} as StringFilter,
|
||||
} as RatingFilter,
|
||||
};
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
@ -249,7 +252,7 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
in: convertGreaterThanRatingToArrayOfRatingValues(
|
||||
parseFloat(filter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
} as RatingFilter,
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
@ -257,7 +260,7 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
in: convertLessThanRatingToArrayOfRatingValues(
|
||||
parseFloat(filter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
} as RatingFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
@ -309,30 +312,28 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
|
||||
const parsedRecordIds = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
if (parsedRecordIds.length === 0) return;
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot: {
|
||||
if (parsedRecordIds.length === 0) return;
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
if (parsedRecordIds.length > 0) {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
switch (filter.operand) {
|
||||
@ -349,7 +350,6 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CURRENCY':
|
||||
switch (filter.operand) {
|
||||
@ -601,6 +601,56 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'MULTI_SELECT': {
|
||||
if (isEmptyOperand) {
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
}
|
||||
|
||||
const options = resolveFilterValue(
|
||||
filter as Filter & { definition: { type: 'MULTI_SELECT' } },
|
||||
);
|
||||
|
||||
if (options.length === 0) return;
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
containsAny: options,
|
||||
} as MultiSelectFilter,
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
containsAny: options,
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
isEmptyArray: true,
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
is: 'NULL',
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'SELECT': {
|
||||
if (isEmptyOperand) {
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
@ -609,44 +659,61 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
filter.definition,
|
||||
);
|
||||
}
|
||||
const stringifiedSelectValues = filter.value;
|
||||
let parsedOptionValues: string[] = [];
|
||||
const options = resolveFilterValue(
|
||||
filter as Filter & { definition: { type: 'SELECT' } },
|
||||
);
|
||||
|
||||
if (!isNonEmptyString(stringifiedSelectValues)) {
|
||||
break;
|
||||
}
|
||||
if (options.length === 0) return;
|
||||
|
||||
try {
|
||||
parsedOptionValues = JSON.parse(stringifiedSelectValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedOptionValues.length > 0) {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
in: options,
|
||||
} as SelectFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
in: options,
|
||||
} as SelectFilter,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'ARRAY': {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
containsIlike: `%${filter.value}%`,
|
||||
} as ArrayFilter,
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
containsIlike: `%${filter.value}%`,
|
||||
} as ArrayFilter,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: fix this with a new composite field in ViewFilter entity
|
||||
case 'ACTOR': {
|
||||
@ -665,18 +732,17 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
case ViewFilterOperand.IsNot: {
|
||||
const parsedRecordIds = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
if (parsedRecordIds.length === 0) return;
|
||||
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
@ -716,7 +782,6 @@ const computeFilterRecordGqlOperationFilter = (
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.label} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'EMAILS':
|
||||
switch (filter.operand) {
|
||||
@ -806,7 +871,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = (
|
||||
);
|
||||
|
||||
if (!currentViewFilterGroup) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const groupFilters = filters.filter(
|
||||
|
@ -6,12 +6,14 @@ import {
|
||||
DateFilter,
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
MultiSelectFilter,
|
||||
RatingFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
RelationFilter,
|
||||
SelectFilter,
|
||||
StringFilter,
|
||||
URLFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
@ -255,7 +257,7 @@ export const getEmptyRecordGqlOperationFilter = (
|
||||
break;
|
||||
case 'RATING':
|
||||
emptyRecordFilter = {
|
||||
[correspondingField.name]: { is: 'NULL' } as StringFilter,
|
||||
[correspondingField.name]: { is: 'NULL' } as RatingFilter,
|
||||
};
|
||||
break;
|
||||
case 'DATE':
|
||||
@ -266,7 +268,21 @@ export const getEmptyRecordGqlOperationFilter = (
|
||||
break;
|
||||
case 'SELECT':
|
||||
emptyRecordFilter = {
|
||||
[correspondingField.name]: { is: 'NULL' } as UUIDFilter,
|
||||
[correspondingField.name]: { is: 'NULL' } as SelectFilter,
|
||||
};
|
||||
break;
|
||||
case 'MULTI_SELECT':
|
||||
emptyRecordFilter = {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: { is: 'NULL' } as MultiSelectFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
isEmptyArray: true,
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
case 'RELATION':
|
||||
@ -296,6 +312,9 @@ export const getEmptyRecordGqlOperationFilter = (
|
||||
{
|
||||
[correspondingField.name]: { is: 'NULL' } as ArrayFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: { isEmptyArray: true } as ArrayFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
|
@ -5,19 +5,9 @@ export const isMatchingArrayFilter = ({
|
||||
value,
|
||||
}: {
|
||||
arrayFilter: ArrayFilter;
|
||||
value: string[];
|
||||
value: string[] | null;
|
||||
}) => {
|
||||
if (value === null || !Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case arrayFilter.contains !== undefined: {
|
||||
return arrayFilter.contains.every((item) => value.includes(item));
|
||||
}
|
||||
case arrayFilter.not_contains !== undefined: {
|
||||
return !arrayFilter.not_contains.some((item) => value.includes(item));
|
||||
}
|
||||
case arrayFilter.is !== undefined: {
|
||||
if (arrayFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
@ -25,6 +15,16 @@ export const isMatchingArrayFilter = ({
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
case arrayFilter.isEmptyArray !== undefined: {
|
||||
return Array.isArray(value) && value.length === 0;
|
||||
}
|
||||
case arrayFilter.containsIlike !== undefined: {
|
||||
const searchTerm = arrayFilter.containsIlike.toLowerCase();
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.some((item) => item.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for array filter: ${JSON.stringify(arrayFilter)}`,
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { MultiSelectFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
|
||||
export const isMatchingMultiSelectFilter = ({
|
||||
multiSelectFilter,
|
||||
value,
|
||||
}: {
|
||||
multiSelectFilter: MultiSelectFilter;
|
||||
value: string[] | null;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case multiSelectFilter.containsAny !== undefined: {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
multiSelectFilter.containsAny.every((item) => value.includes(item))
|
||||
);
|
||||
}
|
||||
case multiSelectFilter.isEmptyArray !== undefined: {
|
||||
return Array.isArray(value) && value.length === 0;
|
||||
}
|
||||
case multiSelectFilter.is !== undefined: {
|
||||
if (multiSelectFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for multi-select filter: ${JSON.stringify(
|
||||
multiSelectFilter,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import { RatingFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
|
||||
export const isMatchingRatingFilter = ({
|
||||
ratingFilter,
|
||||
value,
|
||||
}: {
|
||||
ratingFilter: RatingFilter;
|
||||
value: string | null;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case ratingFilter.eq !== undefined: {
|
||||
return value === ratingFilter.eq;
|
||||
}
|
||||
case ratingFilter.in !== undefined: {
|
||||
return value !== null && ratingFilter.in.includes(value);
|
||||
}
|
||||
case ratingFilter.is !== undefined: {
|
||||
if (ratingFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for rating filter : ${JSON.stringify(ratingFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { SelectFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
|
||||
export const isMatchingSelectFilter = ({
|
||||
selectFilter,
|
||||
value,
|
||||
}: {
|
||||
selectFilter: SelectFilter;
|
||||
value: string;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case selectFilter.in !== undefined: {
|
||||
return selectFilter.in.includes(value);
|
||||
}
|
||||
case selectFilter.is !== undefined: {
|
||||
if (selectFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -48,18 +48,6 @@ export const isMatchingStringFilter = ({
|
||||
|
||||
return regexCaseInsensitive.test(value);
|
||||
}
|
||||
case stringFilter.gt !== undefined: {
|
||||
return value > stringFilter.gt;
|
||||
}
|
||||
case stringFilter.gte !== undefined: {
|
||||
return value >= stringFilter.gte;
|
||||
}
|
||||
case stringFilter.lt !== undefined: {
|
||||
return value < stringFilter.lt;
|
||||
}
|
||||
case stringFilter.lte !== undefined: {
|
||||
return value <= stringFilter.lte;
|
||||
}
|
||||
case stringFilter.startsWith !== undefined: {
|
||||
return value.startsWith(stringFilter.startsWith);
|
||||
}
|
||||
|
@ -14,11 +14,14 @@ import {
|
||||
FullNameFilter,
|
||||
LeafObjectRecordFilter,
|
||||
LinksFilter,
|
||||
MultiSelectFilter,
|
||||
NotObjectRecordFilter,
|
||||
OrObjectRecordFilter,
|
||||
PhonesFilter,
|
||||
RatingFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
SelectFilter,
|
||||
StringFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
@ -27,7 +30,10 @@ import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isM
|
||||
import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter';
|
||||
import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter';
|
||||
import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter';
|
||||
import { isMatchingMultiSelectFilter } from '@/object-record/record-filter/utils/isMatchingMultiSelectFilter';
|
||||
import { isMatchingRatingFilter } from '@/object-record/record-filter/utils/isMatchingRatingFilter';
|
||||
import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isMatchingRawJsonFilter';
|
||||
import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter';
|
||||
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
|
||||
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -160,15 +166,27 @@ export const isRecordMatchingFilter = ({
|
||||
}
|
||||
|
||||
switch (objectMetadataField.type) {
|
||||
case FieldMetadataType.Select:
|
||||
case FieldMetadataType.Rating:
|
||||
case FieldMetadataType.MultiSelect:
|
||||
return isMatchingRatingFilter({
|
||||
ratingFilter: filterValue as RatingFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
case FieldMetadataType.Text: {
|
||||
return isMatchingStringFilter({
|
||||
stringFilter: filterValue as StringFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.Select:
|
||||
return isMatchingSelectFilter({
|
||||
selectFilter: filterValue as SelectFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
case FieldMetadataType.MultiSelect:
|
||||
return isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: filterValue as MultiSelectFilter,
|
||||
value: record[filterKey],
|
||||
});
|
||||
case FieldMetadataType.Array: {
|
||||
return isMatchingArrayFilter({
|
||||
arrayFilter: filterValue as ArrayFilter,
|
||||
|
@ -75,7 +75,10 @@ export const ViewBarFilterEffect = ({
|
||||
? JSON.parse(viewFilterUsedInDropdown.value)
|
||||
: [];
|
||||
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecords);
|
||||
} else if (filterDefinitionUsedInDropdown?.type === 'SELECT') {
|
||||
} else if (
|
||||
isDefined(filterDefinitionUsedInDropdown) &&
|
||||
['SELECT', 'MULTI_SELECT'].includes(filterDefinitionUsedInDropdown.type)
|
||||
) {
|
||||
const viewFilterUsedInDropdown =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
|
||||
(filter) =>
|
||||
|
@ -2,6 +2,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue';
|
||||
import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue';
|
||||
import {
|
||||
resolveDateViewFilterValue,
|
||||
ResolvedDateViewFilterValue,
|
||||
@ -14,7 +15,9 @@ type ResolvedFilterValue<
|
||||
? ResolvedDateViewFilterValue<O>
|
||||
: T extends 'NUMBER'
|
||||
? ReturnType<typeof resolveNumberViewFilterValue>
|
||||
: string;
|
||||
: T extends 'SELECT' | 'MULTI_SELECT'
|
||||
? string[]
|
||||
: string;
|
||||
|
||||
type PartialFilter<
|
||||
T extends FilterableFieldType,
|
||||
@ -36,6 +39,9 @@ export const resolveFilterValue = <
|
||||
return resolveDateViewFilterValue(filter) as ResolvedFilterValue<T, O>;
|
||||
case 'NUMBER':
|
||||
return resolveNumberViewFilterValue(filter) as ResolvedFilterValue<T, O>;
|
||||
case 'SELECT':
|
||||
case 'MULTI_SELECT':
|
||||
return resolveSelectViewFilterValue(filter) as ResolvedFilterValue<T, O>;
|
||||
default:
|
||||
return filter.value as ResolvedFilterValue<T, O>;
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { z } from 'zod';
|
||||
|
||||
const selectViewFilterValueSchema = z
|
||||
.string()
|
||||
.transform((val) => (val === '' ? [] : JSON.parse(val)))
|
||||
.refine(
|
||||
(parsed) =>
|
||||
Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'),
|
||||
{
|
||||
message: 'Expected an array of strings',
|
||||
},
|
||||
);
|
||||
|
||||
export const resolveSelectViewFilterValue = (
|
||||
viewFilter: Pick<ViewFilter, 'value'>,
|
||||
) => {
|
||||
return selectViewFilterValueSchema.parse(viewFilter.value);
|
||||
};
|
@ -13,7 +13,7 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
const ARRAY_OPERATORS = ['in', 'contains', 'not_contains'];
|
||||
const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
|
||||
|
||||
export class GraphqlQueryFilterFieldParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
|
@ -19,6 +19,11 @@ export const computeWhereConditionParts = (
|
||||
const uuid = Math.random().toString(36).slice(2, 7);
|
||||
|
||||
switch (operator) {
|
||||
case 'isEmptyArray':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = '{}'`,
|
||||
params: {},
|
||||
};
|
||||
case 'eq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
|
||||
@ -84,10 +89,19 @@ export const computeWhereConditionParts = (
|
||||
sql: `"${objectNameSingular}"."${key}" @> ARRAY[:...${key}${uuid}]`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
|
||||
case 'not_contains':
|
||||
case 'notContains':
|
||||
return {
|
||||
sql: `NOT ("${objectNameSingular}"."${key}" && ARRAY[:...${key}${uuid}])`,
|
||||
sql: `NOT ("${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[])`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'containsAny':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[]`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'containsIlike':
|
||||
return {
|
||||
sql: `EXISTS (SELECT 1 FROM unnest("${objectNameSingular}"."${key}") AS elem WHERE elem ILIKE :${key}${uuid})`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql';
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
GraphQLList,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
|
||||
@ -103,7 +108,9 @@ export class InputTypeFactory {
|
||||
eq: { type: enumType },
|
||||
neq: { type: enumType },
|
||||
in: { type: new GraphQLList(enumType) },
|
||||
containsAny: { type: new GraphQLList(enumType) },
|
||||
is: { type: FilterIs },
|
||||
isEmptyArray: { type: GraphQLBoolean },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql';
|
||||
import { GraphQLBoolean, GraphQLInputObjectType, GraphQLString } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const ArrayFilterType = new GraphQLInputObjectType({
|
||||
name: 'ArrayFilter',
|
||||
fields: {
|
||||
contains: { type: new GraphQLList(GraphQLString) },
|
||||
not_contains: { type: new GraphQLList(GraphQLString) },
|
||||
containsIlike: { type: GraphQLString },
|
||||
is: { type: FilterIs },
|
||||
isEmptyArray: { type: GraphQLBoolean },
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,17 @@
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const MultiSelectFilterType = new GraphQLInputObjectType({
|
||||
name: 'MultiSelectFilter',
|
||||
fields: {
|
||||
containsAny: { type: new GraphQLList(GraphQLString) },
|
||||
is: { type: FilterIs },
|
||||
isEmptyArray: { type: GraphQLBoolean },
|
||||
},
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const SelectFilterType = new GraphQLInputObjectType({
|
||||
name: 'SelectFilter',
|
||||
fields: {
|
||||
in: { type: new GraphQLList(GraphQLString) },
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
});
|
@ -27,6 +27,8 @@ import {
|
||||
StringFilterType,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
|
||||
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
|
||||
import { MultiSelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/multi-select-filter.input-type';
|
||||
import { SelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/select-filter.input-type';
|
||||
import {
|
||||
BigFloatScalarType,
|
||||
UUIDScalarType,
|
||||
@ -115,6 +117,8 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
||||
[FieldMetadataType.RICH_TEXT, StringFilterType],
|
||||
[FieldMetadataType.ARRAY, ArrayFilterType],
|
||||
[FieldMetadataType.MULTI_SELECT, MultiSelectFilterType],
|
||||
[FieldMetadataType.SELECT, SelectFilterType],
|
||||
[FieldMetadataType.TS_VECTOR, StringFilterType], // TODO: Add TSVectorFilterType
|
||||
]);
|
||||
|
||||
|
@ -58,7 +58,9 @@ export const generateFields = <
|
||||
? {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
isArray:
|
||||
kind !== InputTypeDefinitionKind.Filter &&
|
||||
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
settings: fieldMetadata.settings,
|
||||
isIdField: fieldMetadata.name === 'id',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user