@miketromba/issy-core 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/autocomplete.ts +168 -164
- package/src/lib/browser.ts +5 -5
- package/src/lib/formatDate.ts +49 -49
- package/src/lib/index.ts +42 -42
- package/src/lib/issues.ts +336 -332
- package/src/lib/query-parser.ts +70 -70
- package/src/lib/search.ts +295 -288
- package/src/lib/types.ts +34 -34
package/src/lib/search.ts
CHANGED
|
@@ -8,22 +8,22 @@ import type { Issue, IssueFilters } from './types'
|
|
|
8
8
|
|
|
9
9
|
// Fuse.js configuration for fuzzy search
|
|
10
10
|
const FUSE_OPTIONS: IFuseOptions<Issue> = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
keys: [
|
|
12
|
+
{ name: 'frontmatter.title', weight: 1.0 },
|
|
13
|
+
{ name: 'frontmatter.description', weight: 0.7 },
|
|
14
|
+
{ name: 'frontmatter.labels', weight: 0.5 },
|
|
15
|
+
{ name: 'content', weight: 0.3 }
|
|
16
|
+
],
|
|
17
|
+
threshold: 0.4, // 0 = exact match, 1 = match anything
|
|
18
|
+
ignoreLocation: true, // search entire string, not just beginning
|
|
19
|
+
includeScore: true
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Create a Fuse.js instance for searching issues
|
|
24
24
|
*/
|
|
25
25
|
export function createSearchIndex(issues: Issue[]): Fuse<Issue> {
|
|
26
|
-
|
|
26
|
+
return new Fuse(issues, FUSE_OPTIONS)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -31,33 +31,36 @@ export function createSearchIndex(issues: Issue[]): Fuse<Issue> {
|
|
|
31
31
|
* Returns issues sorted by relevance
|
|
32
32
|
*/
|
|
33
33
|
export function searchIssues(fuse: Fuse<Issue>, query: string): Issue[] {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (!query.trim()) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const results = fuse.search(query)
|
|
39
|
+
return results.map(r => r.item)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Filter issues by frontmatter fields
|
|
44
44
|
*/
|
|
45
45
|
export function filterIssues(issues: Issue[], filters: IssueFilters): Issue[] {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
return issues.filter(issue => {
|
|
47
|
+
if (filters.status && issue.frontmatter.status !== filters.status) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
filters.priority &&
|
|
52
|
+
issue.frontmatter.priority !== filters.priority
|
|
53
|
+
) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
if (filters.scope && issue.frontmatter.scope !== filters.scope) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
if (filters.type && issue.frontmatter.type !== filters.type) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return true
|
|
63
|
+
})
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/**
|
|
@@ -66,58 +69,60 @@ export function filterIssues(issues: Issue[], filters: IssueFilters): Issue[] {
|
|
|
66
69
|
* ID matches (exact prefix) are ranked first
|
|
67
70
|
*/
|
|
68
71
|
export function filterAndSearchIssues(
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
issues: Issue[],
|
|
73
|
+
filters: IssueFilters
|
|
71
74
|
): Issue[] {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
75
|
+
// First apply dropdown filters
|
|
76
|
+
let result = filterIssues(issues, filters)
|
|
77
|
+
|
|
78
|
+
// Then apply fuzzy search if there's a search term
|
|
79
|
+
if (filters.search?.trim()) {
|
|
80
|
+
const query = filters.search.trim()
|
|
81
|
+
|
|
82
|
+
// Check for ID matches first (exact prefix match)
|
|
83
|
+
// Supports: "1" -> "0001", "01" -> "0001", "0001" -> "0001"
|
|
84
|
+
const idMatches: Issue[] = []
|
|
85
|
+
const nonIdMatches: Issue[] = []
|
|
86
|
+
|
|
87
|
+
const normalizedQuery = query.replace(/^0+/, '') // Remove leading zeros
|
|
88
|
+
|
|
89
|
+
for (const issue of result) {
|
|
90
|
+
const normalizedId = issue.id.replace(/^0+/, '')
|
|
91
|
+
if (
|
|
92
|
+
normalizedId.startsWith(normalizedQuery) ||
|
|
93
|
+
issue.id.startsWith(query)
|
|
94
|
+
) {
|
|
95
|
+
idMatches.push(issue)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Now do fuzzy search
|
|
100
|
+
const fuse = createSearchIndex(issues)
|
|
101
|
+
const searchResults = fuse.search(query)
|
|
102
|
+
const matchedIds = new Set(searchResults.map(r => r.item.id))
|
|
103
|
+
|
|
104
|
+
// Get fuzzy matches that aren't already ID matches
|
|
105
|
+
const idMatchSet = new Set(idMatches.map(i => i.id))
|
|
106
|
+
for (const issue of result) {
|
|
107
|
+
if (!idMatchSet.has(issue.id) && matchedIds.has(issue.id)) {
|
|
108
|
+
nonIdMatches.push(issue)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sort fuzzy matches by relevance
|
|
113
|
+
nonIdMatches.sort((a, b) => {
|
|
114
|
+
const aScore =
|
|
115
|
+
searchResults.find(r => r.item.id === a.id)?.score ?? 1
|
|
116
|
+
const bScore =
|
|
117
|
+
searchResults.find(r => r.item.id === b.id)?.score ?? 1
|
|
118
|
+
return aScore - bScore // Lower score = better match
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// ID matches first, then fuzzy matches
|
|
122
|
+
result = [...idMatches, ...nonIdMatches]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
/**
|
|
@@ -127,88 +132,88 @@ export function filterAndSearchIssues(
|
|
|
127
132
|
* @param sortBy - Sort option: "roadmap", "priority", "scope", "created", "updated", or "id"
|
|
128
133
|
*/
|
|
129
134
|
function sortIssues(issues: Issue[], sortBy: string): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
135
|
+
const sortOption = sortBy.toLowerCase()
|
|
136
|
+
|
|
137
|
+
if (sortOption === 'roadmap') {
|
|
138
|
+
issues.sort((a, b) => {
|
|
139
|
+
const orderA = a.frontmatter.order
|
|
140
|
+
const orderB = b.frontmatter.order
|
|
141
|
+
if (orderA && orderB)
|
|
142
|
+
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0
|
|
143
|
+
if (orderA && !orderB) return -1
|
|
144
|
+
if (!orderA && orderB) return 1
|
|
145
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
146
|
+
})
|
|
147
|
+
} else if (sortOption === 'priority') {
|
|
148
|
+
// Sort by priority (high → medium → low), then by ID (newest first)
|
|
149
|
+
const priorityOrder: Record<string, number> = {
|
|
150
|
+
high: 0,
|
|
151
|
+
medium: 1,
|
|
152
|
+
low: 2
|
|
153
|
+
}
|
|
154
|
+
issues.sort((a, b) => {
|
|
155
|
+
const priorityA = priorityOrder[a.frontmatter.priority] ?? 999
|
|
156
|
+
const priorityB = priorityOrder[b.frontmatter.priority] ?? 999
|
|
157
|
+
if (priorityA !== priorityB) return priorityA - priorityB
|
|
158
|
+
return b.id.localeCompare(a.id) // newest first within priority
|
|
159
|
+
})
|
|
160
|
+
} else if (sortOption === 'scope') {
|
|
161
|
+
// Sort by scope (small → medium → large), then by ID (newest first)
|
|
162
|
+
// Issues without scope go to the end
|
|
163
|
+
const scopeOrder: Record<string, number> = {
|
|
164
|
+
small: 0,
|
|
165
|
+
medium: 1,
|
|
166
|
+
large: 2
|
|
167
|
+
}
|
|
168
|
+
issues.sort((a, b) => {
|
|
169
|
+
const scopeA = a.frontmatter.scope
|
|
170
|
+
? (scopeOrder[a.frontmatter.scope] ?? 99)
|
|
171
|
+
: 99
|
|
172
|
+
const scopeB = b.frontmatter.scope
|
|
173
|
+
? (scopeOrder[b.frontmatter.scope] ?? 99)
|
|
174
|
+
: 99
|
|
175
|
+
if (scopeA !== scopeB) return scopeA - scopeB
|
|
176
|
+
return b.id.localeCompare(a.id) // newest first within scope
|
|
177
|
+
})
|
|
178
|
+
} else if (sortOption === 'created') {
|
|
179
|
+
// Sort by creation date (newest first)
|
|
180
|
+
issues.sort((a, b) => {
|
|
181
|
+
const dateA = a.frontmatter.created || ''
|
|
182
|
+
const dateB = b.frontmatter.created || ''
|
|
183
|
+
if (dateA !== dateB) return dateB.localeCompare(dateA) // newest first
|
|
184
|
+
return b.id.localeCompare(a.id) // fallback to ID
|
|
185
|
+
})
|
|
186
|
+
} else if (sortOption === 'created-asc') {
|
|
187
|
+
// Sort by creation date (oldest first)
|
|
188
|
+
issues.sort((a, b) => {
|
|
189
|
+
const dateA = a.frontmatter.created || ''
|
|
190
|
+
const dateB = b.frontmatter.created || ''
|
|
191
|
+
if (dateA !== dateB) return dateA.localeCompare(dateB) // oldest first
|
|
192
|
+
return a.id.localeCompare(b.id) // fallback to ID
|
|
193
|
+
})
|
|
194
|
+
} else if (sortOption === 'updated') {
|
|
195
|
+
// Sort by last updated date (most recent first), fallback to created if no updated
|
|
196
|
+
issues.sort((a, b) => {
|
|
197
|
+
const dateA = a.frontmatter.updated || a.frontmatter.created || ''
|
|
198
|
+
const dateB = b.frontmatter.updated || b.frontmatter.created || ''
|
|
199
|
+
if (dateA !== dateB) return dateB.localeCompare(dateA) // newest first
|
|
200
|
+
return b.id.localeCompare(a.id) // fallback to ID
|
|
201
|
+
})
|
|
202
|
+
} else if (sortOption === 'id') {
|
|
203
|
+
// Sort by issue ID (newest first)
|
|
204
|
+
issues.sort((a, b) => b.id.localeCompare(a.id))
|
|
205
|
+
} else {
|
|
206
|
+
// Invalid sort option - default to roadmap order
|
|
207
|
+
issues.sort((a, b) => {
|
|
208
|
+
const orderA = a.frontmatter.order
|
|
209
|
+
const orderB = b.frontmatter.order
|
|
210
|
+
if (orderA && orderB)
|
|
211
|
+
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0
|
|
212
|
+
if (orderA && !orderB) return -1
|
|
213
|
+
if (!orderA && orderB) return 1
|
|
214
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
215
|
+
})
|
|
216
|
+
}
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
/**
|
|
@@ -259,129 +264,131 @@ function sortIssues(issues: Issue[], sortBy: string): void {
|
|
|
259
264
|
* // Returns open issues matching "dashboard" via fuzzy search, sorted by relevance
|
|
260
265
|
*/
|
|
261
266
|
export function filterByQuery(issues: Issue[], query: string): Issue[] {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
267
|
+
const parsed = parseQuery(query)
|
|
268
|
+
|
|
269
|
+
// First, filter by qualifiers
|
|
270
|
+
let result = issues.filter(issue => {
|
|
271
|
+
// is: qualifier (maps to status)
|
|
272
|
+
if (parsed.qualifiers.is) {
|
|
273
|
+
const statusValue = parsed.qualifiers.is.toLowerCase()
|
|
274
|
+
// Only filter if value is valid (open or closed)
|
|
275
|
+
if (statusValue === 'open' || statusValue === 'closed') {
|
|
276
|
+
if (issue.frontmatter.status !== statusValue) {
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Invalid values are ignored (issue passes filter)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// priority: qualifier
|
|
284
|
+
if (parsed.qualifiers.priority) {
|
|
285
|
+
const priorityValue = parsed.qualifiers.priority.toLowerCase()
|
|
286
|
+
// Only filter if value is valid (high, medium, or low)
|
|
287
|
+
if (
|
|
288
|
+
priorityValue === 'high' ||
|
|
289
|
+
priorityValue === 'medium' ||
|
|
290
|
+
priorityValue === 'low'
|
|
291
|
+
) {
|
|
292
|
+
if (issue.frontmatter.priority !== priorityValue) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Invalid values are ignored (issue passes filter)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// scope: qualifier
|
|
300
|
+
if (parsed.qualifiers.scope) {
|
|
301
|
+
const scopeValue = parsed.qualifiers.scope.toLowerCase()
|
|
302
|
+
// Only filter if value is valid (small, medium, or large)
|
|
303
|
+
if (
|
|
304
|
+
scopeValue === 'small' ||
|
|
305
|
+
scopeValue === 'medium' ||
|
|
306
|
+
scopeValue === 'large'
|
|
307
|
+
) {
|
|
308
|
+
if (issue.frontmatter.scope !== scopeValue) {
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Invalid values are ignored (issue passes filter)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// type: qualifier
|
|
316
|
+
if (parsed.qualifiers.type) {
|
|
317
|
+
const typeValue = parsed.qualifiers.type.toLowerCase()
|
|
318
|
+
// Only filter if value is valid (bug or improvement)
|
|
319
|
+
if (typeValue === 'bug' || typeValue === 'improvement') {
|
|
320
|
+
if (issue.frontmatter.type !== typeValue) {
|
|
321
|
+
return false
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Invalid values are ignored (issue passes filter)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// label: qualifier
|
|
328
|
+
if (parsed.qualifiers.label) {
|
|
329
|
+
const labelQuery = parsed.qualifiers.label.toLowerCase()
|
|
330
|
+
const issueLabels = (issue.frontmatter.labels || '').toLowerCase()
|
|
331
|
+
// Check if the label query appears in the issue's labels (partial match)
|
|
332
|
+
if (!issueLabels.includes(labelQuery)) {
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return true
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Apply sorting if no search text (search text uses relevance sorting)
|
|
341
|
+
if (!parsed.searchText.trim()) {
|
|
342
|
+
const sortBy = parsed.qualifiers.sort?.toLowerCase() || 'roadmap'
|
|
343
|
+
sortIssues(result, sortBy)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// If there's search text, apply fuzzy search
|
|
347
|
+
if (parsed.searchText.trim()) {
|
|
348
|
+
const searchQuery = parsed.searchText.trim()
|
|
349
|
+
|
|
350
|
+
// Check for ID matches first (exact prefix match)
|
|
351
|
+
// Supports: "1" -> "0001", "01" -> "0001", "0001" -> "0001"
|
|
352
|
+
const idMatches: Issue[] = []
|
|
353
|
+
const nonIdMatches: Issue[] = []
|
|
354
|
+
|
|
355
|
+
const normalizedQuery = searchQuery.replace(/^0+/, '') // Remove leading zeros
|
|
356
|
+
|
|
357
|
+
for (const issue of result) {
|
|
358
|
+
const normalizedId = issue.id.replace(/^0+/, '')
|
|
359
|
+
if (
|
|
360
|
+
normalizedId.startsWith(normalizedQuery) ||
|
|
361
|
+
issue.id.startsWith(searchQuery)
|
|
362
|
+
) {
|
|
363
|
+
idMatches.push(issue)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Now do fuzzy search on the filtered results
|
|
368
|
+
const fuse = createSearchIndex(result)
|
|
369
|
+
const searchResults = fuse.search(searchQuery)
|
|
370
|
+
const matchedIds = new Set(searchResults.map(r => r.item.id))
|
|
371
|
+
|
|
372
|
+
// Get fuzzy matches that aren't already ID matches
|
|
373
|
+
const idMatchSet = new Set(idMatches.map(i => i.id))
|
|
374
|
+
for (const issue of result) {
|
|
375
|
+
if (!idMatchSet.has(issue.id) && matchedIds.has(issue.id)) {
|
|
376
|
+
nonIdMatches.push(issue)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Sort fuzzy matches by relevance
|
|
381
|
+
nonIdMatches.sort((a, b) => {
|
|
382
|
+
const aScore =
|
|
383
|
+
searchResults.find(r => r.item.id === a.id)?.score ?? 1
|
|
384
|
+
const bScore =
|
|
385
|
+
searchResults.find(r => r.item.id === b.id)?.score ?? 1
|
|
386
|
+
return aScore - bScore // Lower score = better match
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ID matches first, then fuzzy matches
|
|
390
|
+
result = [...idMatches, ...nonIdMatches]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return result
|
|
387
394
|
}
|