Skip to content
This repository was archived by the owner on Jul 11, 2023. It is now read-only.

Commit e64b72a

Browse files
committed
add filters
content type labels
1 parent a05f74c commit e64b72a

5 files changed

Lines changed: 166 additions & 8 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,24 @@ The `${{ secrets.GITHUB_TOKEN }}` token can be used only when all project column
5050
2. `repo` to access information in private repositories
5151
3. `user` to access information in user repositories (if needed)
5252

53+
### Filtering cards mirroring
5354

55+
Cards can be filtered from mirroring by specifying additional inputs on the action workflow.
5456

57+
**type_filter**
5558

59+
Filters mirrored cards only to the specified type. Must be one of
60+
- `content` (linked issue or PR)
61+
- `note`
5662

63+
**label_filter**
5764

65+
Filters mirrored cards based labels that match the input. Note cards cannot be tagged with labels, and will never be filtered based on this input.
5866

67+
Label filters use exact, case sensitive comparisons to determine whether to mirror a card. Label filter inputs can contain multiple labels separated by commas (`'first, second'`), and will be mirrored if any labels match (i.e. `OR` logic).
5968

69+
**content_filter**
6070

71+
Filters mirrored cards based on the displayed card content. Issue/PR titles is evaluated when they are linked as cards, otherwise the card's note text is used.
6172

73+
Content filters use partial, case insensitive comparisons when determining which cards to mirror. Content filter inputs can contain multiple filters separated by commas (`'first, second'`), and will be mirrored if any content matches are found (i.e. `OR` logic). Content filters containing commas must be wrapped in quotes (`'first, second, "matching, with a comma"'`)

action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ inputs:
1212
description: "GitHub token to use for authenticated API calls"
1313
required: false
1414
default: ${{ github.token }}
15+
type_filter:
16+
description: "Filter to a single card type, either 'note' or 'content'"
17+
required: false
18+
label_filter:
19+
description: "Filter to cards containing matching labels"
20+
required: false
21+
content_filter:
22+
description: "Filter to cards with matching note content or issue/PR title"
23+
required: false
1524
runs:
1625
using: 'node12'
1726
main: 'dist/index.js'

dist/index.js

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,15 +1923,24 @@ module.exports = require("child_process");
19231923
"use strict";
19241924

19251925
Object.defineProperty(exports, "__esModule", { value: true });
1926+
const projectCardContentFields = `
1927+
id
1928+
title
1929+
labels(first: 20) {
1930+
nodes {
1931+
name
1932+
}
1933+
}
1934+
`.trim();
19261935
const projectCardFields = `
19271936
id
19281937
note
19291938
content {
19301939
... on Issue {
1931-
id
1940+
${projectCardContentFields}
19321941
}
19331942
... on PullRequest {
1934-
id
1943+
${projectCardContentFields}
19351944
}
19361945
}
19371946
`.trim();
@@ -2212,7 +2221,6 @@ function ensureCard(card, index, targetColumn) {
22122221
}
22132222
if (!targetCard) {
22142223
// add!
2215-
core.info(`Creating card in ${targetColumn.name}`);
22162224
const cardData = {};
22172225
if (card.content) {
22182226
cardData.contentId = card.content.id;
@@ -2228,11 +2236,9 @@ function ensureCard(card, index, targetColumn) {
22282236
targetCard = response['addProjectCard'].cardEdge.node;
22292237
targetColumn.cards.nodes.unshift(targetCard);
22302238
targetCardIndex = 0;
2231-
core.info(`created card: ${targetCard.id}`);
22322239
}
22332240
if (targetCardIndex !== index) {
22342241
// move!
2235-
core.info(`moving card: ${targetCard.id}`);
22362242
const moveData = {
22372243
cardId: targetCard.id,
22382244
columnId: targetColumn.id,
@@ -2242,11 +2248,61 @@ function ensureCard(card, index, targetColumn) {
22422248
// update the target column card location in the local card array
22432249
targetColumn.cards.nodes.splice(index, 0, targetColumn.cards.nodes.splice(targetCardIndex, 1)[0]);
22442250
targetCardIndex = index;
2245-
core.info(`moved card: ${targetCard.id} after ${moveData.afterCardId}`);
22462251
}
22472252
return targetCard;
22482253
});
22492254
}
2255+
// content filters are parsed from a string either as wrapped in quotes or comma separated
2256+
const FILTER_LIST_REGEX = /\s*(?:((["'])([^\2]+?)\2)|([^"',]+))\s*/g;
2257+
function getFilterList(input) {
2258+
if (!input) {
2259+
return [];
2260+
}
2261+
return [...input.matchAll(FILTER_LIST_REGEX)]
2262+
.map(match => match[3] || match[4])
2263+
.map(filter => filter.trim())
2264+
.filter(filter => !!filter);
2265+
}
2266+
function applyFilters(column) {
2267+
const typeFilter = core.getInput('type_filter', { required: false });
2268+
if (typeFilter === 'note') {
2269+
column.cards.nodes = column.cards.nodes.filter(card => !!card.note);
2270+
}
2271+
else if (typeFilter === 'content') {
2272+
column.cards.nodes = column.cards.nodes.filter(card => !!card.content);
2273+
}
2274+
else if (typeFilter) {
2275+
core.warning(`cannot apply unknown type_filter ${typeFilter}`);
2276+
}
2277+
const contentFilters = getFilterList(core.getInput('content_filter', { required: false }));
2278+
if (contentFilters.length > 0) {
2279+
// match content in case-insensitive manner
2280+
const contentMatchers = contentFilters.map(filter => new RegExp(filter, 'i'));
2281+
// filter to cards with displayed text content that matches at least one
2282+
// of the user-supplied filters
2283+
column.cards.nodes = column.cards.nodes.filter((card) => {
2284+
if (card.content) {
2285+
return contentMatchers.some(filter => filter.test(card.content.title));
2286+
}
2287+
else if (card.note) {
2288+
return contentMatchers.some(filter => filter.test(card.note));
2289+
}
2290+
// don't filter cards that cannot be filtered by content
2291+
return true;
2292+
});
2293+
}
2294+
const labelFilters = getFilterList(core.getInput('label_filter', { required: false }));
2295+
if (labelFilters.length > 0) {
2296+
column.cards.nodes = column.cards.nodes.filter((card) => {
2297+
if (card.content) {
2298+
// only include cards for issues and PRs that have a matching label
2299+
return card.content.labels.nodes.some(label => labelFilters.includes(label.name));
2300+
}
2301+
// don't filter cards that can't be filtered by labels
2302+
return true;
2303+
});
2304+
}
2305+
}
22502306
function run() {
22512307
return __awaiter(this, void 0, void 0, function* () {
22522308
try {
@@ -2262,6 +2318,9 @@ function run() {
22622318
}
22632319
const sourceColumn = response['sourceColumn'];
22642320
const targetColumn = response['targetColumn'];
2321+
// apply user supplied filters to cards from the source column and mirror the
2322+
// target column based on the remaining filters
2323+
applyFilters(sourceColumn);
22652324
// make sure that a card explaining the automation on the column exists
22662325
// at index 0 in the target column
22672326
const automationNoteCard = {

src/graphql.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
const projectCardContentFields = `
2+
id
3+
title
4+
labels(first: 20) {
5+
nodes {
6+
name
7+
}
8+
}
9+
`.trim()
10+
111
const projectCardFields = `
212
id
313
note
414
content {
515
... on Issue {
6-
id
16+
${projectCardContentFields}
717
}
818
... on PullRequest {
9-
id
19+
${projectCardContentFields}
1020
}
1121
}
1222
`.trim()

src/main.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,70 @@ async function ensureCard(
9494
return targetCard
9595
}
9696

97+
// content filters are parsed from a string either as wrapped in quotes or comma separated
98+
const FILTER_LIST_REGEX = /\s*(?:((["'])([^\2]+?)\2)|([^"',]+))\s*/g
99+
function getFilterList(input: string): string[] {
100+
if (!input) {
101+
return []
102+
}
103+
104+
return [...input.matchAll(FILTER_LIST_REGEX)]
105+
.map(match => match[3] || match[4])
106+
.map(filter => filter.trim())
107+
.filter(filter => !!filter)
108+
}
109+
110+
function applyFilters(column: any): void {
111+
const typeFilter = core.getInput('type_filter', {required: false})
112+
if (typeFilter === 'note') {
113+
column.cards.nodes = column.cards.nodes.filter(card => !!card.note)
114+
} else if (typeFilter === 'content') {
115+
column.cards.nodes = column.cards.nodes.filter(card => !!card.content)
116+
} else if (typeFilter) {
117+
core.warning(`cannot apply unknown type_filter ${typeFilter}`)
118+
}
119+
120+
const contentFilters = getFilterList(
121+
core.getInput('content_filter', {required: false})
122+
)
123+
if (contentFilters.length > 0) {
124+
// match content in case-insensitive manner
125+
const contentMatchers = contentFilters.map(
126+
filter => new RegExp(filter, 'i')
127+
)
128+
129+
// filter to cards with displayed text content that matches at least one
130+
// of the user-supplied filters
131+
column.cards.nodes = column.cards.nodes.filter((card: any): boolean => {
132+
if (card.content) {
133+
return contentMatchers.some(filter => filter.test(card.content.title))
134+
} else if (card.note) {
135+
return contentMatchers.some(filter => filter.test(card.note))
136+
}
137+
138+
// don't filter cards that cannot be filtered by content
139+
return true
140+
})
141+
}
142+
143+
const labelFilters = getFilterList(
144+
core.getInput('label_filter', {required: false})
145+
)
146+
if (labelFilters.length > 0) {
147+
column.cards.nodes = column.cards.nodes.filter((card: any): boolean => {
148+
if (card.content) {
149+
// only include cards for issues and PRs that have a matching label
150+
return card.content.labels.nodes.some(label =>
151+
labelFilters.includes(label.name)
152+
)
153+
}
154+
155+
// don't filter cards that can't be filtered by labels
156+
return true
157+
})
158+
}
159+
}
160+
97161
async function run(): Promise<void> {
98162
try {
99163
const sourceColumnId = core.getInput('source_column_id')
@@ -111,6 +175,10 @@ async function run(): Promise<void> {
111175
const sourceColumn = response['sourceColumn']
112176
const targetColumn = response['targetColumn']
113177

178+
// apply user supplied filters to cards from the source column and mirror the
179+
// target column based on the remaining filters
180+
applyFilters(sourceColumn)
181+
114182
// make sure that a card explaining the automation on the column exists
115183
// at index 0 in the target column
116184
const automationNoteCard: any = {

0 commit comments

Comments
 (0)