diff --git a/assets/icon16.png b/assets/icon16.png new file mode 100644 index 0000000..0ea6df0 Binary files /dev/null and b/assets/icon16.png differ diff --git a/assets/icon48.png b/assets/icon48.png new file mode 100644 index 0000000..16fff19 Binary files /dev/null and b/assets/icon48.png differ diff --git a/css/options.css b/css/options.css index 73e9e05..ce4717f 100644 --- a/css/options.css +++ b/css/options.css @@ -29,7 +29,7 @@ .form-textarea { font-size: 1rem; width: 100%; - height: 10rem; + height: 7rem; } .btn { diff --git a/css/popup.css b/css/popup.css new file mode 100644 index 0000000..2d055c3 --- /dev/null +++ b/css/popup.css @@ -0,0 +1,59 @@ +.popup-wrapper { + margin: 0; + overflow: hidden; + min-width: 150px; +} + +.popup { + margin: 0; + overflow: hidden; + min-width: 100px; +} + +.btn { + font-family: Helvetica, Arial, sans-serif; + font-size: 0.85rem; + padding: 0.5rem 1rem; + width: 100%; + height: 50%; + border: 0.07rem solid black; +} + +.btn:hover { + background-color: lightskyblue; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.work-log-modal { + width: 400px; +} +.input-title { + font-size: 1rem; +} + +.input-text { + font-size: 1rem; + margin-top: 0.5rem; + height: 2rem; + width: 100%; +} + +.track-button { + font-family: Helvetica, Arial, sans-serif; + font-size: 0.85rem; + padding: 0.5rem 1rem; + width: 100%; + border: 0.07rem solid black; + width: 100%; +} + +.track-button:hover { + background-color: lightskyblue; +} diff --git a/index.js b/index.js index d158b69..979aff3 100644 --- a/index.js +++ b/index.js @@ -1,90 +1,138 @@ -import {MetadataReader} from './lib/MetadataReader'; -import {PageController} from './lib/PageController'; -import {SimpleTemplateDriver} from './lib/SimpleTemplateDriver'; -import {JiraApiClient} from './lib/JiraApiClient'; -import {ChromePluginConfig} from './lib/ChromePluginConfig'; +import { MetadataReader } from "./lib/MetadataReader"; +import { PageController } from "./lib/PageController"; +import { SimpleTemplateDriver } from "./lib/SimpleTemplateDriver"; +import { JiraApiClient } from "./lib/JiraApiClient"; +import { ChromePluginConfig } from "./lib/ChromePluginConfig"; +import updateJiraBoard from "./updateJiraBoard"; +import workLog from "./workLog"; +import "./css/normalize.css"; +import "./css/popup.css"; -chrome.browserAction.onClicked.addListener(() => { +function generateTemplate() { chrome.tabs.getSelected(null, function (tab) { const controller = new PageController(chrome.tabs, tab.id); const reader = new MetadataReader(controller); const templateDriver = new SimpleTemplateDriver(); const pluginConfig = new ChromePluginConfig(chrome.storage); - pluginConfig.load().then(() => reader.collect({ - reviewers: { - strategy: 'dom-query', - selector: '.js-issue-sidebar-form .css-truncate', - mapper: function (element) { - return Array.from(element.children) - .filter(reviewerParagraph => Boolean(reviewerParagraph.querySelector('.octicon-check'))) - .map(reviewerParagraph => reviewerParagraph.innerText.trim()); - } - }, - jiraTicket: { - strategy: 'dom-query', - selector: 'a[href*="atlassian.net/"]', - mapper: e => e.innerHTML.trim(), - }, - prNumber: { - strategy: 'js-eval', - code: document => document.location.pathname.split("/").pop(), - }, - mergeTitle: { - strategy: 'dom-query', - selector: '#merge_title_field', - mapper: e => e.value, - }, - hasToUpdateJiraTicket: { - strategy: 'js-eval', - code: document => - document.querySelector('a[href*="atlassian.net/"]') && confirm('Do you want to update jira ticket ?'), - } - })).then(data => { - const userAliases = pluginConfig.get('userAliases'); + pluginConfig + .load() + .then(() => + reader.collect({ + reviewers: { + strategy: "dom-query", + selector: ".js-issue-sidebar-form .css-truncate", + mapper: function (element) { + return Array.from(element.children) + .filter((reviewerParagraph) => + Boolean(reviewerParagraph.querySelector(".octicon-check")) + ) + .map( + (reviewerParagraph) => + console.log({ reviewerParagraph }) || + reviewerParagraph.innerText.trim().split("\n")[0] + ); + }, + }, + jiraTicket: { + strategy: "dom-query", + selector: `a[href*="${pluginConfig.get("jiraBase")}/"]`, + mapper: (e) => e.innerHTML.trim(), + }, + prNumber: { + strategy: "js-eval", + code: (document) => document.location.pathname.split("/").pop(), + }, + mergeTitle: { + strategy: "dom-query", + selector: "#merge_title_field", + mapper: (e) => e.value, + }, + hasToUpdateJiraTicket: { + strategy: "js-eval", + code: (document) => + document.querySelector( + `a[href*=${new JiraApiClient(pluginConfig.get("jiraBase"))}]` + ) && confirm("Do you want to update jira ticket ?"), + }, + }) + ) + .then((data) => { + const userAliases = pluginConfig.get("userAliases"); - data.reviewers = data.reviewers.map(reviewer => { - return userAliases[reviewer] || reviewer.toLowerCase(); - }); + data.reviewers = data.reviewers.map((reviewer) => { + console.log("---- reviewer", reviewer); - const {jiraTicket, hasToUpdateJiraTicket} = data; - const jiraApi = new JiraApiClient(pluginConfig.get('jiraBase')); - const boardColumnName = pluginConfig.get('boardColumn'); - const commitMessage = templateDriver.renderToString(pluginConfig.get('template'), data); - const mergeTitle = templateDriver.renderToString('{{ title | clear }}', { - title: data.mergeTitle, - }); + return userAliases[reviewer] || reviewer.toLowerCase(); + }); + + const { jiraTicket, hasToUpdateJiraTicket } = data; + const jiraApi = new JiraApiClient(pluginConfig.get("jiraBase")); + const boardColumnName = pluginConfig.get("boardColumn"); + const commitMessage = templateDriver.renderToString( + pluginConfig.get("template"), + data + ); + const mergeTitle = templateDriver.renderToString( + "{{ title | clear }}", + { + title: data.mergeTitle, + } + ); - const updateJiraTicket = jiraTicket && hasToUpdateJiraTicket - ? jiraApi - .getTransitions(jiraTicket) - .catch(e => { - throw new Error(`${e.message}. Please ensure "${pluginConfig.get('jiraBase')}" is accessible.`); - }) - .then(response => { - const transitions = response.data.transitions; - const toDevCompleteTransition = transitions.find(t => new RegExp(boardColumnName, 'i').test(t.name)); + const updateJiraTicket = + jiraTicket && hasToUpdateJiraTicket + ? jiraApi + .getTransitions(jiraTicket) + .catch((e) => { + throw new Error( + `${e.message}. Please ensure "${pluginConfig.get( + "jiraBase" + )}" is accessible.` + ); + }) + .then((response) => { + const transitions = response.data.transitions; + const toDevCompleteTransition = transitions.find((t) => + new RegExp(boardColumnName, "i").test(t.name) + ); - if (!toDevCompleteTransition) { - const tNames = transitions.map(t => t.name).join(', '); + if (!toDevCompleteTransition) { + const tNames = transitions.map((t) => t.name).join(", "); - throw new Error(`Couldn't find column matching "${boardColumnName}". Available columns: ${tNames}.`); - } + throw new Error( + `Couldn't find column matching "${boardColumnName}". Available columns: ${tNames}.` + ); + } - const {id, name} = toDevCompleteTransition; + const { id, name } = toDevCompleteTransition; - return jiraApi - .postTransition(jiraTicket, {transition: {id}}) - .then(() => controller.alert(`Ticket ${jiraTicket} successfully moved to ${name}`)); - }) - .catch(e => controller.alert(`Error moving jira ticket: ${e.message}`)) - : Promise.resolve(); + return jiraApi + .postTransition(jiraTicket, { transition: { id } }) + .then(() => + controller.alert( + `Ticket ${jiraTicket} successfully moved to ${name}` + ) + ); + }) + .catch((e) => + controller.alert(`Error moving jira ticket: ${e.message}`) + ) + : Promise.resolve(); - return Promise.all([ - controller.updateInputValue('#merge_message_field', commitMessage), - controller.updateInputValue('#merge_title_field', mergeTitle), - updateJiraTicket, - ]); - }); + return Promise.all([ + controller.updateInputValue("#merge_message_field", commitMessage), + controller.updateInputValue("#merge_title_field", mergeTitle), + updateJiraTicket, + ]); + }); }); -}); +} + +document + .getElementById("geenerate_template") + .addEventListener("click", generateTemplate); +document + .getElementById("update_jira_board") + .addEventListener("click", updateJiraBoard); +document.getElementById("work_log").addEventListener("click", workLog); diff --git a/lib/ChromePluginConfig.js b/lib/ChromePluginConfig.js index 7e031e2..a96448d 100644 --- a/lib/ChromePluginConfig.js +++ b/lib/ChromePluginConfig.js @@ -5,6 +5,11 @@ export const defaultOptions = { userAliases: { userName: 'first.lastName', }, + labelTransitions: { + hold: 'Open', + wip: 'In Progress', + default: 'Review', + } }; export class ChromePluginConfig { diff --git a/lib/JiraApiClient.js b/lib/JiraApiClient.js index 62ea752..2b705f0 100644 --- a/lib/JiraApiClient.js +++ b/lib/JiraApiClient.js @@ -1,7 +1,8 @@ -import axios from 'axios'; +import axios from "axios"; const ENDPOINTS = { - TRANSACTIONS: '/rest/api/2/issue/:ticketId/transitions', + TRANSACTIONS: "/rest/api/2/issue/:ticketId/transitions", + WORKLOG: "/rest/api/2/issue/:ticketId/worklog" }; export class JiraApiClient { @@ -10,7 +11,7 @@ export class JiraApiClient { */ constructor(apiBase) { this.client = axios.create({ - baseURL: apiBase, + baseURL: apiBase }); } @@ -19,7 +20,7 @@ export class JiraApiClient { * @returns {Promise} */ getTransitions(ticketId) { - const url = this.buildUrl(ENDPOINTS.TRANSACTIONS, {ticketId}); + const url = this.buildUrl(ENDPOINTS.TRANSACTIONS, { ticketId }); return this.client.get(url); } @@ -30,7 +31,18 @@ export class JiraApiClient { * @returns {Promise} */ postTransition(ticketId, params) { - const url = this.buildUrl(ENDPOINTS.TRANSACTIONS, {ticketId}); + const url = this.buildUrl(ENDPOINTS.TRANSACTIONS, { ticketId }); + + return this.client.post(url, params); + } + + /** + * @param {String} ticketId + * @param {Object} params + * @returns {Promise} + */ + postAddTime(ticketId, params) { + const url = this.buildUrl(ENDPOINTS.WORKLOG, { ticketId }); return this.client.post(url, params); } @@ -43,7 +55,9 @@ export class JiraApiClient { buildUrl(resourceUrlTpl, params) { return resourceUrlTpl.replace(/:([^\/]+)/g, (match, paramName) => { if (!params.hasOwnProperty(paramName)) { - throw new Error(`Missing "${paramName}" parameter. Cannot build "${resourceUrlTpl}" resource"`); + throw new Error( + `Missing "${paramName}" parameter. Cannot build "${resourceUrlTpl}" resource"` + ); } const paramValue = params[paramName]; diff --git a/manifest.json b/manifest.json index 84b14ba..99a684d 100644 --- a/manifest.json +++ b/manifest.json @@ -3,17 +3,19 @@ "name": "Merge assistant", "description": "This extension creates merge commit for you", "version": "1.0", - "background": { - "page": "index.html" + "browser_action": { + "default_title": "Git Merger", + "default_icon": "icon128.png", + "default_popup": "popup.html" }, - "browser_action": {}, "options_page": "options.html", "permissions": [ "tabs", "activeTab", "background", "storage", - "https://*.atlassian.net/*" + "https://*.atlassian.net/*", + "https://jira.tenkasu.net/*" ], "icons": { "16": "icon128.png", diff --git a/options.js b/options.js index 1b93c9b..a1ee426 100644 --- a/options.js +++ b/options.js @@ -9,12 +9,14 @@ function saveOptions() { const boardColumn = document.getElementById('boardColumn').value; const template = document.getElementById('template').value; const userAliases = JSON.parse(document.getElementById('userAliases').value); + const labelTransitions = JSON.parse(document.getElementById('labelTransitions').value); config.set({ jiraBase, boardColumn, template, userAliases, + labelTransitions, }).then(() => { const status = document.getElementById('status'); @@ -32,6 +34,7 @@ function loadOptions() { document.getElementById('boardColumn').value = options.boardColumn; document.getElementById('template').value = options.template; document.getElementById('userAliases').value = JSON.stringify(options.userAliases, null, ' '); + document.getElementById('labelTransitions').value = JSON.stringify(options.labelTransitions, null, ' '); }); } diff --git a/updateJiraBoard.js b/updateJiraBoard.js new file mode 100644 index 0000000..a8dc0e9 --- /dev/null +++ b/updateJiraBoard.js @@ -0,0 +1,89 @@ +import { ChromePluginConfig } from "./lib/ChromePluginConfig"; +import { PageController } from "./lib/PageController"; +import { MetadataReader } from "./lib/MetadataReader"; +import { JiraApiClient } from "./lib/JiraApiClient"; + +function updateJiraBoard() { + chrome.tabs.getSelected(null, function (tab) { + const controller = new PageController(chrome.tabs, tab.id); + const reader = new MetadataReader(controller); + const pluginConfig = new ChromePluginConfig(chrome.storage); + + pluginConfig + .load() + .then(() => + reader.collect({ + labels: { + strategy: "dom-query", + selector: ".labels.css-truncate", + mapper: (e) => Array.from(e.children).map((label) => label.title), + }, + jiraTicket: { + strategy: "dom-query", + selector: `a[href*="${pluginConfig.get("jiraBase")}/"]`, + mapper: (e) => e.innerHTML.trim(), + }, + }) + ) + .then((data) => jiraMoveRequest(data, pluginConfig, controller)); + }); +} + +function jiraMoveRequest( + { labels, jiraTicket, hasToUpdateJiraTicket }, + pluginConfig, + controller +) { + const boardColumnName = getJiraColumnUsing(labels, pluginConfig); + const jiraApi = new JiraApiClient(pluginConfig.get("jiraBase")); + + return jiraTicket + ? jiraApi + .getTransitions(jiraTicket) + .catch((e) => { + throw new Error( + `${e.message}. Please ensure "${pluginConfig.get( + "jiraBase" + )}" is accessible.` + ); + }) + .then((response) => { + const transitions = response.data.transitions; + const targetColumn = transitions.find((t) => + new RegExp(boardColumnName, "i").test(t.name) + ); + + if (!targetColumn) { + const tNames = transitions.map((t) => t.name).join(", "); + + throw new Error( + `Couldn't find column matching "${boardColumnName}". Available columns: ${tNames}.` + ); + } + const { id, name } = targetColumn; + + return jiraApi + .postTransition(jiraTicket, { transition: { id } }) + .then(() => + controller.alert( + `Ticket ${jiraTicket} successfully moved to ${name}` + ) + ); + }) + .catch((e) => + controller.alert(`Error moving jira ticket: ${e.message}`) + ) + : Promise.resolve(); +} + +function getJiraColumnUsing(labels, pluginConfig) { + const transitions = pluginConfig.get("labelTransitions"); + const intersetedKeys = Object.keys(transitions).filter( + (key) => labels.indexOf(key) !== -1 + ); + const [firstIntersectedLabel] = intersetedKeys; + + return transitions[firstIntersectedLabel || "default"]; +} + +export default updateJiraBoard; diff --git a/view/index.html b/view/index.html index 1d0a047..7784319 100644 --- a/view/index.html +++ b/view/index.html @@ -3,7 +3,5 @@