wuffle 0.70.1 → 0.71.0
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/lib/apps/search/Search.js +67 -38
- package/lib/util/search.js +111 -20
- package/package.json +7 -8
- package/public/bundle.js +1 -1
- package/public/bundle.js.map +1 -1
- package/LICENSE +0 -21
|
@@ -9,6 +9,11 @@ const CHILD_LINK_TYPES = {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef { { defaultFilter?: string } } SearchConfig
|
|
12
|
+
* @typedef { import('../../util/search.js').SearchTerm } SearchTerm
|
|
13
|
+
*
|
|
14
|
+
* @typedef { import('../../types.js').GitHubUser } GitHubUser
|
|
15
|
+
*
|
|
16
|
+
* @typedef { (issue: any) => boolean } FilterFn
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
/**
|
|
@@ -318,71 +323,95 @@ export default function Search(config, logger, store) {
|
|
|
318
323
|
};
|
|
319
324
|
}
|
|
320
325
|
|
|
321
|
-
|
|
326
|
+
/**
|
|
327
|
+
* @param {SearchTerm} term
|
|
328
|
+
* @param {any} user
|
|
329
|
+
*
|
|
330
|
+
* @return {FilterFn}
|
|
331
|
+
*/
|
|
332
|
+
function buildTermFn(term, user) {
|
|
333
|
+
let {
|
|
334
|
+
qualifier,
|
|
335
|
+
value,
|
|
336
|
+
negated,
|
|
337
|
+
exact
|
|
338
|
+
} = term;
|
|
322
339
|
|
|
323
|
-
const
|
|
340
|
+
const wrap = (fn) => negated ? ((issue) => !fn(issue)) : fn;
|
|
324
341
|
|
|
325
|
-
|
|
326
|
-
let {
|
|
327
|
-
qualifier,
|
|
328
|
-
value,
|
|
329
|
-
negated,
|
|
330
|
-
exact
|
|
331
|
-
} = term;
|
|
342
|
+
if ([ 'or', 'and' ].includes(qualifier)) {
|
|
332
343
|
|
|
333
|
-
|
|
334
|
-
return noopFilter();
|
|
335
|
-
}
|
|
344
|
+
const childTerms = /** @type {SearchTerm[]} */ (value);
|
|
336
345
|
|
|
337
|
-
|
|
338
|
-
if (!user) {
|
|
339
|
-
return noneFilter();
|
|
340
|
-
}
|
|
346
|
+
const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
|
|
341
347
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
return wrap(
|
|
349
|
+
(issue) => filterFns[qualifier === 'or' ? 'some' : 'every'](
|
|
350
|
+
filterFn => filterFn(issue)
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
345
354
|
|
|
346
|
-
|
|
355
|
+
if (!value) {
|
|
356
|
+
return noopFilter();
|
|
357
|
+
}
|
|
347
358
|
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
if (value === '@me') {
|
|
360
|
+
if (!user) {
|
|
361
|
+
return noneFilter();
|
|
350
362
|
}
|
|
351
363
|
|
|
352
|
-
|
|
364
|
+
value = user.login;
|
|
365
|
+
exact = true;
|
|
366
|
+
}
|
|
353
367
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
368
|
+
const factoryFn = filters[qualifier];
|
|
369
|
+
|
|
370
|
+
if (!factoryFn) {
|
|
371
|
+
return noopFilter();
|
|
372
|
+
}
|
|
359
373
|
|
|
360
|
-
|
|
361
|
-
|
|
374
|
+
return wrap(factoryFn(value, exact));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function buildFilterFn(search, user) {
|
|
378
|
+
|
|
379
|
+
const term = search && parseSearch(search);
|
|
380
|
+
|
|
381
|
+
if (!term) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return buildTermFn(term);
|
|
362
386
|
}
|
|
363
387
|
|
|
364
388
|
/**
|
|
365
389
|
* Retrieve a filter function from the given search string.
|
|
366
390
|
*
|
|
367
391
|
* @param {string} search
|
|
368
|
-
* @param {
|
|
392
|
+
* @param {GitHubUser} [user]
|
|
369
393
|
*
|
|
370
|
-
* @return {
|
|
394
|
+
* @return {FilterFn}
|
|
371
395
|
*/
|
|
372
396
|
function getSearchFilter(search, user) {
|
|
373
397
|
|
|
374
|
-
const
|
|
398
|
+
const filterFn = buildFilterFn(search, user);
|
|
375
399
|
|
|
376
|
-
const
|
|
400
|
+
const ignoreFilterFn = buildFilterFn(config.defaultFilter, user);
|
|
377
401
|
|
|
378
402
|
return function(issue) {
|
|
379
403
|
try {
|
|
380
|
-
if (
|
|
381
|
-
return
|
|
382
|
-
}
|
|
404
|
+
if (filterFn) {
|
|
405
|
+
return filterFn(issue);
|
|
406
|
+
}
|
|
383
407
|
|
|
384
|
-
|
|
408
|
+
if (ignoreFilterFn) {
|
|
409
|
+
return ignoreFilterFn(issue);
|
|
385
410
|
}
|
|
411
|
+
|
|
412
|
+
// no user search, no ignore filter,
|
|
413
|
+
// show all issues
|
|
414
|
+
return true;
|
|
386
415
|
} catch (err) {
|
|
387
416
|
log.warn({ issue: issueIdent(issue), err }, 'filter failed');
|
|
388
417
|
|
package/lib/util/search.js
CHANGED
|
@@ -54,58 +54,149 @@ function startOfDay(time) {
|
|
|
54
54
|
return date.getTime();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @typedef { {
|
|
59
|
+
* qualifier: string,
|
|
60
|
+
* exact?: boolean,
|
|
61
|
+
* negated?: boolean,
|
|
62
|
+
* value?: string|SearchTerm[],
|
|
63
|
+
* } } SearchTerm
|
|
64
|
+
*/
|
|
65
|
+
|
|
57
66
|
/**
|
|
58
67
|
* @param {string} str
|
|
59
68
|
*
|
|
60
|
-
* @return {
|
|
61
|
-
* qualifier: string,
|
|
62
|
-
* value: string|undefined,
|
|
63
|
-
* exact: boolean,
|
|
64
|
-
* negated?: boolean
|
|
65
|
-
* }[] }
|
|
69
|
+
* @return {SearchTerm}
|
|
66
70
|
*/
|
|
67
71
|
export function parseSearch(str) {
|
|
68
72
|
|
|
69
|
-
const regexp = /(
|
|
73
|
+
const regexp = /(\()|(\))|(OR)\s*|(AND)\s|([-!]|NOT\s+)?(?:"([^"]+)"|([\w#/&]+)(?:(:)(?:([\w-#/&@<>=.]+)|"([^"]+)")?)?)/g;
|
|
70
74
|
|
|
71
|
-
const
|
|
75
|
+
const stack = [ {
|
|
76
|
+
qualifier: 'and',
|
|
77
|
+
value: /** @type {SearchTerm[]} */ ([])
|
|
78
|
+
} ];
|
|
72
79
|
|
|
73
80
|
let match;
|
|
74
81
|
|
|
75
82
|
while ((match = regexp.exec(str))) {
|
|
76
83
|
|
|
84
|
+
const term = stack[stack.length - 1];
|
|
85
|
+
|
|
77
86
|
const [
|
|
78
87
|
_,
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
openGroup,
|
|
89
|
+
closeGroup,
|
|
90
|
+
or,
|
|
91
|
+
and,
|
|
81
92
|
negated,
|
|
93
|
+
textEscaped,
|
|
94
|
+
text,
|
|
82
95
|
qualifier,
|
|
83
96
|
qualifierText,
|
|
84
97
|
qualifierTextEscaped
|
|
85
98
|
] = match;
|
|
86
99
|
|
|
100
|
+
if (openGroup) {
|
|
101
|
+
const groupTerm = {
|
|
102
|
+
qualifier: 'and',
|
|
103
|
+
value: [],
|
|
104
|
+
group: true
|
|
105
|
+
};
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
stack.push(groupTerm);
|
|
108
|
+
term.value.push(groupTerm);
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (closeGroup) {
|
|
114
|
+
|
|
115
|
+
let lastTerm;
|
|
116
|
+
|
|
117
|
+
do {
|
|
118
|
+
lastTerm = stack.pop();
|
|
119
|
+
} while (!lastTerm.group);
|
|
120
|
+
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (and) {
|
|
125
|
+
|
|
126
|
+
// default semantics
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (or) {
|
|
131
|
+
const nextTerm = {
|
|
132
|
+
qualifier: 'and',
|
|
133
|
+
value: []
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const groupTerm = {
|
|
137
|
+
qualifier: 'or',
|
|
138
|
+
value: [
|
|
139
|
+
{
|
|
140
|
+
qualifier: 'and',
|
|
141
|
+
value: term.value.slice()
|
|
142
|
+
},
|
|
143
|
+
nextTerm
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// replace existing terms with <OR> term
|
|
148
|
+
term.value.length = 0;
|
|
149
|
+
term.value.push(groupTerm);
|
|
150
|
+
|
|
151
|
+
stack.push(nextTerm);
|
|
152
|
+
|
|
153
|
+
continue;
|
|
96
154
|
}
|
|
97
155
|
|
|
156
|
+
const textValue = text || textEscaped;
|
|
98
157
|
const qualifierValue = qualifierText || qualifierTextEscaped;
|
|
99
158
|
|
|
100
159
|
if (qualifier) {
|
|
101
|
-
|
|
102
|
-
qualifier,
|
|
160
|
+
term.value.push({
|
|
161
|
+
qualifier: textValue,
|
|
103
162
|
value: qualifierValue,
|
|
104
163
|
negated: !!negated,
|
|
105
164
|
exact: !!qualifierTextEscaped
|
|
106
165
|
});
|
|
166
|
+
} else {
|
|
167
|
+
term.value.push({
|
|
168
|
+
qualifier: 'text',
|
|
169
|
+
value: textValue,
|
|
170
|
+
exact: !!textEscaped,
|
|
171
|
+
negated: !!negated
|
|
172
|
+
});
|
|
107
173
|
}
|
|
108
174
|
}
|
|
109
175
|
|
|
110
|
-
return
|
|
176
|
+
return collapseSearch(stack[0]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {SearchTerm} term
|
|
181
|
+
*
|
|
182
|
+
* @return {SearchTerm}
|
|
183
|
+
*/
|
|
184
|
+
export function collapseSearch(term) {
|
|
185
|
+
|
|
186
|
+
if (![ 'and', 'or' ].includes(term.qualifier)) {
|
|
187
|
+
return term;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const nestedTerms = /** @type {SearchTerm[]} */ (term.value);
|
|
191
|
+
|
|
192
|
+
const nestedTermsCollapsed = nestedTerms.map(collapseSearch);
|
|
193
|
+
|
|
194
|
+
if (term.qualifier === 'and' && nestedTermsCollapsed.length === 1) {
|
|
195
|
+
return nestedTermsCollapsed[0];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...term,
|
|
200
|
+
value: nestedTermsCollapsed
|
|
201
|
+
};
|
|
111
202
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wuffle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "A multi-repository task board for GitHub issues",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nico Rehwaldt",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"auto-test": "npm test -- --watch"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@aws-sdk/client-s3": "^3.
|
|
45
|
+
"@aws-sdk/client-s3": "^3.1037.0",
|
|
46
46
|
"async-didi": "^1.0.0",
|
|
47
47
|
"body-parser": "^2.2.2",
|
|
48
48
|
"compression": "^1.8.1",
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"@graphql-eslint/eslint-plugin": "^4.4.0",
|
|
60
60
|
"@octokit/graphql-schema": "^15.25.0",
|
|
61
61
|
"@types/compression": "^1.8.1",
|
|
62
|
-
"@types/express-session": "^1.
|
|
62
|
+
"@types/express-session": "^1.19.0",
|
|
63
63
|
"@types/mocha": "^10.0.10",
|
|
64
64
|
"chai": "^6.2.2",
|
|
65
|
-
"graphql": "^16.13.
|
|
65
|
+
"graphql": "^16.13.2",
|
|
66
66
|
"mocha": "^11.7.5",
|
|
67
|
-
"nock": "^14.0.
|
|
67
|
+
"nock": "^14.0.13",
|
|
68
68
|
"nodemon": "^3.1.14",
|
|
69
69
|
"npm-run-all2": "^8.0.4",
|
|
70
|
-
"sinon": "^21.
|
|
70
|
+
"sinon": "^21.1.2",
|
|
71
71
|
"sinon-chai": "^4.0.0",
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
},
|
|
@@ -81,6 +81,5 @@
|
|
|
81
81
|
"app.yml",
|
|
82
82
|
"index.js",
|
|
83
83
|
"wuffle.config.example.js"
|
|
84
|
-
]
|
|
85
|
-
"gitHead": "e3234b1499bb1c75e3bf40c15b0c105bedeba70c"
|
|
84
|
+
]
|
|
86
85
|
}
|