bjira 0.0.16 → 0.0.20

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/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Andrea Marchesini <baku@bnode.dev>
3
+ Copyright (c) 2021-2022 Andrea Marchesini <baku@bnode.dev>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -15,11 +15,11 @@ jira settings.
15
15
 
16
16
  ```
17
17
  $ bjira init
18
- ? Provide your jira host: your-config.atlassian.net
18
+ ? Provide your jira host: your-domain.atlassian.net
19
19
  ? Please provide your jira username: username
20
20
  ? API token: [hidden]
21
21
  ? Enable HTTPS Protocol? Yes
22
- Config file succesfully created in: /home/baku/.bjira.json
22
+ Config file succesfully created in: /home/<username>/.bjira.json
23
23
  ```
24
24
 
25
25
  ## How to use it
@@ -63,10 +63,11 @@ Jira is strongly configurable via custom fields. You can retrieve the list of cu
63
63
  bjira field listall
64
64
  ```
65
65
 
66
- If you want to see some of them in the issue report, add them:
66
+ If you want to see some of them in the issue report, add them for the project (FOO) and the issue type (Story):
67
67
 
68
68
  ```
69
- bjira field add "Story Points"
69
+ bjira field add FOO Story "Story Points"
70
70
  ```
71
71
 
72
72
  Any custom fields added to the list will be shown in the issue report (See `bjira show`).
73
+ You can also set custom fields using `bira set custom `Story Points' ISSUE-ID`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bjira",
3
- "version": "0.0.16",
3
+ "version": "0.0.20",
4
4
  "description": "A simple jira CLI tool",
5
5
  "main": "src/index.js",
6
6
  "author": {
package/src/ask.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -0,0 +1,37 @@
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ import Command from './command.js';
6
+ import Field from './field.js';
7
+ import Issue from './issue.js';
8
+ import Jira from './jira.js';
9
+ import Utils from './utils.js';
10
+
11
+ class Attachment extends Command {
12
+ addOptions(program) {
13
+ const cmd = program.command('attachment')
14
+ .description('Play with attachments');
15
+ cmd.command('get')
16
+ .description('Get the attachment')
17
+ .argument('<issueID>', 'The issue ID')
18
+ .argument('<attachmentID>', 'The attachment ID')
19
+ .action(async (issueId, attachmentId) => {
20
+ const jira = new Jira(program);
21
+
22
+ const resultFields = await Field.listFields(jira);
23
+
24
+ const result = await jira.spin('Running query...', jira.api.findIssue(issueId));
25
+ const issue = Issue.replaceFields(result, resultFields);
26
+
27
+ const attachment = issue.fields['Attachment'].find(attachment => attachment.id === attachmentId);
28
+ const attachmentData = await jira.spin('Retriving attachment...', jira.api.downloadAttachment(attachment));
29
+ process.stdout.write(attachmentData);
30
+ });
31
+
32
+ // TODO: delete attachment
33
+ // TODO: upload attachment
34
+ }
35
+ };
36
+
37
+ export default Attachment;
package/src/command.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/comment.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/create.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -33,8 +33,8 @@ class Create extends Command {
33
33
  }
34
34
  })));
35
35
 
36
- const meta = await jira.spin('Retrieving issue metadata...',
37
- jira.apiRequest(`/issue/createmeta?projectKeys=${project.key}&issuetypeNames=${issueType.name}&expand=projects.issuetypes.fields`));
36
+ const meta = await Project.metadata(jira, project.key, issueType.name);
37
+
38
38
  const newIssue = {
39
39
  fields: {
40
40
  project: {
@@ -58,11 +58,12 @@ class Create extends Command {
58
58
  .issuetypes.find(i => i.name === issueType.name).fields;
59
59
  const requiredFields = Object.keys(issueFields).filter(
60
60
  key => issueFields[key].required &&
61
- Field.isSupported(issueFields[key].schema.type) &&
62
- !["summary", "description", "project"].includes(key)).map(key => issueFields[key]);
61
+ Field.isSupported(issueFields[key]) &&
62
+ !["issuetype", "summary", "description", "project"].includes(key)).map(key => issueFields[key]);
63
63
 
64
64
  for (const field of requiredFields) {
65
- newIssue.fields[field.key] = await Ask.askString(`${field.name}:`);
65
+ const fieldData = await Field.askFieldIfSupported(field);
66
+ newIssue.fields[fieldData.key] = fieldData.value;
66
67
  }
67
68
 
68
69
  if (issueType.name !== 'Epic' &&
@@ -100,10 +101,12 @@ class Create extends Command {
100
101
  await Set.setStatus(jira, issue.key);
101
102
  }
102
103
 
103
- if (jira.fields && jira.fields.length > 0 &&
104
- await Ask.askBoolean('Do you want to set custom fields?')) {
105
- for (let fieldName of jira.fields) {
106
- await Set.setCustomField(jira, fieldName, issue.key);
104
+ const customFields = jira.fields.filter(
105
+ field => field.projectName === project.key &&
106
+ field.issueTypeName === issueType.name);
107
+ if (customFields.length > 0 && await Ask.askBoolean('Do you want to set custom fields?')) {
108
+ for (let customField of customFields) {
109
+ await Set.setCustomField(jira, customField, issue.key);
107
110
  }
108
111
  }
109
112
 
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -20,6 +20,11 @@ class ErrorHandler {
20
20
  color: "blue",
21
21
  text: error
22
22
  }]));
23
+ } else if ("message" in e.error) {
24
+ table.addRow([{
25
+ color: "blue",
26
+ text: e.error.message
27
+ }]);
23
28
  } else {
24
29
  table.addRow([{
25
30
  color: "blue",
package/src/field.js CHANGED
@@ -1,10 +1,13 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
+ import color from 'chalk';
6
+
5
7
  import Ask from './ask.js';
6
8
  import Command from './command.js';
7
9
  import Jira from './jira.js';
10
+ import Project from './project.js';
8
11
  import Table from './table.js';
9
12
 
10
13
  class Field extends Command {
@@ -16,59 +19,76 @@ class Field extends Command {
16
19
  .action(async () => {
17
20
  const jira = new Jira(program);
18
21
 
19
- const resultFields = await Field.listFields(jira);
20
-
21
- const table = new Table({
22
- head: ['Name', 'Supported', 'Type']
23
- });
24
-
25
- resultFields.forEach(field => {
26
- const supported = Field.isSupported(field.schema?.type);
27
- table.addRow([{
28
- color: "blue",
29
- text: field.name
30
- }, supported, supported ? field.schema?.type : ""]);
22
+ const resultFields = await jira.spin('Retrieving the fields...',
23
+ jira.apiRequest('/issue/createmeta?expand=projects.issuetypes.fields'));
24
+
25
+ resultFields.projects.forEach(project => {
26
+ console.log(`\nProject: ${color.blue(project.name)} (${color.blue(project.key)})`);
27
+ project.issuetypes.forEach(issueType => {
28
+ console.log(`\nIssue type: ${color.yellow(issueType.name)}`);
29
+
30
+ const table = new Table({
31
+ head: ['Name', 'Supported', 'Type']
32
+ });
33
+
34
+ for (const fieldName in issueType.fields) {
35
+ const field = issueType.fields[fieldName];
36
+ const supported = Field.isSupported(field);
37
+ table.addRow([{
38
+ color: "blue",
39
+ text: field.name
40
+ }, supported, supported ? field.schema?.type : ""]);
41
+ }
42
+
43
+ console.log(table.toString());
44
+ });
31
45
  });
32
- console.log(table.toString());
33
46
  });
34
47
 
35
48
  cmd.command("add")
36
49
  .description("Add a custom field to be shown")
50
+ .argument('<project>', 'The project name')
51
+ .argument('<issueType>', 'The issue type')
37
52
  .argument('<field>', 'The field name')
38
- .action(async fieldName => {
53
+ .action(async (projectName, issueTypeName, fieldName) => {
39
54
  const jira = new Jira(program);
40
55
 
41
- const resultFields = await Field.listFields(jira);
42
-
43
- const fieldData = resultFields.find(field => field.name === fieldName);
44
- if (!fieldData) {
45
- console.log("Unknown field.");
56
+ const meta = await Project.metadata(jira, projectName, issueTypeName);
57
+ const issueType = meta.projects.find(p => p.key === projectName)
58
+ .issuetypes.find(i => i.name === issueTypeName);
59
+ if (!issueType) {
60
+ console.log(`Issue type ${issueTypeName} does not exist.`);
46
61
  return;
47
62
  }
48
63
 
49
- if (!Field.isSupported(fieldData.schema?.type)) {
50
- console.log("Unsupported field.");
64
+ for (const name in issueType.fields) {
65
+ const field = issueType.fields[name];
66
+ if (field.name !== fieldName) continue;
67
+
68
+ if (!Field.isSupported(field)) {
69
+ console.log("Unsupported field.");
70
+ return;
71
+ }
72
+
73
+ jira.addField(projectName, issueTypeName, fieldName);
74
+ jira.syncConfig();
75
+
76
+ console.log('Config file succesfully updated');
51
77
  return;
52
78
  }
53
79
 
54
- jira.addField(fieldName);
55
- jira.syncConfig();
56
-
57
- console.log('Config file succesfully updated');
80
+ console.log(`Field ${fieldName} does not exist in Issue type ${issueTypeName} for project ${projectName}`);
58
81
  });
59
82
 
60
83
  cmd.command("remove")
61
84
  .description("Remove a custom field")
85
+ .argument('<project>', 'The project name')
86
+ .argument('<issueType>', 'The issue type')
62
87
  .argument('<field>', 'The field name')
63
- .action(async fieldName => {
88
+ .action(async (projectName, issueTypeName, fieldName) => {
64
89
  const jira = new Jira(program);
65
90
 
66
- if (!jira.fields.includes(fieldName)) {
67
- console.log("Unknown field.");
68
- return;
69
- }
70
-
71
- jira.removeField(fieldName);
91
+ jira.removeField(projectName, issueTypeName, fieldName);
72
92
  jira.syncConfig();
73
93
 
74
94
  console.log('Config file succesfully updated');
@@ -80,12 +100,17 @@ class Field extends Command {
80
100
  const jira = new Jira(program);
81
101
 
82
102
  const table = new Table({
83
- head: ['Name']
103
+ head: ['Project', 'Issue Type', 'Name']
84
104
  });
85
105
 
86
- jira.fields.forEach(fieldName => table.addRow([{
106
+ jira.fields.forEach(field => table.addRow([{
87
107
  color: "blue",
88
- text: fieldName
108
+ text: field.projectName,
109
+ }, {
110
+ color: "yellow",
111
+ text: field.issueTypeName
112
+ }, {
113
+ text: field.fieldName
89
114
  }]));
90
115
  console.log(table.toString());
91
116
  });
@@ -95,24 +120,51 @@ class Field extends Command {
95
120
  return await jira.spin('Retrieving the fields...', jira.api.listFields());
96
121
  }
97
122
 
98
- static isSupported(fieldType) {
99
- return ["string", "number"].includes(fieldType);
100
- }
123
+ static isSupported(fieldData) {
124
+ if (["string", "number"].includes(fieldData.schema?.type)) {
125
+ return true;
126
+ }
101
127
 
102
- static async askFieldIfSupported(jira, fieldName) {
103
- const resultFields = await Field.listFields(jira);
128
+ if ("allowedValues" in fieldData) {
129
+ return true;
130
+ }
104
131
 
105
- let fieldData;
106
- resultFields.forEach(field => {
107
- if (field.name === fieldName) fieldData = field;
108
- });
132
+ return false;
133
+ }
109
134
 
110
- if (!fieldData) {
111
- console.log(`Unable to find the field "${fieldName}"`);
135
+ static fieldValue(field, fieldData) {
136
+ if (!Field.isSupported(fieldData)) {
112
137
  return null;
113
138
  }
114
139
 
115
- if (!Field.isSupported(fieldData.schema?.type)) {
140
+ let type;
141
+ switch (fieldData.schema.type) {
142
+ case 'number':
143
+ return field;
144
+ case 'string':
145
+ return field;
146
+ }
147
+
148
+ return field.name;
149
+ }
150
+
151
+ static async fetchAndAskFieldIfSupported(jira, field) {
152
+ const meta = await Project.metadata(jira, field.projectName, field.issueTypeName);
153
+ const fields = meta.projects.find(p => p.key === field.projectName)
154
+ .issuetypes.find(i => i.name === field.issueTypeName).fields;
155
+
156
+ for (const name in fields) {
157
+ const fieldObj = fields[name];
158
+ if (fieldObj.name === field.fieldName) {
159
+ return Field.askFieldIfSupported(fieldObj);
160
+ }
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ static async askFieldIfSupported(fieldData) {
167
+ if (!Field.isSupported(fieldData)) {
116
168
  console.log("Unsupported field");
117
169
  return null;
118
170
  }
@@ -121,15 +173,30 @@ class Field extends Command {
121
173
  switch (fieldData.schema.type) {
122
174
  case 'number':
123
175
  return {
124
- value: await Ask.askNumber(`${fieldName}:`), key: fieldData.key
176
+ value: await Ask.askNumber(`${fieldData.name}:`), key: fieldData.key
125
177
  };
126
178
  case 'string':
127
179
  return {
128
- value: await Ask.askString(`${fieldName}:`), key: fieldData.key
180
+ value: await Ask.askString(`${fieldData.name}:`), key: fieldData.key
129
181
  };
130
182
  }
131
183
 
132
- return null;
184
+ let value = await Ask.askList(`${fieldData.name}:`,
185
+ fieldData.allowedValues.map(value => ({
186
+ name: value.name,
187
+ value: {
188
+ id: value.id
189
+ }
190
+ })));
191
+
192
+ if (fieldData.schema.type === 'array') {
193
+ value = [value];
194
+ }
195
+
196
+ return {
197
+ key: fieldData.key,
198
+ value,
199
+ };
133
200
  }
134
201
  };
135
202
 
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
3
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
4
4
  //
5
5
  // SPDX-License-Identifier: MIT
6
6
 
@@ -9,6 +9,7 @@ import fs from 'fs';
9
9
  import os from 'os';
10
10
  import path from 'path';
11
11
 
12
+ import Attachment from './attachment.js';
12
13
  import Comment from './comment.js';
13
14
  import Create from './create.js';
14
15
  import Field from './field.js';
@@ -24,6 +25,7 @@ import Sprint from './sprint.js';
24
25
  const DEFAULT_CONFIG_FILE = path.join(os.homedir(), ".bjira.json")
25
26
 
26
27
  const commands = [
28
+ new Attachment(),
27
29
  new Comment(),
28
30
  new Create(),
29
31
  new Field(),
package/src/init.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/issue.js CHANGED
@@ -1,10 +1,12 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
5
  import Command from './command.js';
6
6
  import Field from './field.js';
7
7
  import Jira from './jira.js';
8
+ import Project from './project.js';
9
+ import Query from './query.js';
8
10
  import Table from './table.js';
9
11
 
10
12
  const DEFAULT_QUERY_LIMIT = 20;
@@ -17,7 +19,9 @@ class Issue extends Command {
17
19
  addOptions(program) {
18
20
  const cmd = program.command('show')
19
21
  .description('Show an issue')
22
+ .option('-a, --attachments', 'Show the attachments too')
20
23
  .option('-C, --comments', 'Show the comments too')
24
+ .option('-s, --subissues', 'Show the comments too')
21
25
  .argument('<id>', 'The issue ID')
22
26
  .action(async id => {
23
27
  const jira = new Jira(program);
@@ -64,9 +68,6 @@ class Issue extends Command {
64
68
  [
65
69
  'Assignee', Issue.showUser(issue.fields['Assignee'])
66
70
  ],
67
- [
68
- 'Priority', issue.fields['Priority'].name
69
- ],
70
71
  [
71
72
  'Epic Link', {
72
73
  color: "yellow",
@@ -84,7 +85,22 @@ class Issue extends Command {
84
85
  ]
85
86
  ]);
86
87
 
87
- jira.fields.forEach(fieldName => table.addRow([fieldName, issue.fields[fieldName] || "unset"]));
88
+ const customFields = jira.fields.filter(
89
+ field => field.projectName === issue.fields['Project'].key &&
90
+ field.issueTypeName === issue.fields['Issue Type'].name);
91
+ if (customFields.length > 0) {
92
+ const meta = await Project.metadata(jira, issue.fields['Project'].key, issue.fields['Issue Type'].name);
93
+ customFields.forEach(field => {
94
+ const fields = meta.projects.find(p => p.key === field.projectName)
95
+ .issuetypes.find(i => i.name === field.issueTypeName).fields;
96
+ for (const name in fields) {
97
+ const fieldObj = fields[name];
98
+ if (fieldObj.name === field.fieldName) {
99
+ table.addRow([field.fieldName, Field.fieldValue(issue.fields[field.fieldName], fieldObj) || "unset"]);
100
+ }
101
+ }
102
+ });
103
+ }
88
104
 
89
105
  table.addRows([
90
106
  [
@@ -105,11 +121,45 @@ class Issue extends Command {
105
121
  [
106
122
  '', ''
107
123
  ],
124
+ [
125
+ 'Attachments', issue.fields['Attachment'].length
126
+ ],
108
127
  [
109
128
  'Comments', issue.fields['Comment'].total
110
- ]
129
+ ],
111
130
  ]);
112
131
 
132
+ if (cmd.opts().attachments) {
133
+ issue.fields['Attachment'].forEach(attachment => {
134
+ table.addRows([
135
+ [
136
+ '', ''
137
+ ],
138
+ [
139
+ 'Attachment', {
140
+ color: "yellow",
141
+ text: attachment.id
142
+ }
143
+ ],
144
+ [
145
+ 'Filename', attachment.filename
146
+ ],
147
+ [
148
+ 'Author', Issue.showUser(attachment.author)
149
+ ],
150
+ [
151
+ 'Size', attachment.size
152
+ ],
153
+ [
154
+ 'Mime-type', attachment.mimeType
155
+ ],
156
+ [
157
+ 'Created on', attachment['Created']
158
+ ],
159
+ ]);
160
+ });
161
+ }
162
+
113
163
  if (cmd.opts().comments) {
114
164
  issue.fields['Comment'].comments.forEach(comment => {
115
165
  table.addRows([
@@ -133,12 +183,25 @@ class Issue extends Command {
133
183
  ],
134
184
  [
135
185
  'Body', comment.body
136
- ]
186
+ ],
137
187
  ]);
138
188
  });
139
189
  }
140
190
 
141
191
  console.log(table.toString());
192
+
193
+ if (cmd.opts().subissues) {
194
+ console.log("\nSub-issues:");
195
+ const children = await Query.runQuery(jira, `parent = "${id}"`, 999999);
196
+ await Query.showIssues(jira, children.issues, children.total, resultFields, false);
197
+
198
+ if (issue.fields['Issue Type'].name === 'Epic') {
199
+ console.log("\nEpic issues:");
200
+ const children = await jira.spin('Fetching child issues...', jira.api.getIssuesForEpic(id));
201
+ await Query.showIssues(jira, children.issues, children.total, resultFields, false);
202
+ }
203
+ }
204
+
142
205
  });
143
206
  }
144
207
 
package/src/jira.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -31,16 +31,27 @@ class Jira {
31
31
  this._config.latestProject = latestProject;
32
32
  }
33
33
 
34
- addField(fieldName) {
34
+ addField(projectName, issueTypeName, fieldName) {
35
35
  if (!Array.isArray(this._config.fields)) this._config.fields = [];
36
36
 
37
- this._config.fields.push(fieldName);
37
+ if (!this._config.fields.find(field => field.projectName === projectName &&
38
+ field.issueTypeName === issueTypeName && field.fieldName === fieldName)) {
39
+ this._config.fields.push({
40
+ projectName,
41
+ issueTypeName,
42
+ fieldName
43
+ });
44
+ }
38
45
  }
39
46
 
40
- removeField(fieldName) {
41
- if (!Array.isArray(this._config.fields) || this._config.fields.indexOf(fieldName) === -1) return;
47
+ removeField(projectName, issueTypeName, fieldName) {
48
+ if (!Array.isArray(this._config.fields)) return;
42
49
 
43
- this._config.fields.splice(this._config.fields.indexOf(fieldName), 1);
50
+ const pos = this._config.fields.findIndex(field => field.projectName === projectName &&
51
+ field.issueTypeName === issueTypeName && field.fieldName === fieldName);
52
+ if (pos !== -1) {
53
+ this._config.fields.splice(pos, 1);
54
+ }
44
55
  }
45
56
 
46
57
  get fields() {
package/src/preset.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/project.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -73,6 +73,11 @@ class Project extends Command {
73
73
  issueTypes: project.issuetypes,
74
74
  };
75
75
  }
76
+
77
+ static async metadata(jira, project, issueType) {
78
+ return await jira.spin('Retrieving the fields...',
79
+ jira.apiRequest(`/issue/createmeta?projectKeys=${project}&issuetypeNames=${issueType}&expand=projects.issuetypes.fields`));
80
+ }
76
81
  };
77
82
 
78
83
  export default Project;
package/src/query.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -21,6 +21,7 @@ class Query extends Command {
21
21
  .option('-l, --limit <limit>',
22
22
  `Set the query limit. Default ${DEFAULT_QUERY_LIMIT}`,
23
23
  DEFAULT_QUERY_LIMIT)
24
+ .option('-g, --grouped', 'Group the issues by parenting')
24
25
  .action(async query => {
25
26
  const jira = new Jira(program);
26
27
  const opts = cmd.opts();
@@ -28,32 +29,120 @@ class Query extends Command {
28
29
  const resultFields = await Field.listFields(jira);
29
30
  const result = await Query.runQuery(jira, query, opts.limit);
30
31
 
31
- Query.showIssues(jira, result.issues, result.total, resultFields);
32
+ await Query.showIssues(jira, result.issues, result.total, resultFields, opts.grouped);
32
33
  });
33
34
  }
34
35
 
35
- static showIssues(jira, issues, total, fields) {
36
+ static async showIssues(jira, issues, total, fields, grouped) {
36
37
  console.log(`Showing ${color.bold(issues.length)} issues of ${color.bold(total)}`);
37
38
 
39
+ issues = issues.map(issue => Issue.replaceFields(issue, fields)).map(issue => ({
40
+ children: [],
41
+ issue
42
+ }));
43
+
44
+ const addIssueInTree = (issues, issue, parentId) => {
45
+ for (const existingIssue of issues) {
46
+ if (existingIssue.issue.key == parentId) {
47
+ existingIssue.children.push(issue);
48
+ return true;
49
+ }
50
+
51
+ if (addIssueInTree(existingIssue.children, issue, parentId)) {
52
+ return true;
53
+ }
54
+ }
55
+
56
+ return false;
57
+ };
58
+
59
+ const computeIssueInTree = async (issues, newIssues, issue) => {
60
+ // Top level.
61
+ if (!issue.issue.fields['Epic Link'] && !issue.issue.fields['Parent']) {
62
+ newIssues.push(issue);
63
+ return;
64
+ }
65
+
66
+ const parentId = issue.issue.fields['Epic Link'] || issue.issue.fields['Parent'].key;
67
+
68
+ // In the already processed issues
69
+ if (addIssueInTree(newIssues, issue, parentId)) {
70
+ return;
71
+ }
72
+
73
+ // In the non-already processed issues
74
+ if (addIssueInTree(issues, issue, parentId)) {
75
+ return;
76
+ }
77
+
78
+ const parentIssueResult = await jira.spin('Running query...', jira.api.findIssue(parentId));
79
+ const parentIssue = Issue.replaceFields(parentIssueResult, fields);
80
+
81
+ const parentIssueData = {
82
+ children: [issue],
83
+ incompleted: true,
84
+ issue: parentIssue
85
+ };
86
+
87
+ await computeIssueInTree(issues, newIssues, parentIssueData);
88
+ };
89
+
90
+ if (grouped) {
91
+ const newIssues = [];
92
+ for (; issues.length; issues = issues.splice(1)) {
93
+ await computeIssueInTree(issues, newIssues, issues[0]);
94
+ }
95
+ issues = newIssues;
96
+ }
97
+
38
98
  const table = new Table({
39
- head: ['Key', 'Status', 'Type', 'Assignee', 'Summary']
99
+ head: ['Key', 'Status', 'Type', 'Assignee', 'Summary'],
100
+ unresizableColumns: [0],
40
101
  });
41
102
 
42
- issues.forEach(issue => table.addRow([{
103
+ const showIssue = (table, issue, nested) => {
104
+ let pre = "";
105
+ if (nested.length) {
106
+ pre += " ";
107
+ for (let i = 0; i < nested.length - 1; ++i) {
108
+ pre += (nested[i]) ? '│ ' : ' ';
109
+ }
110
+ pre += (nested[nested.length - 1]) ? "├─ " : "└─ ";
111
+ }
112
+
113
+ table.addRow([{
43
114
  color: "blue",
44
- text: issue.key
115
+ style: issue.incompleted ? "italic" : "normal",
116
+ text: pre + issue.issue.key
45
117
  }, {
46
118
  color: "green",
47
- text: issue.fields.status.name
119
+ style: issue.incompleted ? "italic" : "normal",
120
+ text: issue.issue.fields['Status'].name
48
121
  }, {
49
122
  color: "green",
50
- text: issue.fields.issuetype.name
123
+ style: issue.incompleted ? "italic" : "normal",
124
+ text: issue.issue.fields['Issue Type'].name
51
125
  }, {
52
126
  color: "yellow",
53
- text: Issue.showUser(issue.fields.assignee)
54
- },
55
- issue.fields.summary
56
- ]))
127
+ style: issue.incompleted ? "italic" : "normal",
128
+ text: Issue.showUser(issue.issue.fields['Assignee'])
129
+ }, {
130
+ style: issue.incompleted ? "italic" : "normal",
131
+ text: issue.issue.fields['Summary']
132
+ }]);
133
+
134
+ if (issue.children.length) {
135
+ const newNested = [];
136
+ nested.forEach(n => newNested.push(n));
137
+ newNested.push(false);
138
+ issue.children.forEach((subissue, pos) => {
139
+ newNested[nested.length] = (pos < issue.children.length - 1);
140
+ showIssue(table, subissue, newNested);
141
+ });
142
+ }
143
+ };
144
+
145
+ issues.forEach(issue => showIssue(table, issue, []));
57
146
 
58
147
  console.log(table.toString());
59
148
  }
package/src/run.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -19,6 +19,7 @@ class Run extends Command {
19
19
  .option('-l, --limit <limit>',
20
20
  `Set the query limit. Default ${DEFAULT_QUERY_LIMIT}`,
21
21
  DEFAULT_QUERY_LIMIT)
22
+ .option('-g, --grouped', 'Group the issues by parenting')
22
23
  .action(async name => {
23
24
  const jira = new Jira(program);
24
25
  const opts = cmd.opts();
@@ -46,7 +47,7 @@ class Run extends Command {
46
47
  const resultFields = await Field.listFields(jira);
47
48
  const result = await Query.runQuery(jira, query, opts.limit);
48
49
 
49
- Query.showIssues(jira, result.issues, result.total, resultFields);
50
+ await Query.showIssues(jira, result.issues, result.total, resultFields, opts.grouped);
50
51
  });
51
52
  }
52
53
 
package/src/set.js CHANGED
@@ -1,10 +1,11 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
5
  import Ask from './ask.js';
6
6
  import Command from './command.js';
7
7
  import Field from './field.js';
8
+ import Issue from './issue.js';
8
9
  import Jira from './jira.js';
9
10
  import User from './user.js';
10
11
 
@@ -14,7 +15,7 @@ class Set extends Command {
14
15
  .description('Update fields in an issue');
15
16
  setCmd.command('assignee')
16
17
  .description('Assign the issue to somebody')
17
- .argument('<id>', 'The issue ID')
18
+ .argument('<ID>', 'The issue ID')
18
19
  .action(async id => {
19
20
  const jira = new Jira(program);
20
21
  await Set.assignIssue(jira, id);
@@ -22,7 +23,7 @@ class Set extends Command {
22
23
 
23
24
  setCmd.command('unassign')
24
25
  .description('Unassign the issue')
25
- .argument('<id>', 'The issue ID')
26
+ .argument('<ID>', 'The issue ID')
26
27
  .action(async id => {
27
28
  const jira = new Jira(program);
28
29
 
@@ -34,12 +35,12 @@ class Set extends Command {
34
35
  }
35
36
  }
36
37
 
37
- await jira.spin('Updating the issue...', jira.api.updateIssue(id, issue));
38
+ await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, issue));
38
39
  });
39
40
 
40
41
  setCmd.command('status')
41
42
  .description('Change the status')
42
- .argument('<id>', 'The issue ID')
43
+ .argument('<ID>', 'The issue ID')
43
44
  .action(async id => {
44
45
  const jira = new Jira(program);
45
46
  await Set.setStatus(jira, id);
@@ -51,7 +52,21 @@ class Set extends Command {
51
52
  .argument('<id>', 'The issue ID')
52
53
  .action(async (fieldName, id) => {
53
54
  const jira = new Jira(program);
54
- await Set.setCustomField(jira, fieldName, id);
55
+
56
+ const resultFields = await Field.listFields(jira);
57
+ const result = await jira.spin('Running query...', jira.api.findIssue(id));
58
+ const issue = Issue.replaceFields(result, resultFields);
59
+
60
+ const customField = jira.fields.find(
61
+ field => field.projectName === issue.fields['Project'].key &&
62
+ field.issueTypeName === issue.fields['Issue Type'].name &&
63
+ field.fieldName === fieldName);
64
+ if (!customField) {
65
+ console.log("Unknown custom field");
66
+ return;
67
+ }
68
+
69
+ await Set.setCustomField(jira, customField, id);
55
70
  });
56
71
  }
57
72
 
@@ -72,12 +87,12 @@ class Set extends Command {
72
87
  }
73
88
  }
74
89
 
75
- await jira.spin('Updating the issue...', jira.api.updateIssue(id, issue));
90
+ await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, issue));
76
91
  }
77
92
 
78
- static async setCustomField(jira, fieldName, id) {
79
- const field = await Field.askFieldIfSupported(jira, fieldName);
80
- if (!field) {
93
+ static async setCustomField(jira, customField, id) {
94
+ const field = await Field.fetchAndAskFieldIfSupported(jira, customField);
95
+ if (field === null) {
81
96
  console.log("Unsupported field type");
82
97
  return;
83
98
  }
@@ -85,7 +100,7 @@ class Set extends Command {
85
100
  const data = {};
86
101
  data[field.key] = field.value;
87
102
 
88
- await jira.spin('Updating the issue...', jira.api.updateIssue(id, {
103
+ await jira.spin(`Updating issue ${id}...`, jira.api.updateIssue(id, {
89
104
  fields: {
90
105
  ...data
91
106
  }
@@ -94,18 +109,33 @@ class Set extends Command {
94
109
 
95
110
  static async setStatus(jira, id) {
96
111
  const transitionList = await jira.spin('Retrieving transitions...', jira.api.listTransitions(id));
97
- const transitionId = await Ask.askList('Status:',
98
- transitionList.transitions.map(transition => ({
112
+ const transitionData = await Ask.askList('Status:',
113
+ transitionList.transitions.filter(transition => transition.isAvailable).map(transition => ({
99
114
  name: transition.name,
100
- value: transition.id
115
+ value: transition
101
116
  })));
117
+
102
118
  const transition = {
103
119
  transition: {
104
- id: transitionId
120
+ id: transitionData.id
105
121
  }
106
122
  };
107
123
 
108
- await jira.spin('Updating the issue...', jira.api.transitionIssue(id, transition));
124
+ for (const field of Object.keys(transitionData.fields)) {
125
+ const fieldData = transitionData.fields[field];
126
+
127
+ if (!fieldData.required) continue;
128
+
129
+ if (!Field.isSupported(fieldData)) {
130
+ console.log(`Field ${field} is required but it's not supported`);
131
+ return;
132
+ }
133
+
134
+ const fieldValue = await Field.askFieldIfSupported(fieldData);
135
+ transition.transition[fieldValue.key] = fieldValue.value;
136
+ }
137
+
138
+ await jira.spin(`Updating issue ${id}...`, jira.api.transitionIssue(id, transition));
109
139
  }
110
140
  };
111
141
 
package/src/sprint.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/table.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
@@ -12,6 +12,8 @@ class Table {
12
12
  options.head?.forEach(head => {
13
13
  this._columns.push(this._createColumn(head, 0));
14
14
  });
15
+
16
+ this._unresizableColumns = options.unresizableColumns || [];
15
17
  }
16
18
 
17
19
  addRow(row) {
@@ -58,7 +60,7 @@ class Table {
58
60
  const rowHeight = Math.max(...this._columns.map(column => this._computeRowHeight(column, row)));
59
61
 
60
62
  for (let i = 0; i < rowHeight; ++i) {
61
- lines.push(this._columns.map(column => this._colorize(column.rows[row], this._toWidth(this._computeLine(column.rows[row], i), column.width))).join(" "));
63
+ lines.push(this._columns.map(column => this._stylize(column.rows[row], this._toWidth(this._computeLine(column.rows[row], i), column.width))).join(" "));
62
64
  }
63
65
  }
64
66
 
@@ -118,7 +120,7 @@ class Table {
118
120
  }
119
121
 
120
122
  _maybeSplitRow(row, width) {
121
- if (row.length < width) return [row];
123
+ if (row.length <= width) return [row];
122
124
 
123
125
  const rows = [];
124
126
  let currentRow = "";
@@ -142,8 +144,9 @@ class Table {
142
144
 
143
145
  _resizeWidthOfOne() {
144
146
  const max = Math.max(...this._columns.map(column => column.width));
145
- for (let column of this._columns) {
146
- if (column.width === max) {
147
+ for (let columnId in this._columns) {
148
+ const column = this._columns[columnId];
149
+ if (!this._unresizableColumns.includes(columnId) && column.width === max) {
147
150
  --column.width;
148
151
  break;
149
152
  }
@@ -154,6 +157,14 @@ class Table {
154
157
  return str.slice(0, width);
155
158
  }
156
159
 
160
+ _stylize(row, text) {
161
+ if (!("style" in row) || row.style === "normal") {
162
+ return this._colorize(row, text);
163
+ }
164
+
165
+ return color[row.style].apply(null, [this._colorize(row, text)]);
166
+ }
167
+
157
168
  _colorize(row, text) {
158
169
  if (!("color" in row)) {
159
170
  return text;
package/src/user.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/src/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2021 Andrea Marchesini <baku@bnode.dev>
1
+ // SPDX-FileCopyrightText: 2021-2022 Andrea Marchesini <baku@bnode.dev>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
package/aqtinstall.log DELETED
File without changes