-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
266 lines (233 loc) · 10.1 KB
/
background.js
File metadata and controls
266 lines (233 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// In MV3, we need to use chrome.storage instead of localStorage
function getCustomMailtoUrl() {
return new Promise((resolve) => {
chrome.storage.local.get('customMailtoUrl', (result) => {
resolve(result.customMailtoUrl || "");
});
});
}
async function executeMailto(tab_id, to, subject, body, selection) {
const customUrl = await getCustomMailtoUrl();
const default_handler = customUrl.length === 0;
let action_url = "mailto:" + to;
action_url += "?";
if (subject.length > 0)
action_url += "subject=" + encodeURIComponent(subject) + "&";
if (body.length > 0) {
action_url += "body=" + encodeURIComponent(body);
// Append the current selection to the end of the text message.
if (selection.length > 0) {
action_url += encodeURIComponent("\n\n") +
encodeURIComponent(selection);
}
}
if (!default_handler) {
// Custom URL's (such as opening mailto in Gmail tab) should have a
// separate tab to avoid clobbering the page you are on.
action_url = customUrl.replace("%s", encodeURIComponent(action_url));
console.log('Custom url: ' + action_url);
chrome.tabs.create({ url: action_url });
} else {
// Plain vanilla mailto links open up in the same tab to prevent
// blank tabs being left behind.
console.log('Action url: ' + action_url);
chrome.tabs.update(tab_id, { url: action_url });
}
}
// Copies text to the clipboard inside the given tab.
// Tries the modern Clipboard API first; falls back to execCommand when the
// tab's document does not have focus (e.g. while the popup is open).
function copyTextInTab(tabId, text) {
return chrome.scripting.executeScript({
target: { tabId },
func: (t) => {
return navigator.clipboard.writeText(t).catch(() => {
const el = document.createElement('textarea');
el.value = t;
document.body.appendChild(el);
el.select();
// execCommand is deprecated but used here as a focus-independent fallback.
document.execCommand('copy'); // eslint-disable-line no-document-execcommand
document.body.removeChild(el);
});
},
args: [text]
});
}
// Stores the template body chosen in the popup until the content script responds.
let pendingTemplateBody = null;
// Stores a pending copyId callback until the content script responds.
let pendingCopyIdCallback = null;
chrome.runtime.onConnect.addListener(function (port) {
var tab = port.sender.tab;
const templateBody = pendingTemplateBody;
pendingTemplateBody = null;
const copyIdCallback = pendingCopyIdCallback;
pendingCopyIdCallback = null;
// This will get called by the content script we execute in
// the tab as a result of the user pressing the browser action.
port.onMessage.addListener(function (info) {
// Handle copyId action: just copy the ID to clipboard and respond.
if (copyIdCallback) {
copyTextInTab(tab.id, info.itsm + ":" + info.op)
.then(() => copyIdCallback({ success: true }))
.catch((err) => copyIdCallback({ success: false, error: err.message }));
return;
}
var max_length = 1024;
if (info.selection.length > max_length)
info.selection = info.selection.substring(0, max_length);
// Prepend the template body (if any) before the page URL.
let body = tab.url;
if (templateBody !== null && templateBody.length > 0) {
body = templateBody + "\n\n" + tab.url;
}
executeMailto(tab.id, info.mailto, info.title, body, info.selection);
copyTextInTab(tab.id, info.itsm + ":" + info.op);
});
});
// Extracts the JIRA issue key (e.g. "PROJ-123") from a JIRA browse URL.
function extractIssueKey(url) {
const match = url.match(/\/browse\/([A-Z]+-\d+)/i);
return match ? match[1] : null;
}
// Fetches available transitions for an issue and returns the ID of the one
// whose target status matches targetStatusName.
async function getTransitionId(baseUrl, issueKey, targetStatusName) {
const res = await fetch(`${baseUrl}/issue/${issueKey}/transitions`, {
credentials: 'include'
});
if (!res.ok) throw new Error(`Could not fetch transitions (HTTP ${res.status})`);
const data = await res.json();
const t = data.transitions.find(t => t.to.name === targetStatusName);
if (!t) throw new Error(`Transition to "${targetStatusName}" not available`);
return t.id;
}
// Posts a transition to move the issue to a new status.
// Optional fields object is included in the POST body when provided
// (required for mandatory transition-screen fields such as INC Status Reason).
async function postTransition(baseUrl, issueKey, transitionId, fields) {
const body = { transition: { id: transitionId } };
if (fields) body.fields = fields;
const res = await fetch(`${baseUrl}/issue/${issueKey}/transitions`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`Transition failed (HTTP ${res.status})`);
}
// Handles the setCompleted action: transitions the current issue to Resolved.
// Pending transitions directly; Assigned goes via In Progress first.
// "INC Status Reason" and "INC Resolution" are passed inside the Resolved
// transition POST because they are mandatory fields on the transition screen.
// Field IDs are looked up dynamically by name from GET /rest/api/2/field.
async function handleSetCompleted(incResolution, sendResponse) {
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
const issueKey = extractIssueKey(tab.url);
if (!issueKey) {
sendResponse({ success: false, error: 'Not a JIRA issue page' });
return;
}
const base = 'https://issue.swisscom.ch/rest/api/2';
const issueRes = await fetch(`${base}/issue/${issueKey}?fields=status`, {
credentials: 'include'
});
if (!issueRes.ok) throw new Error(`Could not fetch issue (HTTP ${issueRes.status})`);
const issueData = await issueRes.json();
const status = issueData.fields.status.name;
if (status !== 'Assigned' && status !== 'In Progress' && status !== 'Pending') {
const msg = status === 'Resolved' ? 'Ticket is already Resolved'
: `Cannot resolve from status "${status}"`;
sendResponse({ success: false, error: msg });
return;
}
// Look up custom field IDs before transitioning so they can be
// included as mandatory fields in the Resolved transition POST.
const fieldsRes = await fetch(`${base}/field`, { credentials: 'include' });
if (!fieldsRes.ok) throw new Error(`Could not fetch fields (HTTP ${fieldsRes.status})`);
const allFields = await fieldsRes.json();
const statusReasonField = allFields.find(f => f.name === 'INC Status Reason');
const resolutionField = allFields.find(f => f.name === 'INC Resolution');
if (!statusReasonField) throw new Error('Field "INC Status Reason" not found in JIRA');
if (!resolutionField) throw new Error('Field "INC Resolution" not found in JIRA');
const completionFields = {};
completionFields[statusReasonField.id] = { value: 'No Further Action Required' };
if (incResolution && incResolution.length > 0) {
completionFields[resolutionField.id] = incResolution;
}
if (status === 'Assigned') {
const tid1 = await getTransitionId(base, issueKey, 'In Progress');
await postTransition(base, issueKey, tid1);
const tid2 = await getTransitionId(base, issueKey, 'Resolved');
await postTransition(base, issueKey, tid2, completionFields);
} else {
// In Progress and Pending both transition directly to Resolved
const tid = await getTransitionId(base, issueKey, 'Resolved');
await postTransition(base, issueKey, tid, completionFields);
}
sendResponse({ success: true });
chrome.tabs.reload(tab.id);
} catch (err) {
sendResponse({ success: false, error: err.message });
}
}
// Handles the sendMail message sent from popup.js when the user picks a template.
chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
if (message.action === 'setCompleted') {
handleSetCompleted(message.incResolution || '', sendResponse);
return true; // keep channel open for async response
}
if (message.action === 'copyId') {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const tab = tabs[0];
if (!tab || tab.url.indexOf("https://issue.swisscom.ch") !== 0) {
sendResponse({ success: false, error: 'Not a JIRA issue page' });
return;
}
pendingCopyIdCallback = sendResponse;
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content_script.js"]
});
});
return true; // keep channel open for async response
}
if (message.action === 'sendMail') {
pendingTemplateBody = message.templateBody;
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const tab = tabs[0];
if (tab.url.indexOf("https://issue.swisscom.ch") !== 0) {
// Not a JIRA page — open mail with just the URL, no template body.
executeMailto(tab.id, "", "", tab.url, "");
pendingTemplateBody = null;
} else {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content_script.js"]
});
}
});
}
});
// Called when the user clicks on the browser action icon.
// NOTE: This handler is only active when no default_popup is set in manifest.json.
// With a popup configured it will not fire, but is kept here for reference.
chrome.action.onClicked.addListener(function (tab) {
// We can only inject scripts to find the title on pages loaded with http
// and https so for all other pages, we don't ask for the title.
if (tab.url.indexOf("https://issue.swisscom.ch") != 0) {
executeMailto(tab.id, "", "", tab.url, "");
} else {
// Updated to use the scripting API
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content_script.js"]
});
}
});