Skip to content

Commit d94770e

Browse files
committed
feat(UI): add filters for autocomplete
1 parent da4a350 commit d94770e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1823
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Domain, Entity, EntityType } from '@src/types.generated';
2+
3+
/**
4+
* Type guard for domains
5+
*/
6+
export function isItDomainEntity(entity?: Entity | null | undefined): entity is Domain {
7+
return !!entity && entity.type === EntityType.Domain;
8+
}

datahub-web-react/src/app/entityV2/shared/utils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,13 @@ export const tryExtractSubResourceDescription = (entity: Entity, subResource: st
327327
)?.description;
328328
return maybeEditableMetadataDescription?.valueOf() || maybeSchemaMetadataDescription?.valueOf();
329329
};
330+
331+
/**
332+
* Type guard for entity type
333+
*/
334+
export function isItEntityType(entityType?: string): entityType is EntityType {
335+
if (entityType === undefined) return false;
336+
337+
const possibleValues: Array<string> = Array.from(Object.values(EntityType));
338+
return possibleValues.includes(entityType);
339+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Entity, EntityType, Tag } from '@src/types.generated';
2+
3+
/**
4+
* Type guard for tags
5+
*/
6+
export function isItTagEntity(entity?: Entity | null | undefined): entity is Tag {
7+
return !!entity && entity.type === EntityType.Tag;
8+
}

datahub-web-react/src/app/previewV2/BrowsePaths.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function BrowsePaths(props: Props) {
110110
{remainingParentPaths &&
111111
remainingParentPaths.map((container, index) => {
112112
return (
113-
<PlatformText key={`${index}-${container.name}`}>
113+
<PlatformText>
114114
<BrowsePathSection path={container} linksDisabled={linksDisabled} />
115115
{index < remainingParentPaths.length - 1 && <ContextPathSeparator />}
116116
</PlatformText>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, { useMemo } from 'react';
2+
import { useSearchFiltersContext } from './context';
3+
import { FieldName, Filter } from './types';
4+
5+
interface Props {
6+
fieldNames: FieldName[];
7+
hideEmptyFilters?: boolean;
8+
}
9+
10+
export default function FiltersRenderingRunner({ fieldNames, hideEmptyFilters = false }: Props) {
11+
const {
12+
filtersRenderer,
13+
fieldToFacetStateMap,
14+
updateFieldAppliedFilters,
15+
fieldToAppliedFiltersMap,
16+
filtersRegistry,
17+
} = useSearchFiltersContext();
18+
19+
const Renderer = useMemo(() => filtersRenderer, [filtersRenderer]);
20+
21+
const filters: Filter[] = useMemo(() => {
22+
const isEmptyFilter = (fieldName: string) => {
23+
const hasAnyAggregations = (fieldToFacetStateMap.get(fieldName)?.facet?.aggregations?.length ?? 0) > 0;
24+
const hasAnyAppliedFilters = (fieldToAppliedFiltersMap.get(fieldName)?.filters?.length ?? 0) > 0;
25+
26+
return !(hasAnyAggregations || hasAnyAppliedFilters);
27+
};
28+
29+
return fieldNames
30+
.filter((fieldName) => !hideEmptyFilters || !isEmptyFilter(fieldName))
31+
.map((fieldName) => ({
32+
fieldName,
33+
props: {
34+
fieldName,
35+
appliedFilters: fieldToAppliedFiltersMap.get(fieldName),
36+
onUpdate: (values) => updateFieldAppliedFilters(fieldName, values),
37+
facetState: fieldToFacetStateMap.get(fieldName),
38+
},
39+
component: filtersRegistry.get(fieldName) || (() => null),
40+
}));
41+
}, [
42+
fieldNames,
43+
fieldToFacetStateMap,
44+
updateFieldAppliedFilters,
45+
fieldToAppliedFiltersMap,
46+
hideEmptyFilters,
47+
filtersRegistry,
48+
]);
49+
50+
return <Renderer filters={filters} />;
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useMemo, useState } from 'react';
2+
import DynamicFacetsUpdater from './defaults/DefaultFacetsUpdater/DefaultFacetsUpdater';
3+
import FiltersRenderingRunner from './FiltersRenderingRunner';
4+
import { FieldName, FieldToFacetStateMap, FiltersAppliedHandler } from './types';
5+
import { SearchFiltersProvider } from './context';
6+
import defaultFiltersRegistry from './defaults/defaultFiltersRegistry';
7+
8+
interface Props {
9+
query: string;
10+
onFiltersApplied?: FiltersAppliedHandler;
11+
fields: FieldName[];
12+
}
13+
14+
export default function SearchFilters({ query, onFiltersApplied, fields }: Props) {
15+
const [fieldToFacetStateMap, setFieldToFacetStateMap] = useState<FieldToFacetStateMap>(new Map());
16+
17+
const wrappedQuery = useMemo(() => {
18+
const cleanedQuery = query.trim();
19+
if (cleanedQuery.length === 0) return cleanedQuery;
20+
if (cleanedQuery.includes('*')) return cleanedQuery;
21+
if (cleanedQuery.length < 3 && !cleanedQuery.endsWith('*')) return `${cleanedQuery}*`;
22+
return query;
23+
}, [query]);
24+
25+
return (
26+
<SearchFiltersProvider
27+
fields={fields}
28+
fieldToFacetStateMap={fieldToFacetStateMap}
29+
filtersRegistry={defaultFiltersRegistry}
30+
onFiltersApplied={onFiltersApplied}
31+
>
32+
<DynamicFacetsUpdater
33+
fieldNames={fields}
34+
query={wrappedQuery}
35+
onFieldFacetsUpdated={(map) => setFieldToFacetStateMap(map)}
36+
/>
37+
<FiltersRenderingRunner fieldNames={fields} hideEmptyFilters />
38+
</SearchFiltersProvider>
39+
);
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { getUniqueItemsByKey } from '../utils';
2+
3+
describe('getUniqueItemsByKey', () => {
4+
it('returns unique items based on a keyAccessor function', () => {
5+
const items = [
6+
{ id: 1, name: 'Alice' },
7+
{ id: 2, name: 'Bob' },
8+
{ id: 1, name: 'Charlie' },
9+
];
10+
const keyAccessor = (item: { id: number; name: string }) => String(item.id);
11+
12+
const result = getUniqueItemsByKey(items, keyAccessor);
13+
14+
expect(result).toHaveLength(2);
15+
expect(result).toEqual([
16+
{ id: 1, name: 'Charlie' },
17+
{ id: 2, name: 'Bob' },
18+
]);
19+
});
20+
21+
it('handles empty array input', () => {
22+
const items: Array<{ id: number; name: string }> = [];
23+
const keyAccessor = (item: { id: number; name: string }) => String(item.id);
24+
25+
const result = getUniqueItemsByKey(items, keyAccessor);
26+
27+
expect(result).toHaveLength(0);
28+
expect(result).toEqual([]);
29+
});
30+
31+
it('handles all unique items', () => {
32+
const items = [
33+
{ id: 1, name: 'Alice' },
34+
{ id: 2, name: 'Bob' },
35+
{ id: 3, name: 'Charlie' },
36+
];
37+
const keyAccessor = (item: { id: number; name: string }) => String(item.id);
38+
39+
const result = getUniqueItemsByKey(items, keyAccessor);
40+
41+
expect(result).toHaveLength(3);
42+
expect(result).toEqual(items);
43+
});
44+
45+
it('handles non-primitive keys (e.g., objects)', () => {
46+
const items = [
47+
{ id: { subId: 1 }, name: 'Alice' },
48+
{ id: { subId: 2 }, name: 'Bob' },
49+
{ id: { subId: 1 }, name: 'Charlie' },
50+
];
51+
const keyAccessor = (item: { id: { subId: number }; name: string }) => String(item.id.subId);
52+
53+
const result = getUniqueItemsByKey(items, keyAccessor);
54+
55+
expect(result).toHaveLength(2);
56+
expect(result).toEqual([
57+
{ id: { subId: 1 }, name: 'Charlie' },
58+
{ id: { subId: 2 }, name: 'Bob' },
59+
]);
60+
});
61+
62+
it('handles mixed primitive and object keys', () => {
63+
const items = [
64+
{ id: 1, name: 'Alice' },
65+
{ id: 'two', name: 'Bob' },
66+
{ id: 1, name: 'Charlie' },
67+
];
68+
const keyAccessor = (item: { id: number | string; name: string }) => String(item.id);
69+
70+
const result = getUniqueItemsByKey(items, keyAccessor);
71+
72+
expect(result).toHaveLength(2);
73+
expect(result).toEqual([
74+
{ id: 1, name: 'Charlie' },
75+
{ id: 'two', name: 'Bob' },
76+
]);
77+
});
78+
79+
it('handles null or undefined values in the array', () => {
80+
const items = [{ id: 1, name: 'Alice' }, null, undefined, { id: 1, name: 'Charlie' }];
81+
const keyAccessor = (item: { id: number; name: string } | null | undefined) =>
82+
item?.id !== undefined ? String(item.id) : '';
83+
84+
const result = getUniqueItemsByKey(items, keyAccessor);
85+
86+
expect(result).toHaveLength(2);
87+
expect(result).toEqual([{ id: 1, name: 'Charlie' }, undefined]);
88+
});
89+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { getUniqueItemsByKeyFromArrrays } from '../utils';
2+
3+
describe('getUniqueItemsByKeyFromArrrays', () => {
4+
it('deduplicates items across multiple arrays based on a keyAccessor', () => {
5+
const arrays = [
6+
[
7+
{ id: 1, name: 'Alice' },
8+
{ id: 2, name: 'Bob' },
9+
],
10+
[
11+
{ id: 1, name: 'Charlie' },
12+
{ id: 3, name: 'David' },
13+
],
14+
];
15+
const keyAccessor = (item: { id: number; name: string }) => item.id;
16+
17+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
18+
19+
expect(result).toHaveLength(3);
20+
expect(result).toEqual([
21+
{ id: 1, name: 'Charlie' },
22+
{ id: 2, name: 'Bob' },
23+
{ id: 3, name: 'David' },
24+
]);
25+
});
26+
27+
it('handles empty arrays input', () => {
28+
const arrays: Array<Array<{ id: number; name: string }>> = [[], []];
29+
const keyAccessor = (item: { id: number; name: string }) => item.id;
30+
31+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
32+
33+
expect(result).toHaveLength(0);
34+
expect(result).toEqual([]);
35+
});
36+
37+
it('handles arrays with all unique items', () => {
38+
const arrays = [
39+
[
40+
{ id: 1, name: 'Alice' },
41+
{ id: 2, name: 'Bob' },
42+
],
43+
[
44+
{ id: 3, name: 'Charlie' },
45+
{ id: 4, name: 'David' },
46+
],
47+
];
48+
const keyAccessor = (item: { id: number; name: string }) => item.id;
49+
50+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
51+
52+
expect(result).toHaveLength(4);
53+
expect(result).toEqual([
54+
{ id: 1, name: 'Alice' },
55+
{ id: 2, name: 'Bob' },
56+
{ id: 3, name: 'Charlie' },
57+
{ id: 4, name: 'David' },
58+
]);
59+
});
60+
61+
it('handles non-primitive keys (e.g., objects)', () => {
62+
const arrays = [
63+
[
64+
{ id: { subId: 1 }, name: 'Alice' },
65+
{ id: { subId: 2 }, name: 'Bob' },
66+
],
67+
[
68+
{ id: { subId: 1 }, name: 'Charlie' },
69+
{ id: { subId: 3 }, name: 'David' },
70+
],
71+
];
72+
const keyAccessor = (item: { id: { subId: number }; name: string }) => item.id.subId;
73+
74+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
75+
76+
expect(result).toHaveLength(3);
77+
expect(result).toEqual([
78+
{ id: { subId: 1 }, name: 'Charlie' },
79+
{ id: { subId: 2 }, name: 'Bob' },
80+
{ id: { subId: 3 }, name: 'David' },
81+
]);
82+
});
83+
84+
it('handles mixed primitive and object keys', () => {
85+
const arrays = [
86+
[
87+
{ id: 1, name: 'Alice' },
88+
{ id: 'two', name: 'Bob' },
89+
],
90+
[
91+
{ id: 1, name: 'Charlie' },
92+
{ id: 'three', name: 'David' },
93+
],
94+
];
95+
const keyAccessor = (item: { id: number | string; name: string }) => item.id;
96+
97+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
98+
99+
expect(result).toHaveLength(3);
100+
expect(result).toEqual([
101+
{ id: 1, name: 'Charlie' },
102+
{ id: 'two', name: 'Bob' },
103+
{ id: 'three', name: 'David' },
104+
]);
105+
});
106+
107+
it('handles null or undefined values in arrays', () => {
108+
const arrays = [
109+
[{ id: 1, name: 'Alice' }, null, undefined],
110+
[{ id: 1, name: 'Charlie' }, null, undefined],
111+
];
112+
const keyAccessor = (item: { id: number; name: string } | null | undefined) =>
113+
item?.id !== undefined ? item.id : '';
114+
115+
const result = getUniqueItemsByKeyFromArrrays(arrays, keyAccessor);
116+
117+
expect(result).toHaveLength(2);
118+
expect(result).toEqual([{ id: 1, name: 'Charlie' }, undefined]);
119+
});
120+
121+
it('handles no keyAccessor (uses item as key)', () => {
122+
const arrays = [
123+
['a', 'b'],
124+
['a', 'c'],
125+
];
126+
127+
const result = getUniqueItemsByKeyFromArrrays(arrays);
128+
129+
expect(result).toHaveLength(3);
130+
expect(result).toEqual(['a', 'b', 'c']);
131+
});
132+
});

0 commit comments

Comments
 (0)