bjira 0.0.21 → 0.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bjira",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "A simple jira CLI tool",
5
5
  "main": "src/index.js",
6
6
  "author": {
@@ -29,7 +29,8 @@
29
29
  "fuzzy": "^0.1.3",
30
30
  "inquirer": "^8.2.0",
31
31
  "inquirer-autocomplete-prompt": "^1.4.0",
32
- "jira-client": "^6.22.0",
32
+ "inquirer-checkbox-plus-prompt": "^1.0.1",
33
+ "jira-client": "^6.23.0",
33
34
  "ora": "^6.0.1",
34
35
  "temp": "^0.9.4"
35
36
  }
package/src/ask.js CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  import inquirer from 'inquirer';
6
6
  import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt';
7
+ import inquirerCheckboxPlusPrompt from 'inquirer-checkbox-plus-prompt';
7
8
  import fuzzy from 'fuzzy';
8
9
 
9
10
  inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt);
11
+ inquirer.registerPrompt('checkbox-plus', inquirerCheckboxPlusPrompt);
10
12
 
11
13
  class Ask {
12
14
  static async askString(message, defaultValue = undefined) {
@@ -61,6 +63,35 @@ class Ask {
61
63
  }]);
62
64
  return answer.value;
63
65
  }
66
+
67
+ static async askCallback(message, callback) {
68
+ const answer = await inquirer.prompt([{
69
+ type: 'autocomplete',
70
+ name: 'value',
71
+ message,
72
+ source: (answers, input) => callback(input),
73
+ }]);
74
+ return answer.value;
75
+ }
76
+
77
+ static async askMultiList(message, list, defaultValue = null) {
78
+ const answer = await inquirer.prompt([{
79
+ type: 'checkbox-plus',
80
+ name: 'value',
81
+ message,
82
+ default: defaultValue,
83
+ searchable: true,
84
+ source: (answers, input) => {
85
+ return new Promise(resolve => {
86
+ resolve(fuzzy.filter(input || '', list, {
87
+ extract: el => el.name
88
+ }).map(element => element.original));
89
+ });
90
+ },
91
+ }]);
92
+ return answer.value;
93
+ }
94
+
64
95
  };
65
96
 
66
97
  export default Ask;
package/src/create.js CHANGED
@@ -110,7 +110,7 @@ class Create extends Command {
110
110
  }
111
111
  }
112
112
 
113
- if (await Ask.askBoolean('Do you want to add it to the current sprint?')) {
113
+ if (await Ask.askBoolean('Do you want to add it to a sprint?')) {
114
114
  await Sprint.add(jira, issue.key);
115
115
  }
116
116
  });
package/src/field.js CHANGED
@@ -145,10 +145,14 @@ class Field extends Command {
145
145
  return field;
146
146
  }
147
147
 
148
+ if (Array.isArray(field)) {
149
+ return field.map(f => this.fieldValue(f, fieldData)).join(", ");
150
+ }
151
+
148
152
  return field.name;
149
153
  }
150
154
 
151
- static async fetchAndAskFieldIfSupported(jira, field) {
155
+ static async fetchAndAskFieldIfSupported(jira, field, defaultValue = null) {
152
156
  const meta = await Project.metadata(jira, field.projectName, field.issueTypeName);
153
157
  const fields = meta.projects.find(p => p.key === field.projectName)
154
158
  .issuetypes.find(i => i.name === field.issueTypeName).fields;
@@ -156,14 +160,14 @@ class Field extends Command {
156
160
  for (const name in fields) {
157
161
  const fieldObj = fields[name];
158
162
  if (fieldObj.name === field.fieldName) {
159
- return Field.askFieldIfSupported(fieldObj);
163
+ return Field.askFieldIfSupported(fieldObj, defaultValue);
160
164
  }
161
165
  }
162
166
 
163
167
  return null;
164
168
  }
165
169
 
166
- static async askFieldIfSupported(fieldData) {
170
+ static async askFieldIfSupported(fieldData, defaultValue = null) {
167
171
  if (!Field.isSupported(fieldData)) {
168
172
  console.log("Unsupported field");
169
173
  return null;
@@ -179,9 +183,30 @@ class Field extends Command {
179
183
  return {
180
184
  value: await Ask.askString(`${fieldData.name}:`), key: fieldData.key
181
185
  };
186
+
187
+ case 'array':
188
+ if (Array.isArray(defaultValue)) {
189
+ defaultValue = defaultValue.map(a => ({
190
+ id: a.id
191
+ }));
192
+ }
193
+
194
+ const value = await Ask.askMultiList(`${fieldData.name}:`,
195
+ fieldData.allowedValues.map(value => ({
196
+ name: value.name,
197
+ value: {
198
+ id: value.id
199
+ }
200
+ })),
201
+ defaultValue);
202
+
203
+ return {
204
+ key: fieldData.key,
205
+ value: value,
206
+ };
182
207
  }
183
208
 
184
- let value = await Ask.askList(`${fieldData.name}:`,
209
+ const value = await Ask.askList(`${fieldData.name}:`,
185
210
  fieldData.allowedValues.map(value => ({
186
211
  name: value.name,
187
212
  value: {
@@ -189,10 +214,6 @@ class Field extends Command {
189
214
  }
190
215
  })));
191
216
 
192
- if (fieldData.schema.type === 'array') {
193
- value = [value];
194
- }
195
-
196
217
  return {
197
218
  key: fieldData.key,
198
219
  value,
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ import Create from './create.js';
15
15
  import Field from './field.js';
16
16
  import Init from './init.js';
17
17
  import Issue from './issue.js';
18
+ import Label from './label.js';
18
19
  import Preset from './preset.js';
19
20
  import Project from './project.js';
20
21
  import Query from './query.js';
@@ -31,6 +32,7 @@ const commands = [
31
32
  new Field(),
32
33
  new Init(),
33
34
  new Issue(),
35
+ new Label(),
34
36
  new Preset(),
35
37
  new Project(),
36
38
  new Query(),
package/src/init.js CHANGED
@@ -21,7 +21,7 @@ class Init extends Command {
21
21
  protocol: await Ask.askBoolean('Enable HTTPS Protocol?') ? 'https' : 'http',
22
22
  username: (await Ask.askString('Please provide your jira username:')).trim(),
23
23
  password: (await Ask.askPassword('API token:')).trim(),
24
- apiVersion: '2',
24
+ apiVersion: '3',
25
25
  strictSSL: true,
26
26
  },
27
27
  presets: {},
package/src/jira.js CHANGED
@@ -20,6 +20,11 @@ class Jira {
20
20
  }
21
21
 
22
22
  this._config = JSON.parse(fs.readFileSync(this.configFile));
23
+ if (this._config.jira.apiVersion !== 3) {
24
+ this._config.jira.apiVersion = 3;
25
+ this.syncConfig();
26
+ }
27
+
23
28
  this._jiraClient = new jiraClient(this._config.jira);
24
29
  }
25
30
 
package/src/label.js ADDED
@@ -0,0 +1,103 @@
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ import color from 'chalk';
6
+
7
+ import Ask from './ask.js';
8
+ import Command from './command.js';
9
+ import Field from './field.js';
10
+ import Issue from './issue.js';
11
+ import Jira from './jira.js';
12
+ import Project from './project.js';
13
+ import Table from './table.js';
14
+ import User from './user.js';
15
+
16
+ class Label extends Command {
17
+ addOptions(program) {
18
+ const labelCmd = program.command('label')
19
+ .description('Play with labels');
20
+ labelCmd.command('add')
21
+ .description('Add a label to an issue')
22
+ .argument('<id>', 'The issue ID')
23
+ .action(async id => {
24
+ const jira = new Jira(program);
25
+ const resultFields = await Field.listFields(jira);
26
+ const result = await jira.spin('Running query...', jira.api.findIssue(id));
27
+ const issue = Issue.replaceFields(result, resultFields);
28
+
29
+ const meta = await Project.metadata(jira, issue.fields['Project'].key, issue.fields['Issue Type'].name);
30
+ const field = meta.projects.find(p => p.key === issue.fields['Project'].key)
31
+ .issuetypes.find(i => i.name === issue.fields['Issue Type'].name).fields['labels'];
32
+ if (!field) {
33
+ console.log("This type does not support labels");
34
+ return;
35
+ }
36
+
37
+ const label = await Ask.askCallback("Add label:", async input => {
38
+ if (!input) return [];
39
+ const data = await jira.api.doRequest({
40
+ url: field.autoCompleteUrl + input
41
+ })
42
+ return JSON.parse(data).suggestions.map(label => label.label);
43
+ });
44
+
45
+ const labels = issue.fields['Labels'] || [];
46
+ if (labels.includes(label)) {
47
+ console.log("Duplicate label");
48
+ return;
49
+ }
50
+
51
+ labels.push(label);
52
+
53
+ const data = {};
54
+ data[field.key] = labels;
55
+
56
+ await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, {
57
+ fields: {
58
+ ...data
59
+ }
60
+ }));
61
+ });
62
+
63
+ labelCmd.command('remove')
64
+ .description('Remove a label from an issue')
65
+ .argument('<id>', 'The issue ID')
66
+ .action(async id => {
67
+ const jira = new Jira(program);
68
+ const resultFields = await Field.listFields(jira);
69
+ const result = await jira.spin('Running query...', jira.api.findIssue(id));
70
+ const issue = Issue.replaceFields(result, resultFields);
71
+
72
+ const meta = await Project.metadata(jira, issue.fields['Project'].key, issue.fields['Issue Type'].name);
73
+ const field = meta.projects.find(p => p.key === issue.fields['Project'].key)
74
+ .issuetypes.find(i => i.name === issue.fields['Issue Type'].name).fields['labels'];
75
+ if (!field) {
76
+ console.log("This type does not support labels");
77
+ return;
78
+ }
79
+
80
+ let labels = issue.fields['Labels'] || [];
81
+ if (labels.length === 0) {
82
+ console.log("No labels");
83
+ return;
84
+ }
85
+
86
+ let label = await Ask.askList("Remove label:", labels.map(label => ({
87
+ name: label,
88
+ value: label
89
+ })));
90
+
91
+ const data = {};
92
+ data[field.key] = labels.filter(l => label != l)
93
+
94
+ await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, {
95
+ fields: {
96
+ ...data
97
+ }
98
+ }));
99
+ });
100
+ }
101
+ };
102
+
103
+ export default Label;
package/src/query.js CHANGED
@@ -149,12 +149,18 @@ class Query extends Command {
149
149
 
150
150
  static async runQuery(jira, query, expectedResult) {
151
151
  let issues = [];
152
- let total = 0;
152
+ let nextPageToken = null;
153
153
  while (issues.length < (expectedResult === undefined ? issues.length + 1 : expectedResult)) {
154
154
  const result = await jira.spin('Running query...',
155
- jira.api.searchJira(query, {
156
- startAt: issues.length,
157
- maxResults: expectedResult - issues.length
155
+ jira.apiRequest('/search/jql', {
156
+ method: 'POST',
157
+ followAllRedirects: true,
158
+ body: {
159
+ jql: query,
160
+ nextPageToken,
161
+ fields: ['*all'],
162
+ maxResults: Math.min(5000, expectedResult - issues.length)
163
+ }
158
164
  }));
159
165
 
160
166
  if (result.warningMessages) {
@@ -162,14 +168,14 @@ class Query extends Command {
162
168
  return;
163
169
  }
164
170
 
165
- total = result.total;
166
171
  issues = issues.concat(result.issues);
172
+ nextPageToken = result.nextPageToken;
167
173
 
168
- if (issues.length >= total) break;
174
+ if (result.isLast) break;
169
175
  }
170
176
 
171
177
  return {
172
- total,
178
+ total: issues.length,
173
179
  issues
174
180
  };
175
181
  }
package/src/run.js CHANGED
@@ -17,7 +17,7 @@ class Run extends Command {
17
17
  .argument('<name>', 'The preset name')
18
18
  .option('-q, --query <query...>')
19
19
  .option('-l, --limit <limit>',
20
- `Set the query limit. Default ${DEFAULT_QUERY_LIMIT}`,
20
+ `Set the query limit`,
21
21
  DEFAULT_QUERY_LIMIT)
22
22
  .option('-g, --grouped', 'Group the issues by parenting')
23
23
  .action(async name => {
package/src/set.js CHANGED
@@ -66,7 +66,7 @@ class Set extends Command {
66
66
  return;
67
67
  }
68
68
 
69
- await Set.setCustomField(jira, customField, id);
69
+ await Set.setCustomField(jira, customField, id, issue.fields[customField.fieldName]);
70
70
  });
71
71
  }
72
72
 
@@ -90,8 +90,8 @@ class Set extends Command {
90
90
  await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, issue));
91
91
  }
92
92
 
93
- static async setCustomField(jira, customField, id) {
94
- const field = await Field.fetchAndAskFieldIfSupported(jira, customField);
93
+ static async setCustomField(jira, customField, id, defaultValue = null) {
94
+ const field = await Field.fetchAndAskFieldIfSupported(jira, customField, defaultValue);
95
95
  if (field === null) {
96
96
  console.log("Unsupported field type");
97
97
  return;
package/src/sprint.js CHANGED
@@ -156,10 +156,14 @@ class Sprint extends Command {
156
156
  value: board.id
157
157
  })));
158
158
 
159
- const sprintList = await jira.spin('Retrieving sprints...',
160
- jira.api.getAllSprints(boardId));
159
+ let sprintList = [];
160
+ while (true) {
161
+ const sprints = await jira.spin('Retrieving sprints...', jira.api.getAllSprints(boardId, sprintList.length));
162
+ sprintList = sprintList.concat(sprints.values);
163
+ if (sprintList.length >= sprints.total) break;
164
+ }
161
165
 
162
- const sprints = sprintList.values.filter(sprint => sprint.state === 'active' || sprint.state === 'future');
166
+ const sprints = sprintList.filter(sprint => sprint.state === 'active' || sprint.state === 'future');
163
167
 
164
168
  if (sprints.length === 0) {
165
169
  return null;