Skip to content
This repository was archived by the owner on Sep 9, 2024. It is now read-only.

Commit 8f555d7

Browse files
authored
fix: properly handle search within a single entry (#855)
1 parent 9854afe commit 8f555d7

File tree

4 files changed

+211
-27
lines changed

4 files changed

+211
-27
lines changed

packages/core/src/backend.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { DRAFT_MEDIA_FILES, selectMediaFilePublicPath } from './lib/util/media.u
4545
import { selectCustomPath, slugFromCustomPath } from './lib/util/nested.util';
4646
import { isNullish } from './lib/util/null.util';
4747
import { set } from './lib/util/object.util';
48+
import { fileSearch, sortByScore } from './lib/util/search.util';
4849
import { dateParsers, expandPath, extractTemplateVars } from './lib/widgets/stringTemplate';
4950
import createEntry from './valueObjects/createEntry';
5051

@@ -251,18 +252,6 @@ export function mergeExpandedEntries(entries: (Entry & { field: string })[]): En
251252
return Object.values(merged);
252253
}
253254

254-
export function sortByScore(a: fuzzy.FilterResult<Entry>, b: fuzzy.FilterResult<Entry>) {
255-
if (a.score > b.score) {
256-
return -1;
257-
}
258-
259-
if (a.score < b.score) {
260-
return 1;
261-
}
262-
263-
return 0;
264-
}
265-
266255
interface AuthStore {
267256
retrieve: () => User;
268257
store: (user: User) => void;
@@ -605,9 +594,18 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
605594
file?: string,
606595
limit?: number,
607596
): Promise<SearchQueryResponse> {
608-
let entries = await this.listAllEntries(collection as Collection);
597+
const entries = await this.listAllEntries(collection as Collection);
609598
if (file) {
610-
entries = entries.filter(e => e.slug === file);
599+
let hits = fileSearch(
600+
entries.find(e => e.slug === file),
601+
searchFields,
602+
searchTerm,
603+
);
604+
if (limit !== undefined && limit > 0) {
605+
hits = hits.slice(0, limit);
606+
}
607+
608+
return { query: searchTerm, hits };
611609
}
612610

613611
const expandedEntries = expandSearchEntries(entries, searchFields);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { createMockEntry } from '@staticcms/test/data/entry.mock';
2+
import { fileSearch } from '../search.util';
3+
4+
const rightIcon = {
5+
name: 'right',
6+
type: 'control',
7+
icon: '/src/icons/right.svg',
8+
};
9+
10+
const leftIcon = {
11+
name: 'left',
12+
type: 'control',
13+
icon: '/src/icons/left.svg',
14+
};
15+
16+
const zoomIcon = {
17+
name: 'zoom',
18+
type: 'action',
19+
icon: '/src/icons/zoom.svg',
20+
};
21+
22+
const downloadIcon = {
23+
name: 'download',
24+
type: 'action',
25+
icon: '/src/icons/download.svg',
26+
};
27+
28+
const nestedList = createMockEntry({
29+
data: {
30+
icons: [rightIcon, leftIcon, downloadIcon, zoomIcon],
31+
},
32+
});
33+
34+
describe('search.util', () => {
35+
describe('fileSearch', () => {
36+
it('filters nested array', () => {
37+
expect(fileSearch(nestedList, ['icons.*.name'], 'zoom')).toEqual([
38+
createMockEntry({
39+
data: {
40+
icons: [zoomIcon],
41+
},
42+
raw: nestedList.raw,
43+
}),
44+
]);
45+
46+
expect(fileSearch(nestedList, ['icons.*.name'], 'bad input')).toEqual([
47+
createMockEntry({
48+
data: {
49+
icons: [],
50+
},
51+
raw: nestedList.raw,
52+
}),
53+
]);
54+
});
55+
56+
it('filters nested array on multiple search fields', () => {
57+
expect(fileSearch(nestedList, ['icons.*.name', 'icons.*.type'], 'action')).toEqual([
58+
createMockEntry({
59+
data: {
60+
icons: [downloadIcon, zoomIcon],
61+
},
62+
raw: nestedList.raw,
63+
}),
64+
]);
65+
66+
expect(fileSearch(nestedList, ['icons.*.name', 'icons.*.type'], 'down')).toEqual([
67+
createMockEntry({
68+
data: {
69+
icons: [downloadIcon],
70+
},
71+
raw: nestedList.raw,
72+
}),
73+
]);
74+
});
75+
});
76+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import deepmerge from 'deepmerge';
2+
import * as fuzzy from 'fuzzy';
3+
4+
import type { Entry, ObjectValue, ValueOrNestedValue } from '@staticcms/core/interface';
5+
6+
function filter(
7+
value: ValueOrNestedValue,
8+
remainingPath: string[],
9+
searchTerm: string,
10+
): ValueOrNestedValue {
11+
if (Array.isArray(value)) {
12+
const [nextPath, ...rest] = remainingPath;
13+
if (nextPath !== '*') {
14+
return value;
15+
}
16+
17+
if (rest.length === 1) {
18+
const validOptions = value.filter(e => {
19+
if (!e || Array.isArray(e) || typeof e !== 'object' || e instanceof Date) {
20+
return false;
21+
}
22+
23+
return true;
24+
}) as ObjectValue[];
25+
26+
return validOptions.length > 0
27+
? fuzzy
28+
.filter(searchTerm, validOptions, {
29+
extract: input => {
30+
return String(input[rest[0]]);
31+
},
32+
})
33+
.sort(sortByScore)
34+
.map(f => f.original)
35+
: [];
36+
}
37+
38+
return value.map(childValue => {
39+
return filter(childValue, rest, searchTerm);
40+
});
41+
}
42+
43+
if (value && typeof value === 'object' && !(value instanceof Date)) {
44+
const newValue = { ...value };
45+
46+
const [nextPath, ...rest] = remainingPath;
47+
48+
const childValue = newValue[nextPath];
49+
if (
50+
childValue &&
51+
(Array.isArray(childValue) ||
52+
(typeof childValue === 'object' && !(childValue instanceof Date)))
53+
) {
54+
newValue[nextPath] = filter(childValue, rest, searchTerm);
55+
}
56+
57+
return newValue;
58+
}
59+
60+
return value;
61+
}
62+
63+
export function fileSearch(
64+
entry: Entry | undefined,
65+
searchFields: string[],
66+
searchTerm: string,
67+
): Entry[] {
68+
if (!entry) {
69+
return [];
70+
}
71+
72+
return [
73+
{
74+
...entry,
75+
data: searchFields.reduce(
76+
(acc, searchField) =>
77+
deepmerge(acc, filter(entry.data, searchField.split('.'), searchTerm) as ObjectValue),
78+
{},
79+
),
80+
},
81+
];
82+
}
83+
84+
export function sortByScore<T>(a: fuzzy.FilterResult<T>, b: fuzzy.FilterResult<T>) {
85+
if (a.score > b.score) {
86+
return -1;
87+
}
88+
89+
if (a.score < b.score) {
90+
return 1;
91+
}
92+
93+
return 0;
94+
}

packages/core/src/widgets/relation/RelationControl.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import {
88
expandSearchEntries,
99
getEntryField,
1010
mergeExpandedEntries,
11-
sortByScore,
1211
} from '@staticcms/core/backend';
1312
import Autocomplete from '@staticcms/core/components/common/autocomplete/Autocomplete';
1413
import Field from '@staticcms/core/components/common/field/Field';
1514
import Pill from '@staticcms/core/components/common/pill/Pill';
1615
import CircularProgress from '@staticcms/core/components/common/progress/CircularProgress';
1716
import { isNullish } from '@staticcms/core/lib/util/null.util';
17+
import { fileSearch, sortByScore } from '@staticcms/core/lib/util/search.util';
1818
import { isEmpty } from '@staticcms/core/lib/util/string.util';
1919
import {
2020
addFileTemplateFields,
@@ -28,6 +28,7 @@ import { useAppSelector } from '@staticcms/core/store/hooks';
2828
import type {
2929
Entry,
3030
EntryData,
31+
ObjectValue,
3132
RelationField,
3233
WidgetControlProps,
3334
} from '@staticcms/core/interface';
@@ -189,26 +190,41 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
189190
}
190191

191192
const searchFields = field.search_fields;
193+
const file = field.file;
192194
const limit = field.options_length || DEFAULT_OPTIONS_LIMIT;
193-
const expandedEntries = expandSearchEntries(entries, searchFields);
194-
const hits = fuzzy
195-
.filter(inputValue, expandedEntries, {
196-
extract: entry => {
197-
return getEntryField(entry.field, entry);
198-
},
199-
})
200-
.sort(sortByScore)
201-
.map(f => f.original);
202-
203-
let options = uniqBy(parseHitOptions(mergeExpandedEntries(hits)), o => o.value);
195+
196+
let hits: Entry<ObjectValue>[];
197+
198+
if (file) {
199+
hits = fileSearch(
200+
entries.find(e => e.slug === file),
201+
searchFields,
202+
inputValue,
203+
);
204+
console.log('file', file, 'hits', hits);
205+
} else {
206+
const expandedEntries = expandSearchEntries(entries, searchFields);
207+
hits = mergeExpandedEntries(
208+
fuzzy
209+
.filter(inputValue, expandedEntries, {
210+
extract: entry => {
211+
return getEntryField(entry.field, entry);
212+
},
213+
})
214+
.sort(sortByScore)
215+
.map(f => f.original),
216+
);
217+
}
218+
219+
let options = uniqBy(parseHitOptions(hits), o => o.value);
204220

205221
if (limit !== undefined && limit > 0) {
206222
options = options.slice(0, limit);
207223
}
208224

209225
setOptions(options);
210226
},
211-
[entries, field.options_length, field.search_fields, parseHitOptions],
227+
[entries, field.file, field.options_length, field.search_fields, parseHitOptions],
212228
);
213229

214230
useEffect(() => {

0 commit comments

Comments
 (0)