wuffle 0.71.0 → 0.73.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/LICENSE +21 -0
- package/lib/apps/board-api-routes/board-api-filters.js +16 -8
- package/lib/apps/github-reviews/GithubReviews.js +4 -2
- package/lib/apps/search/Search.js +81 -26
- package/lib/apps/store-filter.js +31 -0
- package/lib/filters.js +12 -6
- package/lib/index.js +2 -1
- package/lib/store.js +25 -1
- package/package.json +6 -5
- package/public/bundle.js +1 -1
- package/public/bundle.js.map +1 -1
- package/wuffle.config.example.js +44 -34
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-present Nico Rehwaldt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -63,13 +63,15 @@ export function filterUser(user) {
|
|
|
63
63
|
const {
|
|
64
64
|
login,
|
|
65
65
|
avatar_url,
|
|
66
|
-
html_url
|
|
66
|
+
html_url,
|
|
67
|
+
type
|
|
67
68
|
} = user;
|
|
68
69
|
|
|
69
70
|
return {
|
|
70
71
|
login,
|
|
71
72
|
avatar_url,
|
|
72
|
-
html_url
|
|
73
|
+
html_url,
|
|
74
|
+
type
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -138,13 +140,15 @@ export function filterReview(review) {
|
|
|
138
140
|
const {
|
|
139
141
|
state,
|
|
140
142
|
user,
|
|
141
|
-
html_url
|
|
143
|
+
html_url,
|
|
144
|
+
author_association
|
|
142
145
|
} = review;
|
|
143
146
|
|
|
144
147
|
return {
|
|
145
148
|
state,
|
|
146
149
|
html_url,
|
|
147
|
-
user: filterUser(user)
|
|
150
|
+
user: filterUser(user),
|
|
151
|
+
author_association
|
|
148
152
|
};
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -173,7 +177,8 @@ export function filterPull(pullRequest) {
|
|
|
173
177
|
order,
|
|
174
178
|
column,
|
|
175
179
|
check_runs = [],
|
|
176
|
-
statuses = []
|
|
180
|
+
statuses = [],
|
|
181
|
+
author_association
|
|
177
182
|
} = pullRequest;
|
|
178
183
|
|
|
179
184
|
return {
|
|
@@ -198,7 +203,8 @@ export function filterPull(pullRequest) {
|
|
|
198
203
|
order,
|
|
199
204
|
column,
|
|
200
205
|
statuses: statuses.map(filterStatus),
|
|
201
|
-
check_runs: check_runs.map(filterCheckRun)
|
|
206
|
+
check_runs: check_runs.map(filterCheckRun),
|
|
207
|
+
author_association
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -222,7 +228,8 @@ export function filterIssue(issue) {
|
|
|
222
228
|
comments = [],
|
|
223
229
|
links = [],
|
|
224
230
|
order,
|
|
225
|
-
column
|
|
231
|
+
column,
|
|
232
|
+
author_association
|
|
226
233
|
} = issue;
|
|
227
234
|
|
|
228
235
|
return {
|
|
@@ -242,7 +249,8 @@ export function filterIssue(issue) {
|
|
|
242
249
|
html_url,
|
|
243
250
|
links: links.map(filterLink),
|
|
244
251
|
order,
|
|
245
|
-
column
|
|
252
|
+
column,
|
|
253
|
+
author_association
|
|
246
254
|
};
|
|
247
255
|
|
|
248
256
|
}
|
|
@@ -117,7 +117,8 @@ function filterReview(review) {
|
|
|
117
117
|
submitted_at,
|
|
118
118
|
state,
|
|
119
119
|
user,
|
|
120
|
-
html_url
|
|
120
|
+
html_url,
|
|
121
|
+
author_association
|
|
121
122
|
} = review;
|
|
122
123
|
|
|
123
124
|
return {
|
|
@@ -128,6 +129,7 @@ function filterReview(review) {
|
|
|
128
129
|
submitted_at,
|
|
129
130
|
state: state.toLowerCase(),
|
|
130
131
|
user: filterUser(user),
|
|
131
|
-
html_url
|
|
132
|
+
html_url,
|
|
133
|
+
author_association
|
|
132
134
|
};
|
|
133
135
|
}
|
|
@@ -8,7 +8,11 @@ const CHILD_LINK_TYPES = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* @typedef { {
|
|
11
|
+
* @typedef { {
|
|
12
|
+
* defaultFilter?: string,
|
|
13
|
+
* treatBotsAsReviewers?: boolean
|
|
14
|
+
* } } SearchConfig
|
|
15
|
+
*
|
|
12
16
|
* @typedef { import('../../util/search.js').SearchTerm } SearchTerm
|
|
13
17
|
*
|
|
14
18
|
* @typedef { import('../../types.js').GitHubUser } GitHubUser
|
|
@@ -31,6 +35,11 @@ export default function Search(config, logger, store) {
|
|
|
31
35
|
name: 'wuffle:search'
|
|
32
36
|
});
|
|
33
37
|
|
|
38
|
+
const {
|
|
39
|
+
treatBotsAsReviewers = false,
|
|
40
|
+
defaultFilter
|
|
41
|
+
} = config;
|
|
42
|
+
|
|
34
43
|
function filterNoop(issue) {
|
|
35
44
|
return true;
|
|
36
45
|
}
|
|
@@ -67,12 +76,46 @@ export default function Search(config, logger, store) {
|
|
|
67
76
|
return issue.pull_request;
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
function isValidReview(review) {
|
|
80
|
+
|
|
81
|
+
const {
|
|
82
|
+
user,
|
|
83
|
+
author_association
|
|
84
|
+
} = review;
|
|
85
|
+
|
|
86
|
+
if (!user) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (user.type === 'Bot') {
|
|
91
|
+
return treatBotsAsReviewers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// backwards compatibility - we did not have that
|
|
95
|
+
// data stored previously
|
|
96
|
+
return author_association ? [ 'COLLABORATOR', 'MEMBER', 'OWNER' ].includes(author_association) : true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getEffectiveReviews(reviews) {
|
|
100
|
+
const byReviewer = new Map();
|
|
101
|
+
|
|
102
|
+
for (const review of reviews) {
|
|
103
|
+
if (review.state === 'commented' || !isValidReview(review)) continue;
|
|
104
|
+
|
|
105
|
+
byReviewer.set(review.user?.login, review);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [ ...byReviewer.values() ];
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
function isApproved(issue) {
|
|
71
|
-
return (issue.reviews || []).some(
|
|
112
|
+
return getEffectiveReviews(issue.reviews || []).some(
|
|
113
|
+
r => r.state === 'approved'
|
|
114
|
+
);
|
|
72
115
|
}
|
|
73
116
|
|
|
74
117
|
function isReviewed(issue) {
|
|
75
|
-
return (issue.reviews || []).length > 0;
|
|
118
|
+
return getEffectiveReviews(issue.reviews || []).length > 0;
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
const filters = {
|
|
@@ -158,6 +201,10 @@ export default function Search(config, logger, store) {
|
|
|
158
201
|
|
|
159
202
|
return !links || !links.some(link => CHILD_LINK_TYPES[link.type]);
|
|
160
203
|
};
|
|
204
|
+
case 'referenced':
|
|
205
|
+
return function filterReferenced(issue) {
|
|
206
|
+
return (issue.links || []).length > 0;
|
|
207
|
+
};
|
|
161
208
|
default:
|
|
162
209
|
return filterNoop;
|
|
163
210
|
}
|
|
@@ -286,7 +333,29 @@ export default function Search(config, logger, store) {
|
|
|
286
333
|
return function filterCreated(issue) {
|
|
287
334
|
return matchTemporal(issue.updated_at);
|
|
288
335
|
};
|
|
289
|
-
})
|
|
336
|
+
}),
|
|
337
|
+
|
|
338
|
+
'or': function orFilter(value, _exact, user) {
|
|
339
|
+
|
|
340
|
+
const childTerms = /** @type {SearchTerm[]} */ (value);
|
|
341
|
+
|
|
342
|
+
const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
|
|
343
|
+
|
|
344
|
+
return (issue) => filterFns.some(
|
|
345
|
+
filterFn => filterFn(issue)
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
'and': function andFilter(value, _exact, user) {
|
|
350
|
+
|
|
351
|
+
const childTerms = /** @type {SearchTerm[]} */ (value);
|
|
352
|
+
|
|
353
|
+
const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
|
|
354
|
+
|
|
355
|
+
return (issue) => filterFns.every(
|
|
356
|
+
filterFn => filterFn(issue)
|
|
357
|
+
);
|
|
358
|
+
}
|
|
290
359
|
};
|
|
291
360
|
|
|
292
361
|
function temporalFilter(fn) {
|
|
@@ -339,19 +408,6 @@ export default function Search(config, logger, store) {
|
|
|
339
408
|
|
|
340
409
|
const wrap = (fn) => negated ? ((issue) => !fn(issue)) : fn;
|
|
341
410
|
|
|
342
|
-
if ([ 'or', 'and' ].includes(qualifier)) {
|
|
343
|
-
|
|
344
|
-
const childTerms = /** @type {SearchTerm[]} */ (value);
|
|
345
|
-
|
|
346
|
-
const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
|
|
347
|
-
|
|
348
|
-
return wrap(
|
|
349
|
-
(issue) => filterFns[qualifier === 'or' ? 'some' : 'every'](
|
|
350
|
-
filterFn => filterFn(issue)
|
|
351
|
-
)
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
411
|
if (!value) {
|
|
356
412
|
return noopFilter();
|
|
357
413
|
}
|
|
@@ -371,7 +427,7 @@ export default function Search(config, logger, store) {
|
|
|
371
427
|
return noopFilter();
|
|
372
428
|
}
|
|
373
429
|
|
|
374
|
-
return wrap(factoryFn(value, exact));
|
|
430
|
+
return wrap(factoryFn(value, exact, user));
|
|
375
431
|
}
|
|
376
432
|
|
|
377
433
|
function buildFilterFn(search, user) {
|
|
@@ -395,9 +451,11 @@ export default function Search(config, logger, store) {
|
|
|
395
451
|
*/
|
|
396
452
|
function getSearchFilter(search, user) {
|
|
397
453
|
|
|
398
|
-
|
|
454
|
+
if (!search) {
|
|
455
|
+
search = defaultFilter;
|
|
456
|
+
}
|
|
399
457
|
|
|
400
|
-
const
|
|
458
|
+
const filterFn = buildFilterFn(search, user);
|
|
401
459
|
|
|
402
460
|
return function(issue) {
|
|
403
461
|
try {
|
|
@@ -405,12 +463,7 @@ export default function Search(config, logger, store) {
|
|
|
405
463
|
return filterFn(issue);
|
|
406
464
|
}
|
|
407
465
|
|
|
408
|
-
|
|
409
|
-
return ignoreFilterFn(issue);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// no user search, no ignore filter,
|
|
413
|
-
// show all issues
|
|
466
|
+
// no search, show all issues
|
|
414
467
|
return true;
|
|
415
468
|
} catch (err) {
|
|
416
469
|
log.warn({ issue: issueIdent(issue), err }, 'filter failed');
|
|
@@ -424,4 +477,6 @@ export default function Search(config, logger, store) {
|
|
|
424
477
|
// api ///////////////////////
|
|
425
478
|
|
|
426
479
|
this.getSearchFilter = getSearchFilter;
|
|
480
|
+
|
|
481
|
+
this.buildFilterFn = buildFilterFn;
|
|
427
482
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef { { ignoreFilter?: string } } StoreFilterConfig
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An component that configures the store to filter certain elements,
|
|
7
|
+
* effectively making them invisible to the board and its users.
|
|
8
|
+
*
|
|
9
|
+
* @param { StoreFilterConfig } config
|
|
10
|
+
*
|
|
11
|
+
* @param { import('../store.js').default } store
|
|
12
|
+
* @param { import('./search/Search.js').default } search
|
|
13
|
+
* @param { import('../types.js').Logger } logger
|
|
14
|
+
*/
|
|
15
|
+
export default function StoreFilter(config, store, search, logger) {
|
|
16
|
+
|
|
17
|
+
const log = logger.child({
|
|
18
|
+
name: 'wuffle:store-filter'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if ('ignoreFilter' in config) {
|
|
22
|
+
const ignoreFilterFn = search.buildFilterFn(config.ignoreFilter);
|
|
23
|
+
|
|
24
|
+
if (ignoreFilterFn) {
|
|
25
|
+
store.setIgnoreFilter(ignoreFilterFn);
|
|
26
|
+
} else {
|
|
27
|
+
log.warn('unparseable <ignoreFilter> - please correct your board configuration');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
package/lib/filters.js
CHANGED
|
@@ -47,7 +47,8 @@ export function filterUser(githubUser) {
|
|
|
47
47
|
node_id,
|
|
48
48
|
login,
|
|
49
49
|
avatar_url,
|
|
50
|
-
html_url
|
|
50
|
+
html_url,
|
|
51
|
+
type
|
|
51
52
|
} = githubUser;
|
|
52
53
|
|
|
53
54
|
return {
|
|
@@ -55,7 +56,8 @@ export function filterUser(githubUser) {
|
|
|
55
56
|
node_id,
|
|
56
57
|
login,
|
|
57
58
|
avatar_url,
|
|
58
|
-
html_url
|
|
59
|
+
html_url,
|
|
60
|
+
type
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -147,7 +149,8 @@ export function filterPull(githubPull, githubRepository) {
|
|
|
147
149
|
additions,
|
|
148
150
|
deletions,
|
|
149
151
|
changed_files,
|
|
150
|
-
html_url
|
|
152
|
+
html_url,
|
|
153
|
+
author_association
|
|
151
154
|
} = githubPull;
|
|
152
155
|
|
|
153
156
|
// stable ID that is independent from GitHubs internal issue/pr distinction
|
|
@@ -187,7 +190,8 @@ export function filterPull(githubPull, githubRepository) {
|
|
|
187
190
|
changed_files,
|
|
188
191
|
pull_request: true,
|
|
189
192
|
repository: filterRepository(githubRepository),
|
|
190
|
-
html_url
|
|
193
|
+
html_url,
|
|
194
|
+
author_association
|
|
191
195
|
};
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -209,7 +213,8 @@ export function filterIssue(githubIssue, githubRepository) {
|
|
|
209
213
|
labels,
|
|
210
214
|
milestone,
|
|
211
215
|
pull_request,
|
|
212
|
-
html_url
|
|
216
|
+
html_url,
|
|
217
|
+
author_association
|
|
213
218
|
} = githubIssue;
|
|
214
219
|
|
|
215
220
|
// stable ID that is independent from GitHubs internal issue/pr distinction
|
|
@@ -239,7 +244,8 @@ export function filterIssue(githubIssue, githubRepository) {
|
|
|
239
244
|
milestone: milestone ? filterMilestone(milestone) : null,
|
|
240
245
|
repository: filterRepository(githubRepository),
|
|
241
246
|
pull_request: !!pull_request,
|
|
242
|
-
html_url
|
|
247
|
+
html_url,
|
|
248
|
+
author_association
|
|
243
249
|
};
|
|
244
250
|
|
|
245
251
|
}
|
package/lib/index.js
CHANGED
|
@@ -27,7 +27,8 @@ const appModules = [
|
|
|
27
27
|
import('./apps/auth-routes/index.js'),
|
|
28
28
|
import('./apps/board-api-routes/index.js'),
|
|
29
29
|
import('./apps/board-routes.js'),
|
|
30
|
-
import('./apps/reindex-store.js')
|
|
30
|
+
import('./apps/reindex-store.js'),
|
|
31
|
+
import('./apps/store-filter.js')
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
import loadConfig from './load-config.js';
|
package/lib/store.js
CHANGED
|
@@ -4,6 +4,9 @@ import { issueIdent } from './util/index.js';
|
|
|
4
4
|
import { findLinks } from './util/links.js';
|
|
5
5
|
import { Links } from './links.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef { import('./apps/search/Search.js').FilterFn } FilterFn
|
|
9
|
+
*/
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* The store that holds all board data
|
|
@@ -39,6 +42,10 @@ export default class Store {
|
|
|
39
42
|
this.updateContext = null;
|
|
40
43
|
this.queuedUpdates = [];
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @type { FilterFn }
|
|
47
|
+
*/
|
|
48
|
+
this.ignoreFilter = (issue) => false;
|
|
42
49
|
|
|
43
50
|
this.on('updateIssue', async event => {
|
|
44
51
|
|
|
@@ -303,7 +310,9 @@ export default class Store {
|
|
|
303
310
|
/**
|
|
304
311
|
* Update issue, tagging it as updated.
|
|
305
312
|
*
|
|
306
|
-
*
|
|
313
|
+
* If issue matches the ignore filter it will be removed as part of the update.
|
|
314
|
+
*
|
|
315
|
+
* @return {Promise<Object|undefined>} a promise to the updated issue or undefined in case the update triggered issue deletion
|
|
307
316
|
*/
|
|
308
317
|
updateIssue(update) {
|
|
309
318
|
|
|
@@ -374,6 +383,14 @@ export default class Store {
|
|
|
374
383
|
|
|
375
384
|
const ident = issueIdent(updatedIssue);
|
|
376
385
|
|
|
386
|
+
if (this.ignoreFilter(updatedIssue)) {
|
|
387
|
+
this.log.debug({ issue: ident }, 'issue matching ignore filter');
|
|
388
|
+
|
|
389
|
+
await this.removeIssueById(id);
|
|
390
|
+
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
377
394
|
this.log.debug({
|
|
378
395
|
issue: ident
|
|
379
396
|
}, 'process update');
|
|
@@ -861,6 +878,13 @@ export default class Store {
|
|
|
861
878
|
}, 'load JSON complete');
|
|
862
879
|
}
|
|
863
880
|
|
|
881
|
+
/**
|
|
882
|
+
* @param { FilterFn } ignoreFilter
|
|
883
|
+
*/
|
|
884
|
+
setIgnoreFilter(ignoreFilter) {
|
|
885
|
+
this.ignoreFilter = ignoreFilter;
|
|
886
|
+
}
|
|
887
|
+
|
|
864
888
|
}
|
|
865
889
|
|
|
866
890
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wuffle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"description": "A multi-repository task board for GitHub issues",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nico Rehwaldt",
|
|
7
7
|
"url": "https://github.com/nikku"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"wuffle": "
|
|
10
|
+
"wuffle": "bin/wuffle"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"homepage": "https://github.com/nikku/wuffle",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "https://github.com/nikku/wuffle.git",
|
|
22
|
+
"url": "git+https://github.com/nikku/wuffle.git",
|
|
23
23
|
"directory": "packages/app"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"compression": "^1.8.1",
|
|
49
49
|
"express-session": "^1.19.0",
|
|
50
50
|
"fake-tag": "^5.0.0",
|
|
51
|
-
"memorystore": "^1.6.
|
|
51
|
+
"memorystore": "^1.6.8",
|
|
52
52
|
"min-dash": "^5.0.0",
|
|
53
53
|
"p-defer": "^4.0.1",
|
|
54
54
|
"prexit": "^2.3.0",
|
|
@@ -81,5 +81,6 @@
|
|
|
81
81
|
"app.yml",
|
|
82
82
|
"index.js",
|
|
83
83
|
"wuffle.config.example.js"
|
|
84
|
-
]
|
|
84
|
+
],
|
|
85
|
+
"gitHead": "a58cc7fcd73db7a1dc7babc25b97bb590119b69d"
|
|
85
86
|
}
|