From 06357c5b95739e9923424026cb94383ae0d281e1 Mon Sep 17 00:00:00 2001 From: Shridhar TL Date: Tue, 24 Dec 2024 09:25:22 +0530 Subject: [PATCH 1/2] Implemented trigger for storing story points along with ticket number while starting the sprint --- manifest.yml | 15 +++++++-- src/constants/api-urls.js | 1 + src/jcloud-events.js | 59 ++++++++++++++++++++++++++++++++++ src/services/jira-service.js | 48 ++++++++++++++++++++++++++- src/services/sprint-service.js | 25 +++++++------- 5 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 src/jcloud-events.js diff --git a/manifest.yml b/manifest.yml index a1ab1df3..1e221aaf 100644 --- a/manifest.yml +++ b/manifest.yml @@ -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: @@ -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: diff --git a/src/constants/api-urls.js b/src/constants/api-urls.js index d86cdf6e..df1a8dc4 100644 --- a/src/constants/api-urls.js +++ b/src/constants/api-urls.js @@ -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", diff --git a/src/jcloud-events.js b/src/jcloud-events.js new file mode 100644 index 00000000..ea317d08 --- /dev/null +++ b/src/jcloud-events.js @@ -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()); + } +} \ No newline at end of file diff --git a/src/services/jira-service.js b/src/services/jira-service.js index 1e795e34..2df330a9 100644 --- a/src/services/jira-service.js +++ b/src/services/jira-service.js @@ -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); } diff --git a/src/services/sprint-service.js b/src/services/sprint-service.js index 21512bbd..d970a535 100644 --- a/src/services/sprint-service.js +++ b/src/services/sprint-service.js @@ -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++) { @@ -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; + } } } } From 6d058f39d7140c119db955de2fed0b81e9e05488 Mon Sep 17 00:00:00 2001 From: Shridhar TL Date: Tue, 24 Dec 2024 09:43:36 +0530 Subject: [PATCH 2/2] Minor class name changes done to solve font weight issue --- .../reports/pivot-report/editor/body/index.jsx | 2 +- .../pivot-report/editor/controls/fields/index.jsx | 2 +- .../reports/pivot-report/editor/controls/index.jsx | 4 ++-- src/views/reports/say-do-ratio/settings/index.jsx | 13 +++++++------ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/views/reports/pivot-report/editor/body/index.jsx b/src/views/reports/pivot-report/editor/body/index.jsx index c9104648..e2112e64 100644 --- a/src/views/reports/pivot-report/editor/body/index.jsx +++ b/src/views/reports/pivot-report/editor/body/index.jsx @@ -47,7 +47,7 @@ function ReportParameters() { Provide values for missing parameters to generate report:
{keys.map((key) =>
- +
)}
diff --git a/src/views/reports/pivot-report/editor/controls/fields/index.jsx b/src/views/reports/pivot-report/editor/controls/fields/index.jsx index 23130c42..b619393d 100644 --- a/src/views/reports/pivot-report/editor/controls/fields/index.jsx +++ b/src/views/reports/pivot-report/editor/controls/fields/index.jsx @@ -40,7 +40,7 @@ function FieldsList({ fields, title }) { if (!items?.length) { return null; } return (
-
{title}
+
{title}
{items.map((field, i) => ( - + - +