import ScreenshotEditor from './screenshot-editor.js';
class FeedbackExtension {
constructor() {
this.screenshot = null;
this.editorWindow = null;
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
this.initializeElements();
this.attachEventListeners();
this.loadSettings();
});
// Listen for messages from the editor window
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Popup received message:', message);
try {
if (message.type === 'screenshot-saved') {
this.screenshot = message.screenshot;
this.attachmentPreview.innerHTML = `
`;
// Close the editor window if it exists
if (this.editorWindow && !this.editorWindow.closed) {
this.editorWindow.close();
}
sendResponse({ success: true });
} else if (message.type === 'editor-ready') {
console.log('Editor is ready, sending screenshot...');
if (this.screenshot) {
chrome.runtime.sendMessage({
type: 'init-editor',
screenshot: this.screenshot
}, response => {
console.log('Screenshot sent to editor, response:', response);
});
}
sendResponse({ success: true });
}
} catch (error) {
console.error('Error handling message:', error);
sendResponse({ success: false, error: error.message });
}
return true; // Keep the message channel open for async response
});
}
initializeElements() {
// Form elements
this.feedbackForm = document.getElementById('feedback-form');
this.settingsPanel = document.getElementById('settings-panel');
this.screenshotBtn = document.getElementById('screenshot-btn');
this.recordBtn = document.getElementById('record-btn');
this.submitFeedbackBtn = document.getElementById('submit-feedback');
this.settingsBtn = document.getElementById('settings-btn');
this.saveSettingsBtn = document.getElementById('save-settings');
this.attachmentPreview = document.getElementById('attachment-preview');
// Settings inputs
this.jiraDomainInput = document.getElementById('jira-domain');
this.jiraEmailInput = document.getElementById('jira-email');
this.jiraTokenInput = document.getElementById('jira-token');
this.jiraProjectInput = document.getElementById('jira-project');
// Notification
this.notification = document.getElementById('notification');
}
attachEventListeners() {
if (this.screenshotBtn) {
this.screenshotBtn.addEventListener('click', () => this.captureScreenshot());
}
if (this.recordBtn) {
this.recordBtn.addEventListener('click', () => this.startScreenRecording());
}
if (this.submitFeedbackBtn) {
this.submitFeedbackBtn.addEventListener('click', () => this.handleSubmit());
}
if (this.settingsBtn) {
this.settingsBtn.addEventListener('click', () => this.showSettings());
}
if (this.saveSettingsBtn) {
this.saveSettingsBtn.addEventListener('click', () => this.saveSettings());
}
}
async loadSettings() {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken', 'jiraProject']);
// Fill in settings if they exist
if (settings.jiraDomain) this.jiraDomainInput.value = settings.jiraDomain;
if (settings.jiraEmail) this.jiraEmailInput.value = settings.jiraEmail;
if (settings.jiraToken) this.jiraTokenInput.value = settings.jiraToken;
if (settings.jiraProject) this.jiraProjectInput.value = settings.jiraProject;
// Show appropriate panel based on whether settings exist
if (settings.jiraDomain && settings.jiraEmail && settings.jiraToken && settings.jiraProject) {
this.showFeedbackForm();
} else {
this.showSettings();
}
}
async saveSettings() {
const domain = this.jiraDomainInput.value.trim();
const email = this.jiraEmailInput.value.trim();
const token = this.jiraTokenInput.value.trim();
const project = this.jiraProjectInput.value.trim();
if (!domain || !email || !token || !project) {
this.showNotification('Please fill in all fields', 'error');
return;
}
try {
// Validate credentials by making a test API call
const isValid = await this.validateJiraCredentials(domain, email, token);
if (!isValid) {
this.showNotification('Invalid Jira credentials', 'error');
return;
}
// Save settings
await chrome.storage.local.set({
jiraDomain: domain,
jiraEmail: email,
jiraToken: token,
jiraProject: project
});
this.showNotification('Settings saved successfully', 'success');
this.showFeedbackForm();
} catch (error) {
console.error('Error saving settings:', error);
this.showNotification('Error saving settings: ' + error.message, 'error');
}
}
async validateJiraCredentials(domain, email, token) {
try {
const response = await fetch(`https://${domain}/rest/api/2/myself`, {
method: 'GET',
headers: {
'Authorization': `Basic ${btoa(`${email}:${token}`)}`,
'Accept': 'application/json'
}
});
return response.ok;
} catch (error) {
console.error('Error validating Jira credentials:', error);
return false;
}
}
async handleSubmit() {
try {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken', 'jiraProject']);
if (!settings.jiraDomain || !settings.jiraEmail || !settings.jiraToken || !settings.jiraProject) {
this.showNotification('Please configure Jira settings first', 'error');
this.showSettings();
return;
}
const type = document.getElementById('feedback-type').value;
const title = document.getElementById('feedback-title').value;
const description = document.getElementById('feedback-description').value;
if (!title || !description) {
this.showNotification('Please fill in all required fields', 'error');
return;
}
// Map feedback type to standard Jira issue types
let issueType;
switch (type) {
case 'Bug':
issueType = 'Bug';
break;
case 'Feature':
issueType = 'Story';
break;
case 'Improvement':
issueType = 'Task';
break;
default:
issueType = 'Task';
}
// Get current tab URL for context
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const contextInfo = `
Reported from: ${tab.url}
Browser: ${navigator.userAgent}
Timestamp: ${new Date().toISOString()}
Description:
${description}
`;
const formData = {
fields: {
project: {
key: settings.jiraProject
},
summary: title,
description: contextInfo,
issuetype: {
name: issueType
},
labels: ["feedback-extension"]
}
};
console.log('Submitting to Jira with data:', JSON.stringify(formData, null, 2));
const response = await this.submitToJira(formData);
// If we have a screenshot, attach it to the created issue
if (this.screenshot && response.key) {
await this.attachScreenshotToIssue(response.key, this.screenshot);
}
if (response.key) {
this.showNotification(`Issue ${response.key} created successfully!`, 'success');
document.getElementById('feedback-title').value = '';
document.getElementById('feedback-description').value = '';
this.attachmentPreview.innerHTML = '';
this.screenshot = null;
}
} catch (error) {
console.error('Full error details:', error);
this.showNotification('Error submitting feedback: ' + error.message, 'error');
}
}
async submitToJira(formData) {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken']);
try {
// First, get available issue types for the project
const metaResponse = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue/createmeta?projectKeys=${formData.fields.project.key}&expand=projects.issuetypes.fields`, {
method: 'GET',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'Accept': 'application/json'
}
});
const metaData = await metaResponse.json();
console.log('Project metadata:', metaData);
if (!metaResponse.ok) {
throw new Error('Failed to get project metadata');
}
// Get available issue types
const project = metaData.projects[0];
if (!project) {
throw new Error('Project not found or no access');
}
const availableTypes = project.issuetypes.map(type => type.name);
console.log('Available issue types:', availableTypes);
// If the desired issue type isn't available, fall back to Task
if (!availableTypes.includes(formData.fields.issuetype.name)) {
console.log(`Issue type ${formData.fields.issuetype.name} not available, falling back to Task`);
formData.fields.issuetype.name = 'Task';
}
// Create the issue
const response = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const responseData = await response.json();
console.log('Jira API Response:', responseData);
if (!response.ok) {
if (responseData.errors) {
const errorMessage = Object.entries(responseData.errors)
.map(([field, error]) => `${field}: ${error}`)
.join(', ');
throw new Error(errorMessage);
} else if (responseData.errorMessages) {
throw new Error(responseData.errorMessages.join(', '));
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
return responseData;
} catch (error) {
console.error('Error submitting to Jira:', error);
console.error('Request data:', JSON.stringify(formData, null, 2));
throw error;
}
}
async attachScreenshotToIssue(issueKey, screenshotDataUrl) {
try {
const settings = await chrome.storage.local.get(['jiraDomain', 'jiraEmail', 'jiraToken']);
// Convert base64 data URL to blob
const base64Data = screenshotDataUrl.replace(/^data:image\/png;base64,/, '');
const byteCharacters = atob(base64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, { type: 'image/png' });
// Create form data with the screenshot
const formData = new FormData();
formData.append('file', blob, 'screenshot.png');
// Upload the attachment
const response = await fetch(`https://${settings.jiraDomain}/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${settings.jiraEmail}:${settings.jiraToken}`)}`,
'X-Atlassian-Token': 'no-check' // Required for file uploads
},
body: formData
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to attach screenshot: ${error}`);
}
console.log('Screenshot attached successfully');
} catch (error) {
console.error('Error attaching screenshot:', error);
throw new Error('Failed to attach screenshot: ' + error.message);
}
}
async captureScreenshot() {
try {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
console.log('Current tab:', tab);
// Capture the visible tab
console.log('Capturing screenshot...');
const screenshot = await chrome.tabs.captureVisibleTab(null, {
format: 'png'
});
console.log('Screenshot captured:', screenshot?.substring(0, 100) + '...');
// Verify screenshot data
if (!screenshot || !screenshot.startsWith('data:image')) {
throw new Error('Invalid screenshot data');
}
// Store the screenshot data
this.screenshot = screenshot;
// Calculate window dimensions
const maxWidth = Math.min(1200, window.screen.availWidth * 0.8);
const maxHeight = Math.min(800, window.screen.availHeight * 0.8);
const left = (window.screen.availWidth - maxWidth) / 2;
const top = (window.screen.availHeight - maxHeight) / 2;
// Open editor window
console.log('Opening editor window...');
const editorUrl = chrome.runtime.getURL('editor.html');
this.editorWindow = window.open(
editorUrl,
'screenshot-editor',
`width=${maxWidth},height=${maxHeight},left=${left},top=${top},resizable=yes`
);
if (!this.editorWindow) {
throw new Error('Could not open editor window. Please allow popups for this extension.');
}
// Send screenshot to background script
chrome.runtime.sendMessage({
type: 'init-editor',
screenshot: this.screenshot
}, response => {
console.log('Screenshot sent to background, response:', response);
});
} catch (error) {
console.error('Screenshot error:', error);
this.showNotification('Error capturing screenshot: ' + error.message, 'error');
}
}
async startScreenRecording() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
// Recording implementation here
this.showNotification('Screen recording feature coming soon!', 'info');
} catch (error) {
this.showNotification('Error starting screen recording: ' + error.message, 'error');
}
}
showSettings() {
this.settingsPanel.classList.remove('hidden');
this.feedbackForm.classList.add('hidden');
}
showFeedbackForm() {
this.settingsPanel.classList.add('hidden');
this.feedbackForm.classList.remove('hidden');
}
showNotification(message, type = 'info') {
this.notification.textContent = message;
this.notification.className = `notification ${type}`;
this.notification.classList.remove('hidden');
setTimeout(() => {
this.notification.classList.add('hidden');
}, 5000);
}
}
// Initialize the extension
new FeedbackExtension();