fix: implement the same word boundary search as the like query (#1038)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
@@ -61,6 +61,7 @@ const ReactSelect: React.FC<Props> = (props) => {
|
|||||||
isClearable,
|
isClearable,
|
||||||
isMulti,
|
isMulti,
|
||||||
isSortable,
|
isSortable,
|
||||||
|
filterOption = undefined,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
@@ -108,6 +109,7 @@ const ReactSelect: React.FC<Props> = (props) => {
|
|||||||
MultiValueLabel: SortableMultiValueLabel,
|
MultiValueLabel: SortableMultiValueLabel,
|
||||||
DropdownIndicator: Chevron,
|
DropdownIndicator: Chevron,
|
||||||
}}
|
}}
|
||||||
|
filterOption={filterOption}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,6 +127,7 @@ const ReactSelect: React.FC<Props> = (props) => {
|
|||||||
options={options}
|
options={options}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
|
filterOption={filterOption}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { OptionsType, GroupedOptionsType } from 'react-select';
|
|||||||
|
|
||||||
export type Options = OptionsType<Value> | GroupedOptionsType<Value>;
|
export type Options = OptionsType<Value> | GroupedOptionsType<Value>;
|
||||||
|
|
||||||
|
export type OptionType = {
|
||||||
|
[key: string]: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type Value = {
|
export type Value = {
|
||||||
label: string
|
label: string
|
||||||
value: string | null
|
value: string | null
|
||||||
@@ -23,4 +28,7 @@ export type Props = {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
isSearchable?: boolean
|
isSearchable?: boolean
|
||||||
isClearable?: boolean
|
isClearable?: boolean
|
||||||
|
filterOption?:
|
||||||
|
| (({ label, value, data }: { label: string, value: string, data: OptionType }, search: string) => boolean)
|
||||||
|
| undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { createRelationMap } from './createRelationMap';
|
|||||||
import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
|
import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
|
||||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||||
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
|
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
|
||||||
|
import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>();
|
const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>();
|
||||||
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
|
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false);
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value, validationOptions) => {
|
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||||
return validate(value, { ...validationOptions, required });
|
return validate(value, { ...validationOptions, required });
|
||||||
@@ -322,6 +324,17 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [initialValue, getResults, optionFilters, filterOptions]);
|
}, [initialValue, getResults, optionFilters, filterOptions]);
|
||||||
|
|
||||||
|
// Determine if we should switch to word boundary search
|
||||||
|
useEffect(() => {
|
||||||
|
const relations = Array.isArray(relationTo) ? relationTo : [relationTo];
|
||||||
|
const isIdOnly = relations.reduce((idOnly, relation) => {
|
||||||
|
const collection = collections.find((coll) => coll.slug === relation);
|
||||||
|
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
|
||||||
|
return fieldToSearch === 'id' && idOnly;
|
||||||
|
}, true);
|
||||||
|
setEnableWordBoundarySearch(!isIdOnly);
|
||||||
|
}, [relationTo, collections]);
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
'field-type',
|
'field-type',
|
||||||
baseClass,
|
baseClass,
|
||||||
@@ -392,6 +405,10 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
options={options}
|
options={options}
|
||||||
isMulti={hasMany}
|
isMulti={hasMany}
|
||||||
isSortable={isSortable}
|
isSortable={isSortable}
|
||||||
|
filterOption={enableWordBoundarySearch ? (item, searchFilter) => {
|
||||||
|
const r = wordBoundariesRegex(searchFilter || '');
|
||||||
|
return r.test(item.label);
|
||||||
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{errorLoading && (
|
{errorLoading && (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import mongoose, { SchemaType } from 'mongoose';
|
import mongoose, { SchemaType } from 'mongoose';
|
||||||
import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated';
|
import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated';
|
||||||
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
import { getSchemaTypeOptions } from './getSchemaTypeOptions';
|
||||||
|
import wordBoundariesRegex from '../utilities/wordBoundariesRegex';
|
||||||
|
|
||||||
export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operator: string, val: any): unknown => {
|
export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operator: string, val: any): unknown => {
|
||||||
let formattedValue = val;
|
let formattedValue = val;
|
||||||
@@ -96,12 +97,8 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operator === 'like' && typeof formattedValue === 'string') {
|
if (operator === 'like' && typeof formattedValue === 'string') {
|
||||||
const words = formattedValue.split(' ');
|
const $regex = wordBoundariesRegex(formattedValue)
|
||||||
const regex = words.reduce((pattern, word, i) => {
|
formattedValue = { $regex };
|
||||||
return `${pattern}(?=.*\\b${word}.*\\b)${i + 1 === words.length ? '.+' : ''}`;
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
formattedValue = { $regex: new RegExp(regex), $options: 'i' };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
src/utilities/wordBoundariesRegex.ts
Normal file
7
src/utilities/wordBoundariesRegex.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default (input: string): RegExp => {
|
||||||
|
const words = input.split(' ');
|
||||||
|
const regex = words.reduce((pattern, word, i) => {
|
||||||
|
return `${pattern}(?=.*\\b${word}.*\\b)${i + 1 === words.length ? '.+' : ''}`;
|
||||||
|
}, '');
|
||||||
|
return new RegExp(regex, 'i');
|
||||||
|
};
|
||||||
@@ -156,18 +156,22 @@ export default buildConfig({
|
|||||||
name: 'relation-restricted',
|
name: 'relation-restricted',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { id: relationWithTitleDocId } = await payload.create<RelationWithTitle>({
|
const relationsWithTitle = [];
|
||||||
collection: relationWithTitleSlug,
|
await mapAsync(['relation-title', 'word boundary search'], async (title) => {
|
||||||
data: {
|
const { id } = await payload.create<RelationWithTitle>({
|
||||||
name: 'relation-title',
|
collection: relationWithTitleSlug,
|
||||||
},
|
data: {
|
||||||
|
name: title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
relationsWithTitle.push(id);
|
||||||
});
|
});
|
||||||
await payload.create<FieldsRelationship>({
|
await payload.create<FieldsRelationship>({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
data: {
|
data: {
|
||||||
relationship: relationOneDocId,
|
relationship: relationOneDocId,
|
||||||
relationshipRestricted: restrictedDocId,
|
relationshipRestricted: restrictedDocId,
|
||||||
relationshipWithTitle: relationWithTitleDocId,
|
relationshipWithTitle: relationsWithTitle[0],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await mapAsync([...Array(11)], async () => {
|
await mapAsync([...Array(11)], async () => {
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ describe('fields - relationship', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Doc with useAsTitle for word boundary test
|
||||||
|
await payload.create<RelationWithTitle>({
|
||||||
|
collection: relationWithTitleSlug,
|
||||||
|
data: {
|
||||||
|
name: 'word boundary search',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Add restricted doc as relation
|
// Add restricted doc as relation
|
||||||
docWithExistingRelations = await payload.create<CollectionWithRelationships>({
|
docWithExistingRelations = await payload.create<CollectionWithRelationships>({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
@@ -190,7 +198,25 @@ describe('fields - relationship', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// test.todo('should paginate within the dropdown');
|
// test.todo('should paginate within the dropdown');
|
||||||
// test.todo('should search within the relationship field');
|
|
||||||
|
test('should search within the relationship field', async () => {
|
||||||
|
await page.goto(url.edit(docWithExistingRelations.id));
|
||||||
|
const input = page.locator('#field-relationshipWithTitle input');
|
||||||
|
await input.fill('title');
|
||||||
|
const options = page.locator('#field-relationshipWithTitle .rs__menu .rs__option');
|
||||||
|
await expect(options).toHaveCount(1);
|
||||||
|
|
||||||
|
await input.fill('non-occuring-string');
|
||||||
|
await expect(options).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should search using word boundaries within the relationship field', async () => {
|
||||||
|
await page.goto(url.edit(docWithExistingRelations.id));
|
||||||
|
const input = page.locator('#field-relationshipWithTitle input');
|
||||||
|
await input.fill('word search');
|
||||||
|
const options = page.locator('#field-relationshipWithTitle .rs__menu .rs__option');
|
||||||
|
await expect(options).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should show useAsTitle on relation', async () => {
|
test('should show useAsTitle on relation', async () => {
|
||||||
await page.goto(url.edit(docWithExistingRelations.id));
|
await page.goto(url.edit(docWithExistingRelations.id));
|
||||||
@@ -203,7 +229,7 @@ describe('fields - relationship', () => {
|
|||||||
await field.click({ delay: 100 });
|
await field.click({ delay: 100 });
|
||||||
const options = page.locator('.rs__option');
|
const options = page.locator('.rs__option');
|
||||||
|
|
||||||
await expect(options).toHaveCount(2); // None + 1 Doc
|
await expect(options).toHaveCount(3); // None + 2 Doc
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show id on relation in list view', async () => {
|
test('should show id on relation in list view', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user