mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-26 01:18:20 +00:00
feat(searchBarAutocomplete): add description to matched fields to results of the search bar (#13314)
This commit is contained in:
parent
effff339e5
commit
49bb2b50a5
@ -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) => ({
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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...');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user