diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a938ce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "useTabs": false +} diff --git a/app.js b/app.js index 7d1d36a..22be8f7 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,14 @@ var nunjucks = require("nunjucks"); const passport = require('passport'); const AtlassianOAuth2Strategy = require('passport-atlassian-oauth2'); const session = require('express-session'); + +var fs = require('fs'); + +if (!fs.existsSync('.env')) { + console.log("No .env file found. Please create one using the .env.example file as a template."); + process.exit(1); +} + const dotenv = require('dotenv').config(); const https = require('https'); diff --git a/controllers/jiraAPIController.js b/controllers/jiraAPIController.js index e4ffc28..1f42ae8 100644 --- a/controllers/jiraAPIController.js +++ b/controllers/jiraAPIController.js @@ -124,6 +124,23 @@ exports.searchIssues = function(req, jql) { return withRetry(searchIssuesInternal, req, jql); } +exports.suggestIssues = function(req, query) { + return withRetry(suggestIssuesInternal, req, query); +} + +function suggestIssuesInternal(req, query) { + const url = getCallURL(req); + if (process.env.JIRA_PROJECT_JQL) { + query += '¤tJQL=' + process.env.JIRA_PROJECT_JQL + } + return fetch(url + '/rest/api/3/issue/picker?query=' + query , { + method: 'GET', + headers: getDefaultHeaders(req), + agent:httpsAgent + }).then(res => res.json()); + +} + function getIssueInternal(req, issueId) { const url = getCallURL(req); return fetch(url + '/rest/api/3/issue/' + issueId, { diff --git a/controllers/jiraController.js b/controllers/jiraController.js index c5404b1..378aed3 100644 --- a/controllers/jiraController.js +++ b/controllers/jiraController.js @@ -9,7 +9,9 @@ exports.getUsersWorkLogsAsEvent = function(req, start, end) { const formattedStart = filterStartTime.toLocaleDateString('en-CA'); const formattedEnd = filterEndTime.toLocaleDateString('en-CA'); - return jiraAPIController.searchIssues(req, 'worklogAuthor = currentUser() AND worklogDate >= ' + formattedStart + ' AND worklogDate <= '+ formattedEnd).then(result => { + let issuesPromise = jiraAPIController.searchIssues(req, 'worklogAuthor = currentUser() AND worklogDate >= ' + formattedStart + ' AND worklogDate <= '+ formattedEnd); + + return issuesPromise.then(result => { // create an array of issue IDs and keys from result.issues const issues = result.issues.map(issue => { return {issueId: issue.id, issueKey: issue.key, summary: issue.fields.summary} }); const userWorkLogs = []; @@ -87,6 +89,65 @@ exports.deleteWorkLog = function(req, issueId, worklogId) { return jiraAPIController.deleteWorkLog(req, issueId, worklogId); } +exports.suggestIssues = function(req, start, end, query) { + var startDate = new Date(start).toISOString().split('T')[0]; + var endDate = new Date(end).toISOString().split('T')[0]; + var searchInJira = req.query.searchInJira; + var query = req.query.query; + + var promises = []; + + var emptyJQL = 'worklogAuthor = currentUser() AND worklogDate >= ' + startDate + ' AND worklogDate <= '+endDate+' OR ((assignee = currentUser() OR reporter = currentUser()) AND ((statusCategory != '+ process.env.JIRA_DONE_STATUS +') OR (statusCategory = '+ process.env.JIRA_DONE_STATUS +' AND status CHANGED DURING (' + startDate + ', '+endDate+'))))'; + + var keyJQL = 'key = ' + query + + if (searchInJira != 'true') { + promises.push(jiraAPIController.searchIssues(req, emptyJQL)); + } else { + promises.push(jiraAPIController.searchIssues(req, keyJQL)); + promises.push(jiraAPIController.suggestIssues(req, query)); + } + + + + return Promise.all(promises).then(results => { + // results[0] = base JQL search + // if search in jira + // results[0] = Key search + // results[1] = Suggestion search + + var issues = [] + if (searchInJira != 'true' && results[0].issues) { + issues = results[0].issues.map(mapIssuesFunction); + } + + if (searchInJira == 'true') { + if (results[1] && results[1].sections && results[1].sections.length > 0) { + for (var i = 0; i < results[1].sections.length; i++) { + if (results[1].sections[i].id == "cs") { + //console.log(JSON.stringify(results[0].sections[i])); + // add issues from the suggestion results to the issues array + var mappedIssues = results[1].sections[i].issues.map(mapIssuesFunction); + issues = issues.concat(mappedIssues); + } + } + } + if (results[0] && results[0].issues) { + issues = issues.concat(results[0].issues.map(mapIssuesFunction)); + } + } + return issues; + }); +} + +function mapIssuesFunction(issue) { + return { + id: issue.id, + key: issue.key, + summary: issue.fields ? issue.fields.summary : issue.summary + } +} + function formatDateToJira(toFormat) { const dayJsDate = dayjs(toFormat); diff --git a/example.env b/example.env index 223e240..cb048e6 100644 --- a/example.env +++ b/example.env @@ -29,6 +29,8 @@ JIRA_OAUTH_CALLBACK_URL=http://locahost:3000/auth/callback JIRA_DONE_STATUS="Done" # Maximum number of issues to return at once. JIRA_MAX_SEARCH_RESULTS=200 +# JQL used to filter suggested issues. uswful for restricting to a project +JIRA_PROJECT_JQL="project = PLY" # Express app settings # The port to run the app on diff --git a/public/images/icons8-dots-loading.gif b/public/images/icons8-dots-loading.gif new file mode 100644 index 0000000..4f6c443 Binary files /dev/null and b/public/images/icons8-dots-loading.gif differ diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 8dd7221..2e9fe62 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -59,6 +59,19 @@ a { margin-bottom: 15px; } +.modal-content .search-options { + display: flex; + width: fit-content; + align-items: center; +} +.modal-content .search-options input { + flex-grow: 0; +} +.modal-content .search-options label { + word-break: keep-all; + white-space: nowrap; +} + .modal .destructive { border: 2px solid darkred; background: red; @@ -139,12 +152,16 @@ input#include-weekends { } -.about-section { +#about-section { position: absolute; bottom: 5px; opacity: 35%; } +#loading-container { + width: 50px; + overflow: clip; +} /********** Choices.js **********/ diff --git a/routes/index.js b/routes/index.js index dbca415..5f96085 100644 --- a/routes/index.js +++ b/routes/index.js @@ -30,19 +30,13 @@ router.get('/events', function(req, res, next) { router.get('/issues/user', function(req, res, next) { try { - var startDate = new Date(req.query.start).toISOString().split('T')[0]; - - var endDate = new Date(req.query.end).toISOString().split('T')[0]; - - jiraAPIController.searchIssues(req, 'worklogAuthor = currentUser() AND worklogDate >= ' + startDate + ' AND worklogDate <= '+endDate+' OR ((assignee = currentUser() OR reporter = currentUser()) AND ((statusCategory != '+ process.env.JIRA_DONE_STATUS +') OR (statusCategory = '+ process.env.JIRA_DONE_STATUS +' AND status CHANGED DURING (' + startDate + ', '+endDate+'))))').then(result => { - if (!result.issues) { - console.log(result); - } - res.json(result.issues); + jiraController.suggestIssues(req, req.query.start, req.query.end, req.query.query).then(result => { + res.json(result); }); } catch (error) { console.log(error); } + }); router.get('/issues/:issueId', function(req, res, next) { diff --git a/views/aboutModal.njk b/views/aboutModal.njk new file mode 100644 index 0000000..42a3b46 --- /dev/null +++ b/views/aboutModal.njk @@ -0,0 +1,39 @@ +{% macro modal() %} +
+ + +{% endmacro %} \ No newline at end of file diff --git a/views/createModal.njk b/views/createModal.njk new file mode 100644 index 0000000..b1b471b --- /dev/null +++ b/views/createModal.njk @@ -0,0 +1,225 @@ +{% macro modal(endpoint) %} + + +