diff --git a/.gitignore b/.gitignore index d797e04..f2cab3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.project .idea/ /**/node_modules +.clasp.json \ No newline at end of file diff --git a/projects/gapps-jira-backlog-import/README.md b/projects/gapps-jira-backlog-import/README.md new file mode 100644 index 0000000..980395c --- /dev/null +++ b/projects/gapps-jira-backlog-import/README.md @@ -0,0 +1 @@ +# gapps-jira-backlog-import diff --git a/projects/gapps-jira-backlog-import/package.json b/projects/gapps-jira-backlog-import/package.json new file mode 100644 index 0000000..758d542 --- /dev/null +++ b/projects/gapps-jira-backlog-import/package.json @@ -0,0 +1,17 @@ +{ + "name": "gapps-jira-backlog-import", + "version": "0.1.0", + "scripts": { + "build": "echo \"Not yet implemented\"", + "test": "echo \"Not yet implemented\"", + "test:e2e": "echo \"Not yet implemented\"", + "deploy:pr": "echo \"Not yet implemented\"", + "deploy:develop": "echo \"Not yet implemented\"", + "deploy:staging": "echo \"Not yet implemented\"", + "deploy:production": "echo \"Not yet implemented\"" + }, + "devDependencies": { + }, + "dependencies": { + } +} diff --git a/projects/gapps-jira-backlog-import/src/Code.js b/projects/gapps-jira-backlog-import/src/Code.js new file mode 100644 index 0000000..f6b1a01 --- /dev/null +++ b/projects/gapps-jira-backlog-import/src/Code.js @@ -0,0 +1,123 @@ +function onOpen() { + const ui = SpreadsheetApp.getUi(); + ui.createMenu('Sprint Tracking') + .addItem('Import Jira Tickets', 'importJiraTickets') + .addToUi(); +} + +// Function to create Jira data +function createJiraData() { + // Access script properties to get Jira credentials and base URL + const scriptProperties = PropertiesService.getScriptProperties(); + const baseUrl = scriptProperties.getProperty('JIRA_BASE_URL'); + const username = scriptProperties.getProperty('JIRA_USERNAME'); + const apiToken = scriptProperties.getProperty('JIRA_API_TOKEN'); + const boardId = scriptProperties.getProperty('BOARD_ID'); + + // Ensure all required properties are available + if (!baseUrl || !username || !apiToken) { + throw new Error("Missing one or more Jira API credentials in script properties."); + } + + // Create an instance of the JiraAPI class + const jira = new JiraAPI(baseUrl, username, apiToken); + + // Define the JQL query to get the tickets for the current sprint + let sprintId; + + try { + const currentSprint = jira.fetchCurrentSprint(boardId); + sprintId = currentSprint.id; + Logger.log(`Current Sprint: ${currentSprint.name} (ID: ${currentSprint.id})`); + } catch (error) { + throw new Error(`Error fetching the current sprint: ${error.message}`); + } + + // Fetch issues using a JQL query + const jqlQuery = `sprint = ${sprintId} ORDER BY rank ASC`; // Replace 'rank' with your field ID if needed + let issues; + + try { + issues = jira.fetchIssuesByJQL(jqlQuery); + } catch (error) { + throw new Error(`Error fetching issues by JQL: ${error.message}`); + } + + // Create an array to hold the Jira data + const jiraData = [ + ["Ticket ID", "Summary", "Status", "Assignee", "Due Date", "Story Points", "Balance"] // Header row + ]; + + // Populate the array with issue data + let runningTotal = 0; + issues.forEach(issue => { + const storyPoints = issue.fields.customfield_10004 || 0; // Replace 'customfield_10002' with your Story Points field ID + runningTotal += storyPoints; + + jiraData.push([ + issue.key, + issue.fields.summary, + issue.fields.status.name, + issue.fields.assignee ? issue.fields.assignee.displayName : "", + issue.fields.duedate || "", + storyPoints, + runningTotal + ]); + }); + + return jiraData; +} + +function formatSprintSheet(sprintSheet, headerLength) { + // Apply styles to the column headers + const headerRange = sprintSheet.getRange(1, 1, 1, headerLength); + headerRange.setFontWeight("bold"); + headerRange.setBackground("#4CAF50"); // Green background + headerRange.setFontColor("white"); // White font for better contrast + + // Adjust the width of the "Summary" column + const summaryColumnIndex = 2; // "Summary" is in the second column + sprintSheet.setColumnWidth(summaryColumnIndex, 300); // Set width as needed + + // Apply subdued styling for rows with "Status" = "Done" + const lastRow = sprintSheet.getLastRow(); + const statusColumnIndex = 3; // "Status" is in the third column + + if (lastRow > 1) { // Check if there are any rows beyond the header + const statusRange = sprintSheet.getRange(2, statusColumnIndex, lastRow - 1, 1); // Status column, excluding the header + const statuses = statusRange.getValues(); + + for (let i = 0; i < statuses.length; i++) { + if (statuses[i][0] === "Done") { + const row = i + 2; // Row index starts from 2 (accounting for header row) + const rowRange = sprintSheet.getRange(row, 1, 1, headerLength); + rowRange.setFontColor("#808080"); // Gray font for subdued effect + rowRange.setBackground("#F0F0F0"); // Light gray background + } + } + } +} + + +function importJiraTickets() { + // Get the active spreadsheet + const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); + + // Find the sheet named "Sprint" or create it if it doesn't exist + let sprintSheet = spreadsheet.getSheetByName("Sprint"); + if (!sprintSheet) { + sprintSheet = spreadsheet.insertSheet("Sprint"); + } + + // Clear the contents of the "Sprint" sheet + sprintSheet.clear(); + + // Add new content (placeholder for Jira ticket data) + const jiraData = createJiraData(); + + sprintSheet.getRange(1, 1, jiraData.length, jiraData[0].length).setValues(jiraData); + + // Format the Sprint sheet + formatSprintSheet(sprintSheet, jiraData[0].length); +} + diff --git a/projects/gapps-jira-backlog-import/src/JiraAPI.js b/projects/gapps-jira-backlog-import/src/JiraAPI.js new file mode 100644 index 0000000..a3a30ca --- /dev/null +++ b/projects/gapps-jira-backlog-import/src/JiraAPI.js @@ -0,0 +1,117 @@ +class JiraAPI { + constructor(baseUrl, username, apiToken) { + this.baseUrl = baseUrl; + this.auth = Utilities.base64Encode(`${username}:${apiToken}`); + } + + // Method to fetch the current sprint for a given project + fetchCurrentSprint(boardId) { + const endpoint = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active`; + const options = { + method: 'GET', + headers: { + 'Authorization': `Basic ${this.auth}`, + 'Content-Type': 'application/json' + }, + muteHttpExceptions: true + }; + + const response = UrlFetchApp.fetch(endpoint, options); + + if (response.getResponseCode() !== 200) { + throw new Error(`Failed to fetch current sprint. HTTP Status Code: ${response.getResponseCode()}`); + } + + const data = JSON.parse(response.getContentText()); + const sprints = data.values || []; + + // Return the first active sprint if available + if (sprints.length === 0) { + throw new Error("No active sprint found for the specified board."); + } + + return sprints[0]; // Assuming there's only one active sprint + } + + // Method to fetch sprint tickets + fetchSprintTickets(sprintId) { + const endpoint = `${this.baseUrl}/rest/agile/1.0/sprint/${sprintId}/issue`; + const options = { + method: 'GET', + headers: { + 'Authorization': `Basic ${this.auth}`, + 'Content-Type': 'application/json' + }, + muteHttpExceptions: true + }; + + const response = UrlFetchApp.fetch(endpoint, options); + + if (response.getResponseCode() !== 200) { + throw new Error(`Failed to fetch sprint tickets. HTTP Status Code: ${response.getResponseCode()}`); + } + + const data = JSON.parse(response.getContentText()); + const issues = data.issues || []; + return issues; + } + + // New method: Fetch issues by JQL query + fetchIssuesByJQL(jqlQuery) { + const endpoint = `${this.baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jqlQuery)}`; + const options = { + method: 'GET', + headers: { + 'Authorization': `Basic ${this.auth}`, + 'Content-Type': 'application/json' + }, + muteHttpExceptions: true + }; + + const response = UrlFetchApp.fetch(endpoint, options); + + if (response.getResponseCode() !== 200) { + throw new Error(`Failed to fetch issues by JQL. HTTP Status Code: ${response.getResponseCode()}`); + } + + const data = JSON.parse(response.getContentText()); + return data.issues || []; // Return the list of issues + } +} + +function jira_api_test() { + // Access script properties + const scriptProperties = PropertiesService.getScriptProperties(); + + // Retrieve the necessary values + const baseUrl = scriptProperties.getProperty('JIRA_BASE_URL'); + const username = scriptProperties.getProperty('JIRA_USERNAME'); + const apiToken = scriptProperties.getProperty('JIRA_API_TOKEN'); + + // Ensure all properties are set + if (!baseUrl || !username || !apiToken) { + Logger.log("Missing one or more Jira API credentials in script properties."); + return; + } + + // Create an instance of the JiraAPI class + const jira = new JiraAPI(baseUrl, username, apiToken); + + // Test the instance (e.g., by fetching the current sprint for a test board ID) + try { + const boardId = 317; // Replace with an appropriate test board ID + const currentSprint = jira.fetchCurrentSprint(boardId); + + Logger.log(`Current Sprint: ${currentSprint.name} (ID: ${currentSprint.id})`); + + const sprintBacklog = jira.fetchSprintTickets(currentSprint.id); + Logger.log(`Tickets in current sprint: ${sprintBacklog.length}`); + + const issuesByRankQuery = `sprint = ${currentSprint.id} ORDER BY Rank ASC`; + const issuesByRank = jira.fetchIssuesByJQL(issuesByRankQuery); + Logger.log(`Tickets in backlog: ${issuesByRank.map(issue => issue.key)}`); + } catch (error) { + Logger.log(`Error during Jira API test: ${error.message}`); + } +} + diff --git a/projects/gapps-jira-backlog-import/src/appsscript.json b/projects/gapps-jira-backlog-import/src/appsscript.json new file mode 100644 index 0000000..50abad2 --- /dev/null +++ b/projects/gapps-jira-backlog-import/src/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/Chicago", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file