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