feat(searchBarAutocomplete): add description to matched fields to results of the search bar (#13314)

This commit is contained in:
v-tarasevich-blitz-brain 2025-04-24 16:48:53 +03:00 committed by GitHub
parent effff339e5
commit 49bb2b50a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 234 additions and 13 deletions

View File

@ -33,8 +33,8 @@ export default function Matches({ entity, query, displayName, matchedFields }: P
shouldShowInMatchedFieldList(entity.type, field),
);
return getMatchesPrioritized(entity.type, showableFields, 'fieldLabels');
}, [entity, matchedFields]);
return getMatchesPrioritized(entity.type, query ?? '', showableFields, 'fieldLabels');
}, [entity, matchedFields, query]);
const items = useMemo(() => {
return groupedMatchedFields.map((match) => ({

View File

@ -1,7 +1,9 @@
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { removeMarkdown } from '@app/entity/shared/components/styled/StripMarkdownText';
import { MATCH_COLOR, MATCH_COLOR_LEVEL } from '@app/searchV2/autoCompleteV2/constants';
import { getDescriptionSlice, isDescriptionField } from '@app/searchV2/matches/utils';
import { MatchText, Text } from '@src/alchemy-components';
import { MatchesGroupedByFieldName } from '@src/app/search/matches/constants';
import { getMatchedFieldLabel } from '@src/app/search/matches/utils';
@ -25,8 +27,25 @@ export default function Match({ query, entityType, match }: Props) {
() => capitalizeFirstLetterOnly(getMatchedFieldLabel(entityType, match.fieldName)),
[entityType, match.fieldName],
);
// show only the first value
const value = useMemo(() => match.matchedFields?.[0]?.value, [match]);
const value = useMemo(() => {
// show only the first value
const field = match.matchedFields?.[0];
if (field === undefined) return undefined;
// do not show empty matches
if (field.value === '') return undefined;
if (isDescriptionField(field) && query) {
const cleanedValue: string = removeMarkdown(field.value);
// do not show the description if it doesn't include query
if (!cleanedValue.toLowerCase().includes(query.toLocaleLowerCase())) return undefined;
return getDescriptionSlice(cleanedValue, query);
}
return field.value;
}, [match, query]);
if (value === undefined) return null;

View File

@ -21,6 +21,7 @@ export type MatchedFieldName =
export type MatchedFieldConfig = {
name: MatchedFieldName;
groupInto?: MatchedFieldName;
priorityInGroup?: number;
label: string;
showInMatchedFieldList?: boolean;
};
@ -47,12 +48,15 @@ const DEFAULT_MATCHED_FIELD_CONFIG: Array<MatchedFieldConfig> = [
{
name: 'editedDescription',
groupInto: 'description',
priorityInGroup: 1,
label: 'description',
showInMatchedFieldList: true,
},
{
name: 'description',
groupInto: 'description',
label: 'description',
showInMatchedFieldList: true,
},
{
name: 'editedFieldDescriptions',

View File

@ -6,6 +6,7 @@ import {
getMatchedFieldsByNames,
getMatchedFieldsByUrn,
getMatchesPrioritized,
getMatchesPrioritizedByQueryInQueryParams,
isDescriptionField,
isHighlightableEntityField,
shouldShowInMatchedFieldList,
@ -56,11 +57,124 @@ const MOCK_MATCHED_DESCRIPTION_FIELDS = [
},
];
const MOCK_MATCHED_DESCRIPTION_FIELDS_DESCRIPTION_FIRST = [
{
name: 'description',
value: 'description value',
},
{
name: 'editedDescription',
value: 'edited description value',
},
{
name: 'fieldDescriptions',
value: 'field descriptions value',
},
{
name: 'editedFieldDescriptions',
value: 'edited field descriptions value',
},
];
describe('utils', () => {
describe('getMatchPrioritizingPrimary', () => {
describe('getMatchesPrioritized', () => {
it('prioritizes exact match', () => {
const groupedMatches = getMatchesPrioritized(
EntityType.Dataset,
'rainbow',
MOCK_MATCHED_FIELDS,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'fieldPaths',
matchedFields: [
{ name: 'fieldPaths', value: 'rainbow' },
{ name: 'fieldPaths', value: 'rainbows' },
{ name: 'fieldPaths', value: 'rain' },
],
},
{
fieldName: 'fieldDescriptions',
matchedFields: [{ name: 'fieldDescriptions', value: 'rainbow' }],
},
]);
});
it('will accept first contains match', () => {
const groupedMatches = getMatchesPrioritized(EntityType.Dataset, 'bow', MOCK_MATCHED_FIELDS, 'fieldPaths');
expect(groupedMatches).toEqual([
{
fieldName: 'fieldPaths',
matchedFields: [
{ name: 'fieldPaths', value: 'rainbow' },
{ name: 'fieldPaths', value: 'rainbows' },
{ name: 'fieldPaths', value: 'rain' },
],
},
{
fieldName: 'fieldDescriptions',
matchedFields: [{ name: 'fieldDescriptions', value: 'rainbow' }],
},
]);
});
it('will group by field name', () => {
const groupedMatches = getMatchesPrioritized(
EntityType.Dataset,
'',
MOCK_MATCHED_DESCRIPTION_FIELDS,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'description',
matchedFields: [
{ name: 'editedDescription', value: 'edited description value' },
{ name: 'description', value: 'description value' },
],
},
{
fieldName: 'fieldDescriptions',
matchedFields: [
{ name: 'fieldDescriptions', value: 'field descriptions value' },
{ name: 'editedFieldDescriptions', value: 'edited field descriptions value' },
],
},
]);
});
it('will order matches in group', () => {
const groupedMatches = getMatchesPrioritized(
EntityType.Dataset,
'',
MOCK_MATCHED_DESCRIPTION_FIELDS_DESCRIPTION_FIRST,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'description',
matchedFields: [
{ name: 'editedDescription', value: 'edited description value' },
{ name: 'description', value: 'description value' },
],
},
{
fieldName: 'fieldDescriptions',
matchedFields: [
{ name: 'fieldDescriptions', value: 'field descriptions value' },
{ name: 'editedFieldDescriptions', value: 'edited field descriptions value' },
],
},
]);
});
});
describe('getMatchesPrioritizedByQueryInQueryParams', () => {
it('prioritizes exact match', () => {
global.window.location.search = 'query=rainbow';
const groupedMatches = getMatchesPrioritized(EntityType.Dataset, MOCK_MATCHED_FIELDS, 'fieldPaths');
const groupedMatches = getMatchesPrioritizedByQueryInQueryParams(
EntityType.Dataset,
MOCK_MATCHED_FIELDS,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'fieldPaths',
@ -78,7 +192,11 @@ describe('utils', () => {
});
it('will accept first contains match', () => {
global.window.location.search = 'query=bow';
const groupedMatches = getMatchesPrioritized(EntityType.Dataset, MOCK_MATCHED_FIELDS, 'fieldPaths');
const groupedMatches = getMatchesPrioritizedByQueryInQueryParams(
EntityType.Dataset,
MOCK_MATCHED_FIELDS,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'fieldPaths',
@ -96,7 +214,7 @@ describe('utils', () => {
});
it('will group by field name', () => {
global.window.location.search = '';
const groupedMatches = getMatchesPrioritized(
const groupedMatches = getMatchesPrioritizedByQueryInQueryParams(
EntityType.Dataset,
MOCK_MATCHED_DESCRIPTION_FIELDS,
'fieldPaths',
@ -118,6 +236,30 @@ describe('utils', () => {
},
]);
});
it('will order matches in group', () => {
global.window.location.search = '';
const groupedMatches = getMatchesPrioritizedByQueryInQueryParams(
EntityType.Dataset,
MOCK_MATCHED_DESCRIPTION_FIELDS_DESCRIPTION_FIRST,
'fieldPaths',
);
expect(groupedMatches).toEqual([
{
fieldName: 'description',
matchedFields: [
{ name: 'editedDescription', value: 'edited description value' },
{ name: 'description', value: 'description value' },
],
},
{
fieldName: 'fieldDescriptions',
matchedFields: [
{ name: 'fieldDescriptions', value: 'field descriptions value' },
{ name: 'editedFieldDescriptions', value: 'edited field descriptions value' },
],
},
]);
});
});
describe('shouldShowInMatchedFieldList', () => {
@ -130,7 +272,7 @@ describe('utils', () => {
it('should return false if field should not show in matched field list', () => {
const field = { name: 'description', value: 'edited description value' };
const show = shouldShowInMatchedFieldList(EntityType.Dashboard, field);
expect(show).toBe(false);
expect(show).toBe(true);
});
});
@ -231,5 +373,19 @@ describe('utils', () => {
const slice = getDescriptionSlice(text, target);
expect(slice).toBe('... a sample description text valu...');
});
it('should return slice of text surrounding the target (case insensitive)', () => {
const text = 'This is a sample Description text value';
const target = 'descriptioN';
const slice = getDescriptionSlice(text, target);
expect(slice).toBe('... a sample Description text valu...');
});
it('should return slice from the beginning when the target is not found in the text', () => {
const text = 'This is a sample Description text value';
const target = 'novalue';
const slice = getDescriptionSlice(text, target);
expect(slice).toBe('This is a sample...');
});
});
});

View File

@ -82,6 +82,39 @@ function fromQueryGetBestMatch(
return [...exactMatches, ...containedMatches, ...rest];
}
const orderMatchedFieldsByPriorityInGroup = (entityType: EntityType, matchedFields: MatchedField[]) => {
const configs = getFieldConfigsByEntityType(entityType);
return matchedFields
.map((matchedField) => {
const fieldConfig = configs.find((config) => config.name === matchedField.name);
return {
priority: fieldConfig?.priorityInGroup,
matchedField,
};
})
.sort((matchedFieldA, matchedFieldB) => {
// Both have a priority, order by priority
if (matchedFieldA.priority !== undefined && matchedFieldB.priority !== undefined) {
return matchedFieldA.priority - matchedFieldB.priority;
}
// Only 'matchedFieldA' has a priority, it comes first
if (matchedFieldA.priority !== undefined && matchedFieldB.priority === undefined) {
return -1;
}
// Only 'matchedFieldB' has a priority, it comes first
if (matchedFieldA.priority === undefined && matchedFieldB.priority !== undefined) {
return 1;
}
// Neither has a priority, maintain original order
return 0;
})
.map((matchedFieldWithPriority) => matchedFieldWithPriority.matchedField);
};
const getMatchesGroupedByFieldName = (
entityType: EntityType,
matchedFields: Array<MatchedField>,
@ -100,11 +133,21 @@ const getMatchesGroupedByFieldName = (
});
return fieldNames.map((fieldName) => ({
fieldName,
matchedFields: fieldNameToMatches.get(fieldName) ?? [],
matchedFields: orderMatchedFieldsByPriorityInGroup(entityType, fieldNameToMatches.get(fieldName) ?? []),
}));
};
export const getMatchesPrioritized = (
entityType: EntityType,
query: string,
matchedFields: MatchedField[],
prioritizedField: string,
): Array<MatchesGroupedByFieldName> => {
const matches = fromQueryGetBestMatch(matchedFields, query, prioritizedField);
return getMatchesGroupedByFieldName(entityType, matches);
};
export const getMatchesPrioritizedByQueryInQueryParams = (
entityType: EntityType,
matchedFields: MatchedField[],
prioritizedField: string,
@ -112,8 +155,7 @@ export const getMatchesPrioritized = (
const { location } = window;
const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
const query: string = decodeURIComponent(params.query ? (params.query as string) : '');
const matches = fromQueryGetBestMatch(matchedFields, query, prioritizedField);
return getMatchesGroupedByFieldName(entityType, matches);
return getMatchesPrioritized(entityType, query, matchedFields, prioritizedField);
};
export const isHighlightableEntityField = (field: MatchedField) =>
@ -125,7 +167,7 @@ const SURROUNDING_DESCRIPTION_CHARS = 10;
const MAX_DESCRIPTION_CHARS = 50;
export const getDescriptionSlice = (text: string, target: string) => {
const queryIndex = text.indexOf(target);
const queryIndex = text.toLowerCase().indexOf(target.toLowerCase());
const start = Math.max(0, queryIndex - SURROUNDING_DESCRIPTION_CHARS);
const end = Math.min(
start + MAX_DESCRIPTION_CHARS,