Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/shridhar-tl/jira-assistant
Browse files Browse the repository at this point in the history
… into package
  • Loading branch information
shridhar-tl committed Dec 24, 2024
2 parents 205afce + 6d058f3 commit 1626f6a
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 25 deletions.
15 changes: 12 additions & 3 deletions manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ modules:
label: View time tracker
resource: main
icon: https://app.jiraassistant.com/assets/icon_24.png
trigger:
- key: sprint-started-trigger
function: sprint-started-handler
events:
- avi:jira-software:started:sprint
filter:
ignoreSelf: true
onError: RECEIVE_AND_LOG
function:
- key: sprint-started-handler
handler: jcloud-events.onSprintStarted
- key: resolver
handler: jcloud.handler
providers:
Expand Down Expand Up @@ -71,16 +81,15 @@ permissions:
- read:issue-details:jira
- read:jql:jira
- read:jira-work
- write:jira-work
- read:board-scope:jira-software
- read:project:jira
- read:issue-meta:jira
- read:avatar:jira
- read:issue.changelog:jira
- write:jira-work
- write:sprint:jira-software
app:
id: ari:cloud:ecosystem::app/3864d3bc-aad3-4650-ac35-e15af61fd92d
features:
autoUserConsent: true
runtime:
name: nodejs20.x
remotes:
Expand Down
1 change: 1 addition & 0 deletions src/constants/api-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ApiUrls = {
rapidSprintList: "~/rest/greenhopper/1.0/sprintquery/{0}",
rapidSprintDetails: "~/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId={0}&sprintId={1}",
sprintListAll: "~/rest/greenhopper/1.0/integration/teamcalendars/sprint/list?jql=project+in+({0})",
getSprintProperty: "~/rest/agile/1.0/sprint/{0}/properties/{1}",
getSprintIssues: "~/rest/agile/1.0/sprint/{0}/issue",
bulkIssueChangelogs: '~/rest/api/3/changelog/bulkfetch',
rapidViews: "~/rest/greenhopper/1.0/rapidview",
Expand Down
59 changes: 59 additions & 0 deletions src/jcloud-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { asApp, route } from '@forge/api'

export async function onSprintStarted(event, context) {
if (event.eventType !== "avi:jira-software:started:sprint") {
console.error("Invalid event type received", event.eventType);
}

const sprintId = event.sprint.id;

if (!sprintId) {
console.error("onSprintStarted: Sprint id unavailable", sprintId);
}

const customFieldsResponse = await asApp().requestJira(route`/rest/api/3/field`);

const customFields = await customFieldsResponse.json();
const storyPointFieldId = customFields.find(field => field.name.toLowerCase() === "story points")?.id;

if (!storyPointFieldId) {
console.error("onSprintStarted: Story points field not found");
}

const response = await asApp().requestJira(route`/rest/agile/1.0/sprint/${sprintId}/issue?maxResults=1000&fields=key,${storyPointFieldId}`, {
headers: {
'Accept': 'application/json'
}
});

const { issues } = await response.json();

if (!issues?.length) {
console.error("No issues found in sprint", sprintId, issues);
return;
}

const keysMap = issues.reduce((keys, issue) => {
if (storyPointFieldId) {
keys[issue.key] = { sp: issue.fields?.[storyPointFieldId] ?? '' };
} else {
keys[issue.key] = false;
}
return keys;
}, {});

const saveRequest = await asApp().requestJira(route`/rest/agile/1.0/sprint/${sprintId}/properties/jaSprintStartInfo`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(keysMap)
});

if (saveRequest.ok) {
console.log("Issue keys stored successfully within sprint of id:", sprintId);
} else {
console.error("Failed to store issue keys within sprint of id:", sprintId, saveRequest.status, await saveRequest.text());
}
}
48 changes: 47 additions & 1 deletion src/services/jira-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,57 @@ export default class JiraService {
return this.$ajax.get(ApiUrls.rapidSprintDetails, rapidViewId, sprintId);
}

async getSprintProperty(sprintId, propertyKey) {
try {
const result = await this.$ajax.get(ApiUrls.getSprintProperty.format(sprintId, propertyKey));
return result?.value;
} catch (err) {
if (!err.error?.errorMessages?.[0]?.includes("does not exist")) {
console.error(`Failed to fetch property ${propertyKey} for sprint ${sprintId}:`, error);
}
return null;
}
}

async getSprintIssues(sprintIds, options) {
const { worklogStartDate, worklogEndDate, ...opts } = options || {};
const { worklogStartDate, worklogEndDate, includeRemoved, ...opts } = options || {};

const worker = async (sprintId) => {
const { issues } = await this.$ajax.get(ApiUrls.getSprintIssues.format(sprintId), opts);

try { // if includeRemoved is true, then fetch removed issues based on custom properties added in sprint and add them to the list
const removedIssues = includeRemoved ? await this.getSprintProperty(sprintId, 'jaSprintStartInfo') : null;
if (removedIssues) {
const removedIssueKeys = Object.keys(removedIssues);
let jql = `key in (${removedIssueKeys.join(',')})`;

if (opts?.jql) {
jql = `(${opts.jql}) AND (${jql})`;
}

const closedTickets = await this.searchTickets(jql, options?.fields, 0, { ignoreErrors: true });
if (closedTickets?.length) {
const issuesMap = issues.reduce((obj, issue) => {
obj[issue.key] = issue;
return obj;
}, {});

closedTickets.forEach(t => {
let existingIssue = issuesMap[t.key];
if (!existingIssue) {
issues.push(t);
existingIssue = t;
}
if (removedIssues[t.key] && "sp" in removedIssues[t.key]) {
existingIssue.initialStoryPoints = parseFloat(removedIssues[t.key].sp) || 0;
}
});
}
}
} catch (e) {
console.error("Error trying to retrieve removed issues for sprint", sprintId, e);
}

if (options?.fields?.indexOf("worklog") > -1) {
await this.fillMissingWorklogs(issues, worklogStartDate, worklogEndDate);
}
Expand Down
25 changes: 14 additions & 11 deletions src/services/sprint-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export default class SprintService {

const sprintWiseIssues = await this.$jira.getSprintIssues(sprintIds, {
jql: 'issuetype not in subTaskIssueTypes()',
fields: ['created', 'resolutiondate', storyPointFieldName]
fields: ['created', 'resolutiondate', storyPointFieldName],
includeRemoved: true
});

for (let index = 0; index < closedSprintLists.length; index++) {
Expand Down Expand Up @@ -58,16 +59,18 @@ export default class SprintService {
issue.addedToSprint = startDate.isBefore(issueCreated)
|| (firstSprintLog && !firstSprintLog.from.split(',').some(sid => parseInt(sid) === sprint.id));

issue.initialStoryPoints = parseInt(storyPoint) || 0;

if (allLogs?.length) {
const spLog = getFirstModifiedLog(modifiedWithinSprint, storyPointFieldName);
if (spLog) {
issue.initialStoryPoints = parseInt(spLog.fromString) || 0;
} else {
const spModifiedAfterSprint = allLogs.filter(log => log.fieldId === storyPointFieldName && moment(log.created).isAfter(completeDate))[0];
if (spModifiedAfterSprint) {
issue.initialStoryPoints = parseInt(spModifiedAfterSprint.fromString) || 0;
if (!('initialStoryPoints' in issue)) {
issue.initialStoryPoints = parseFloat(storyPoint) || 0;

if (allLogs?.length) {
const spLog = getFirstModifiedLog(modifiedWithinSprint, storyPointFieldName);
if (spLog) {
issue.initialStoryPoints = parseFloat(spLog.fromString) || 0;
} else {
const spModifiedAfterSprint = allLogs.filter(log => log.fieldId === storyPointFieldName && moment(log.created).isAfter(completeDate))[0];
if (spModifiedAfterSprint) {
issue.initialStoryPoints = parseFloat(spModifiedAfterSprint.fromString) || 0;
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/views/reports/pivot-report/editor/body/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function ReportParameters() {
<strong>Provide values for missing parameters to generate report:</strong>
<br />
{keys.map((key) => <div key={key} className="param-field py-3">
<label className="font-bold me-3">{parameters[key].name}: </label>
<label className="fw-bold me-3">{parameters[key].name}: </label>
<ParameterControl {...parameters[key]} />
</div>)}
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function FieldsList({ fields, title }) {
if (!items?.length) { return null; }

return (<div className="fields-list">
<div className="block font-bold">{title}</div>
<div className="block fw-bold">{title}</div>
{items.map((field, i) => (<Draggable key={i}
className="jira-field"
itemType="jira-field"
Expand Down
4 changes: 2 additions & 2 deletions src/views/reports/pivot-report/editor/controls/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function Source({ onDone }) {
applying a JQL filter to refine the results.
</p>

<label className="font-bold mt-3">Data source type:</label>
<label className="fw-bold mt-3">Data source type:</label>
<RadioButton className="block"
value={dataSourceType} defaultValue={1}
label="Use raw JQL to filter and pull issues list"
Expand All @@ -51,7 +51,7 @@ function Source({ onDone }) {
label="Pull issues for select sprints and apply JQL filter"
onChange={updateDataSourceType} disabled />

<label className="font-bold mt-3">JQL Query:</label>
<label className="fw-bold mt-3">JQL Query:</label>
<JQLEditor jql={jql} plugged onChange={updateJQL} />

<Button className="float-end me-2" icon="fa fa-arrow-right"
Expand Down
13 changes: 7 additions & 6 deletions src/views/reports/say-do-ratio/settings/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { isPluginBuild } from 'src/constants/build-info';
import SideBar from '../../pivot-report/editor/SideBar';
import RapidViewList from '../../../../components/RapidViewList';
import { Button, Checkbox, TextBox } from 'src/controls';
Expand Down Expand Up @@ -27,21 +28,21 @@ function ReportSettings({ settings: actualSettings, show, onHide, onDone }) {
<SideBar show={show} onHide={onHide} title="Report Config"
controls={null} width="500" contentClassName="p-0">
<div className="p-3">
<label className="font-bold pb-2 d-block">Select Sprint Boards:</label>
<label className="fw-bold pb-2 d-block">Select Sprint Boards:</label>
<RapidViewList value={settings.sprintBoards} multiple onChange={setSprintBoards} />
<span className="help-text d-block">
Select all the sprint boards for which you would like to view Say-Do Ratio report.
</span>
</div>
<div className="p-3">
<label className="font-bold pb-2 d-block">Number of Sprints:</label>
<label className="fw-bold pb-2 d-block">Number of Sprints:</label>
<TextBox value={settings.noOfSprints} field="noOfSprints" onChange={setNumeric} maxLength={2} />
<span className="help-text d-block">
Provide the number of sprints to be displayed in chart and table. Minimum value allowed is 3.
</span>
</div>
<div className="p-3">
<label className="font-bold pb-2 d-block">Number of Sprints for velocity:</label>
<label className="fw-bold pb-2 d-block">Number of Sprints for velocity:</label>
<TextBox value={settings.velocitySprints} field="velocitySprints" onChange={setNumeric} maxLength={1} />
<span className="help-text d-block">
Provide the number of sprints to be used for velocity calculation.
Expand All @@ -50,16 +51,16 @@ function ReportSettings({ settings: actualSettings, show, onHide, onDone }) {
</span>
</div>
{!settings?.storyPointField && <div className="p-3">
<label className="font-bold pb-2 d-block">Story Points field unavailable:</label>
<label className="fw-bold pb-2 d-block msg-error">Story Points field unavailable:</label>
Select value for "Story Points field" under General settings -&gt; "Default Values" tab.
Report cannot be generated without having "Story Points field" configured.
</div>}
<div className="p-3">
{!isPluginBuild && <div className="p-3">
<Checkbox checked={true} label="Do not show issues removed from sprint as committed" />
<div className="help-text d-block mt-1">
If an issue is removed from sprint before closing it, then it would not be considered as committed which impacts Sa-Do-Ratio.
</div>
</div>
</div>}
<div className="p-3">
<Checkbox checked={true} label="Include non working days in cycle time calculation" />
</div>
Expand Down

0 comments on commit 1626f6a

Please sign in to comment.