Skip to content

Commit 83e5617

Browse files
committed
Set up the interactive page for finding new issues
1 parent 311666b commit 83e5617

File tree

6 files changed

+439
-6
lines changed

6 files changed

+439
-6
lines changed

themes/vocabulary_theme/templates/issue_finder.html

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
<div class="header">
88
<div class="container">
99
<h1>Issue Finder</h1>
10-
<p class="description">Welcome to the CC developer community! It's wonderful to have you here.</p>
10+
<p class="description">
11+
Welcome to the CC developer community! We're absolutely delighted to
12+
have you here.
13+
</p>
1114
</div>
1215
</div>
1316
<div class="body container">

webpack/js/issue-finder.js

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import {Octokit} from '@octokit/rest';
2+
import Vue from 'vue';
3+
4+
import VueSelect from 'vue-select';
5+
6+
const octokit = new Octokit({
7+
auth: ''
8+
});
9+
10+
const IssueCard = {
11+
template: `
12+
<div class="card entry-post vertical margin-top-normal padding-normal">
13+
<h4 class="card-title b-header margin-bottom-small">{{ issue.title }}</h4>
14+
<a :href="issue.html_url" class="button is-text tiny site-link" target="_blank">
15+
<span class="has-color-forest-green">
16+
{{ issue.repository.name }}#{{ issue.number }}
17+
</span>
18+
<i class="icon external-link has-color-forest-green"></i>
19+
</a>
20+
<div class="labels margin-top-small">
21+
<span
22+
v-for="labelName in issue.labelNames"
23+
class="button tag margin-right-small">
24+
{{ labelName }}
25+
</span>
26+
</div>
27+
</div>`,
28+
props: {
29+
issue: {
30+
type: Object,
31+
required: true
32+
},
33+
}
34+
}
35+
36+
const App = {
37+
el: '#vue-app',
38+
template: `
39+
<div class="find-issues">
40+
<form id="filters">
41+
<label for="aspect">
42+
<strong>Aspect</strong><br>
43+
What aspect of the codebase would you like to work on?
44+
Leave blank if you don't have a preference.
45+
</label>
46+
<VueSelect
47+
v-model="filters.aspect"
48+
id="aspect"
49+
name="aspect"
50+
:options="options.aspects"
51+
label="name"
52+
:reduce="aspect => aspect.code"/>
53+
54+
<label for="skills">
55+
<strong>Skill set</strong><br>
56+
Choose up to three skills that you would like to see issues for.
57+
Leave blank if you don't have a preference.
58+
</label>
59+
<VueSelect
60+
v-model="filters.skills"
61+
id="skills"
62+
name="skills"
63+
:options="options.skills"
64+
label="name"
65+
:reduce="skill => skill.code"
66+
:selectable="() => filters.skills.length < 3"
67+
multiple/>
68+
69+
<label for="experience">
70+
<strong>Experience</strong><br>
71+
Are you a new contributor or do you have experience working with CC?
72+
</label>
73+
<VueSelect
74+
v-model="filters.experience"
75+
id="experience"
76+
name="experience"
77+
:options="options.experiences"
78+
label="name"
79+
:reduce="experience => experience.code"
80+
:clearable="false"/>
81+
82+
<button
83+
class="button small is-success margin-top-small"
84+
@click.prevent="search">
85+
Search
86+
</button>
87+
</form>
88+
89+
<template v-if="issues.length">
90+
<issue-card
91+
v-for="issue in filteredIssues"
92+
:key="issue.id"
93+
:issue="issue"/>
94+
</template>
95+
<p
96+
v-else
97+
class="margin-top-normal">
98+
No results.
99+
</p>
100+
</div>`,
101+
components: {
102+
VueSelect,
103+
IssueCard
104+
},
105+
data() {
106+
return {
107+
message: 'Hello',
108+
options: {
109+
skills: [
110+
{
111+
name: 'Python',
112+
code: 'python'
113+
},
114+
{
115+
name: 'JavaScript',
116+
code: 'javascript'
117+
},
118+
{
119+
name: 'Sass',
120+
code: 'sass'
121+
}
122+
],
123+
experiences: [
124+
{
125+
name: 'New contributor',
126+
code: 'beginner'
127+
},
128+
{
129+
name: 'Experienced',
130+
code: 'experienced'
131+
}
132+
],
133+
aspects: [
134+
{
135+
name: '📄 Text',
136+
code: '📄 aspect: text'
137+
},
138+
{
139+
name: '💻 Code',
140+
code: '💻 aspect: code'
141+
},
142+
{
143+
name: '🕹 Interface',
144+
code: '🕹 aspect: interface'
145+
},
146+
{
147+
name: '🤖 DX',
148+
code: '🤖 aspect: dx'
149+
}
150+
]
151+
},
152+
filters: {
153+
aspect: null,
154+
skills: [],
155+
experience: 'experienced'
156+
},
157+
issues: []
158+
}
159+
},
160+
computed: {
161+
/**
162+
* Get a nested list of all the labels to search for. This is a combination
163+
* of the experience and aspect filter only. The skills filter must be
164+
* applied on the client side due to limitations of the GitHub API.
165+
*
166+
* This returns a list of lists. Each nested list corresponds to a single
167+
* API query and multiple queries need to be run to get the combined set of
168+
* valid issues.
169+
*
170+
* @returns {array} - the array of array of labels
171+
*/
172+
labelsList() {
173+
const labelsList = [
174+
['good first issue']
175+
]
176+
if (this.filters.experience === 'experienced') {
177+
labelsList.push(['help wanted'])
178+
}
179+
if (this.filters.aspect) {
180+
labelsList.forEach(labels => {
181+
labels.push(this.filters.aspect)
182+
})
183+
}
184+
return labelsList
185+
},
186+
/**
187+
* Get all the promises which will yield the issues being searched for.
188+
*
189+
* @returns {array} - the array of promises that will resolve to issues
190+
*/
191+
promises() {
192+
return this.labelsList.map(labels => this.promise(labels))
193+
},
194+
/**
195+
* Remove duplicates from the list of issues. Since two different API
196+
* queries are being collated, some issues may appear more than once. This
197+
* action removes such duplicates from the union of both results.
198+
*
199+
* @returns {array} - the array of issues without any duplicate entries
200+
*/
201+
dedupedIssues() {
202+
const ids = new Set()
203+
const deduped = []
204+
this.issues.forEach(issue => {
205+
if (ids.has(issue.id)) {
206+
return
207+
}
208+
deduped.push(issue)
209+
ids.add(issue.id)
210+
})
211+
return deduped
212+
},
213+
filteredIssues() {
214+
let filtered = this.dedupedIssues
215+
if (this.filters.skills.length) {
216+
filtered = filtered.filter(issue => {
217+
const joinedLabels = issue.labelNames.join(',')
218+
return this.filters.skills.some(skill => joinedLabels.includes(skill))
219+
})
220+
}
221+
return filtered
222+
}
223+
},
224+
methods: {
225+
/**
226+
* Get a Promise for the list of issues pertaining to a given skill.
227+
*
228+
* @param {Array} labels - list of labels to search for
229+
* @returns {Promise} a promise that resolves into a list of issues
230+
*/
231+
promise(labels = []) {
232+
const params = {
233+
org: 'creativecommons',
234+
state: 'open',
235+
filter: 'all'
236+
}
237+
if (labels) {
238+
params.labels = labels.join(',')
239+
}
240+
return octokit
241+
.issues
242+
.listForOrg(params)
243+
.then(response => response.data)
244+
},
245+
/**
246+
* Run the search based on the data submitted via the form and load all
247+
* results into the `issues` attribute.
248+
*/
249+
search() {
250+
Promise
251+
.all(this.promises)
252+
.then(issueLists => {
253+
const issues = issueLists.flat()
254+
issues.forEach(issue => {
255+
issue.labelNames = issue.labels.map(label => label.name)
256+
})
257+
this.issues = issues
258+
})
259+
.catch(err => console.error(err))
260+
}
261+
}
262+
}
263+
264+
$(document).ready(function () {
265+
window.app = new Vue(App)
266+
})

0 commit comments

Comments
 (0)