1
+ import VueSelect from 'vue-select' ;
2
+
3
+ import { octokit } from './octokit' ;
4
+
5
+ export const IssueLabel = {
6
+ template : `
7
+ <span class="gh-label" :class="className">
8
+ {{ name }}
9
+ </span>` ,
10
+ data ( ) {
11
+ return {
12
+ labels : window . labels
13
+ }
14
+ } ,
15
+ props : {
16
+ name : {
17
+ type : String ,
18
+ required : true
19
+ }
20
+ } ,
21
+ computed : {
22
+ /**
23
+ * Get the name of the class to apply to the label based on the group to
24
+ * which it belongs. Falls back to miscellaneous if the label does not
25
+ * belong to a group or a class cannot be identified.
26
+ *
27
+ * @returns {string } the name of the class to apply to the label
28
+ */
29
+ className ( ) {
30
+ return window . className [ this . name ] || 'miscellaneous'
31
+ }
32
+ }
33
+ }
34
+
35
+ export const IssueCard = {
36
+ template : `
37
+ <div class="card entry-post vertical margin-top-normal padding-normal">
38
+ <h4 class="card-title b-header margin-bottom-small">{{ issue.title }}</h4>
39
+ <a :href="issue.html_url" class="button is-text tiny site-link" target="_blank">
40
+ <span class="has-color-forest-green">
41
+ {{ issue.repository.name }}#{{ issue.number }}
42
+ </span>
43
+ <i class="icon external-link has-color-forest-green"></i>
44
+ </a>
45
+ <div class="labels margin-top-small">
46
+ <IssueLabel
47
+ v-for="(name, index) in issue.labelNames"
48
+ :key="index"
49
+ :name="name"/>
50
+ </div>
51
+ </div>` ,
52
+ components : {
53
+ IssueLabel
54
+ } ,
55
+ props : {
56
+ issue : {
57
+ type : Object ,
58
+ required : true
59
+ } ,
60
+ }
61
+ }
62
+
63
+ export const App = {
64
+ el : '#vue-app' ,
65
+ template : `
66
+ <div class="find-issues">
67
+ <div class="columns">
68
+ <div class="column is-one-quarter">
69
+ <form id="filters">
70
+ <label for="skills">
71
+ <strong>Skill set</strong><br>
72
+ Choose up to three skills that you would like to see issues for.
73
+ Leave blank if you don't have a preference.
74
+ </label>
75
+ <VueSelect
76
+ v-model="filters.skills"
77
+ id="skills"
78
+ name="skills"
79
+ :options="options.skills"
80
+ :reduce="skill => skill.toLocaleLowerCase()"
81
+ :selectable="() => filters.skills.length < 3"
82
+ multiple/>
83
+ <br/>
84
+ <label for="experience">
85
+ <strong>Experience</strong><br>
86
+ Is this your first time contributing to CC?
87
+ </label>
88
+ <VueSelect
89
+ v-model="filters.experience"
90
+ id="experience"
91
+ name="experience"
92
+ :options="options.experiences"
93
+ label="name"
94
+ :reduce="experience => experience.code"
95
+ :clearable="false"/>
96
+
97
+ <button
98
+ class="button small is-success margin-top-small"
99
+ @click.prevent="search">
100
+ Search
101
+ </button>
102
+ </form>
103
+ </div>
104
+ <div class="column">
105
+ <template v-if="issues.length">
106
+ <issue-card
107
+ v-for="issue in filteredIssues"
108
+ :key="issue.id"
109
+ :issue="issue"/>
110
+ </template>
111
+ <p
112
+ v-else
113
+ class="margin-top-normal">
114
+ No results.
115
+ </p>
116
+ </div>
117
+ </div>
118
+ </div>` ,
119
+ components : {
120
+ VueSelect,
121
+ IssueCard
122
+ } ,
123
+ data ( ) {
124
+ return {
125
+ options : {
126
+ skills : window . skillSet ,
127
+ experiences : [
128
+ { name : 'Yes, it is' , code : 'beginner' } ,
129
+ { name : 'No, it isn\'t' , code : 'experienced' }
130
+ ]
131
+ } ,
132
+ filters : {
133
+ skills : [ ] ,
134
+ experience : 'experienced'
135
+ } ,
136
+ issues : [ ]
137
+ }
138
+ } ,
139
+ computed : {
140
+ /**
141
+ * Get a nested list of all the labels to search for. This is only based on
142
+ * the experience filter. The skills filter must be applied on the client
143
+ * side due to limitations of the GitHub API.
144
+ *
145
+ * This returns a list of lists. Each nested list corresponds to a single
146
+ * API query and multiple queries need to be run to get the combined set of
147
+ * valid issues.
148
+ *
149
+ * @returns {array } the array of array of labels
150
+ */
151
+ labelsList ( ) {
152
+ const labelsList = [ ]
153
+ if ( this . filters . experience === 'experienced' ) {
154
+ labelsList . push ( 'help wanted' )
155
+ } else {
156
+ labelsList . push ( 'good first issue' )
157
+ }
158
+ return labelsList
159
+ } ,
160
+ /**
161
+ * Get a filtered list of issues matching the chosen skill labels.
162
+ *
163
+ * @returns {array } the array of filtered issues
164
+ */
165
+ filteredIssues ( ) {
166
+ let filtered = this . issues
167
+ if ( this . filters . skills . length ) {
168
+ filtered = filtered . filter ( issue => {
169
+ const joinedLabels = issue . labelNames . join ( ',' )
170
+ return this . filters . skills . some ( skill => joinedLabels . includes ( skill ) )
171
+ } )
172
+ }
173
+ return filtered
174
+ }
175
+ } ,
176
+ methods : {
177
+ /**
178
+ * Get a Promise for the list of issues pertaining to a given labels.
179
+ *
180
+ * @param {Array } labels - list of labels to search for
181
+ * @returns {Promise } a promise that resolves into a list of issues
182
+ */
183
+ promise ( labels = [ ] ) {
184
+ const params = {
185
+ org : 'creativecommons' ,
186
+ state : 'open' ,
187
+ filter : 'all' ,
188
+ per_page : 200
189
+ }
190
+ if ( labels ) {
191
+ params . labels = labels . join ( ',' )
192
+ }
193
+ return octokit
194
+ . issues
195
+ . listForOrg ( params )
196
+ . then ( response => response . data )
197
+ } ,
198
+ /**
199
+ * Run the search based on the data submitted via the form and load all
200
+ * results into the `issues` attribute.
201
+ */
202
+ search ( ) {
203
+ this . promise ( this . labelsList )
204
+ . then ( issueLists => {
205
+ const issues = issueLists . flat ( )
206
+ issues . forEach ( issue => {
207
+ issue . labelNames = issue . labels . map ( label => label . name )
208
+ } )
209
+ this . issues = issues
210
+ } )
211
+ . catch ( err => console . error ( err ) )
212
+ }
213
+ } ,
214
+ mounted ( ) {
215
+ this . search ( )
216
+ }
217
+ }
0 commit comments