diff --git a/tools/jira-ui/config.js b/tools/jira-ui/config.js new file mode 100644 index 0000000..b0576aa --- /dev/null +++ b/tools/jira-ui/config.js @@ -0,0 +1,49 @@ +// Configuration constants for the JIRA Issue Creator +const CONFIG = { + // JIRA API endpoints and settings + JIRA: { + API_VERSION: '2', + // Default JIRA instance settings + DEFAULT_INSTANCE: 'https://spycher.atlassian.net', + DEFAULT_PROJECT_KEY: 'KAN', + DEFAULT_EMAIL: 'subscription@spycher.one', + + ISSUE_TYPES: { + CO_INNOVATION: 'Co-Innovation', + EXPERIMENT: 'Experiment', + }, + + // Issue type display names + ISSUE_TYPE_NAMES: { + 'Co-Innovation': 'Co-Innovation', + Experiment: 'Experiment', + }, + }, + + // UI Constants + UI: { + SUCCESS_MESSAGE_TIMEOUT: 5000, // 5 seconds + MAX_SLACK_URLS: 10, + }, + + // Validation + VALIDATION: { + MIN_TITLE_LENGTH: 3, + MIN_DESCRIPTION_LENGTH: 10, + MAX_TITLE_LENGTH: 255, + MAX_DESCRIPTION_LENGTH: 32767, + }, +}; + +// Utility function to get API endpoint URL +function getJiraApiUrl(baseUrl, endpoint) { + const cleanBaseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash + return `${cleanBaseUrl}/rest/api/${CONFIG.JIRA.API_VERSION}/${endpoint}`; +} + +// Export for use in other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = { CONFIG, getJiraApiUrl }; +} + +export default CONFIG; diff --git a/tools/jira-ui/index.html b/tools/jira-ui/index.html new file mode 100644 index 0000000..0a9f04f --- /dev/null +++ b/tools/jira-ui/index.html @@ -0,0 +1,195 @@ + + + + + + JIRA Issue Creator + + + +
+ +
+
+

JIRA Issue Creator

+

Please log in with your corporate JIRA credentials

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+ OR +
+ +

+ Explore the interface without connecting to JIRA. No data will be sent or stored. +

+
+
+
+ + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/tools/jira-ui/script.js b/tools/jira-ui/script.js new file mode 100644 index 0000000..b78a504 --- /dev/null +++ b/tools/jira-ui/script.js @@ -0,0 +1,815 @@ +import CONFIG from './config.js'; + +// Main application state +const AppState = { + jiraUrl: '', + credentials: '', + projectKey: '', + currentUser: null, + isDemoMode: false, + selectedLabels: [], + useProxy: true, // Use proxy server to avoid CORS issues + proxyUrl: 'http://localhost:3001/api/jira', +}; + +// DOM Elements +const Elements = { + loginSection: null, + issueSection: null, + loginForm: null, + issueForm: null, + issueType: null, + successMessage: null, + errorMessage: null, + loadingOverlay: null, + logoutBtn: null, + demoBtn: null, + demoIndicator: null, + issueDetailsModal: null, + closeModal: null, + closeModalFooter: null, + createAnother: null, + createdIssueKey: null, + jiraIssueLink: null, + issueFieldsContainer: null, + labelsContainer: null, + selectedLabelsInput: null, + selectedLabelsDisplay: null, +}; + +// === Move helpers / used-by-others FIRST === + +function updateLabelsDisplay() { + if (Elements.selectedLabelsInput) { + Elements.selectedLabelsInput.value = AppState.selectedLabels.join(','); + } + + if (Elements.selectedLabelsDisplay) { + if (AppState.selectedLabels.length === 0) { + Elements.selectedLabelsDisplay.textContent = 'No labels selected'; + Elements.selectedLabelsDisplay.style.fontStyle = 'italic'; + Elements.selectedLabelsDisplay.style.color = '#6c757d'; + } else { + Elements.selectedLabelsDisplay.textContent = AppState.selectedLabels.join(', '); + Elements.selectedLabelsDisplay.style.fontStyle = 'normal'; + Elements.selectedLabelsDisplay.style.color = '#495057'; + } + } +} + +function hideMessages() { + Elements.successMessage.classList.add('hidden'); + Elements.errorMessage.classList.add('hidden'); +} + +function showSuccess(message) { + hideMessages(); + Elements.successMessage.textContent = message; + Elements.successMessage.classList.remove('hidden'); + + // Auto-hide success message + setTimeout(() => { + Elements.successMessage.classList.add('hidden'); + }, CONFIG.UI.SUCCESS_MESSAGE_TIMEOUT); +} + +function showError(message) { + hideMessages(); + Elements.errorMessage.textContent = message; + Elements.errorMessage.classList.remove('hidden'); +} + +function showLoading() { + Elements.loadingOverlay.classList.remove('hidden'); +} + +function hideLoading() { + Elements.loadingOverlay.classList.add('hidden'); +} + +function showLoginSection() { + Elements.loginSection.classList.remove('hidden'); + Elements.issueSection.classList.add('hidden'); +} + +function showIssueSection() { + Elements.loginSection.classList.add('hidden'); + Elements.issueSection.classList.remove('hidden'); +} + +function updateDemoIndicator() { + if (AppState.isDemoMode) { + Elements.demoIndicator.classList.remove('hidden'); + } else { + Elements.demoIndicator.classList.add('hidden'); + } +} + +function populateIssueFields(fields) { + Elements.issueFieldsContainer.innerHTML = ''; + + fields.forEach((field) => { + const fieldRow = document.createElement('div'); + fieldRow.className = `field-row ${field.userSet ? 'user-set' : ''}`; + + const fieldLabel = document.createElement('div'); + fieldLabel.className = `field-label ${field.userSet ? 'user-set' : ''}`; + fieldLabel.innerHTML = `${field.name}${field.userSet ? '' : ''}`; + + const fieldValue = document.createElement('div'); + fieldValue.className = `field-value ${field.userSet ? 'user-set' : ''}`; + + if (!field.value || field.value === '') { + fieldValue.textContent = 'None'; + fieldValue.className += ' empty'; + } else if (Array.isArray(field.value)) { + const list = document.createElement('ul'); + field.value.forEach((item) => { + const listItem = document.createElement('li'); + listItem.textContent = item; + list.appendChild(listItem); + }); + fieldValue.appendChild(list); + } else if (typeof field.value === 'string' && field.value.includes('\n')) { + fieldValue.textContent = field.value; + fieldValue.className += ' multi-line'; + } else { + fieldValue.textContent = field.value; + } + + fieldRow.appendChild(fieldLabel); + fieldRow.appendChild(fieldValue); + Elements.issueFieldsContainer.appendChild(fieldRow); + }); +} + +function clearLabelsSelection() { + AppState.selectedLabels = []; + + // Remove selected class from all label options + if (Elements.labelsContainer) { + const labelOptions = Elements.labelsContainer.querySelectorAll('.label-option'); + labelOptions.forEach((option) => { + option.classList.remove('selected'); + }); + } + + updateLabelsDisplay(); +} + +function showConditionalFields(issueType) { + const coInnovationFields = document.getElementById('co-innovation-fields'); + const experimentFields = document.getElementById('experiment-fields'); + + // Hide all conditional fields first + if (coInnovationFields) coInnovationFields.classList.add('hidden'); + if (experimentFields) experimentFields.classList.add('hidden'); + + // Show relevant fields based on issue type + if (issueType === 'Co-Innovation' && coInnovationFields) { + coInnovationFields.classList.remove('hidden'); + } else if (issueType === 'Experiment' && experimentFields) { + experimentFields.classList.remove('hidden'); + } +} + +function clearIssueForm() { + Elements.issueForm.reset(); + clearLabelsSelection(); + hideMessages(); + + // Hide all conditional fields + showConditionalFields(''); +} + +function updateRequiredFields(issueType) { + // Set required attributes for conditional fields + const customerNamesField = document.getElementById('customer-names'); + + if (issueType === 'Co-Innovation') { + if (customerNamesField) { + customerNamesField.setAttribute('required', ''); + } + } else if (customerNamesField) { + customerNamesField.removeAttribute('required'); + } +} + +function getProxyApiUrl(endpoint) { + if (AppState.useProxy) { + return `${AppState.proxyUrl}/${endpoint}`; + } + return CONFIG.getJiraApiUrl(AppState.jiraUrl, endpoint); +} + +function createFetchHeaders(includeAuth = true) { + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + + if (AppState.useProxy) { + headers['X-JIRA-URL'] = AppState.jiraUrl; + if (includeAuth && AppState.credentials) { + headers.Authorization = `Basic ${AppState.credentials}`; + } + } else if (includeAuth && AppState.credentials) { + headers.Authorization = `Basic ${AppState.credentials}`; + } + + return headers; +} + +// === end move helpers === + +function initializeElements() { + Elements.loginSection = document.getElementById('login-section'); + Elements.issueSection = document.getElementById('issue-section'); + Elements.loginForm = document.getElementById('login-form'); + Elements.issueForm = document.getElementById('issue-form'); + Elements.issueType = document.getElementById('issue-type'); + Elements.successMessage = document.getElementById('success-message'); + Elements.errorMessage = document.getElementById('error-message'); + Elements.loadingOverlay = document.getElementById('loading-overlay'); + Elements.logoutBtn = document.getElementById('logout-btn'); + Elements.demoBtn = document.getElementById('demo-btn'); + Elements.demoIndicator = document.getElementById('demo-indicator'); + Elements.issueDetailsModal = document.getElementById('issue-details-modal'); + Elements.closeModal = document.getElementById('close-modal'); + Elements.closeModalFooter = document.getElementById('close-modal-footer'); + Elements.createAnother = document.getElementById('create-another'); + Elements.createdIssueKey = document.getElementById('created-issue-key'); + Elements.jiraIssueLink = document.getElementById('jira-issue-link'); + Elements.issueFieldsContainer = document.getElementById('issue-fields-container'); + Elements.labelsContainer = document.getElementById('labels-container'); + Elements.selectedLabelsInput = document.getElementById('selected-labels'); + Elements.selectedLabelsDisplay = document.getElementById('selected-labels-display'); + + // Initialize labels display + updateLabelsDisplay(); +} + +// Check for existing session +function checkExistingSession() { + const savedCredentials = localStorage.getItem('jira-credentials'); + if (savedCredentials) { + try { + const credentials = JSON.parse(savedCredentials); + AppState.jiraUrl = credentials.jiraUrl; + AppState.credentials = credentials.auth; + AppState.projectKey = credentials.projectKey; + AppState.currentUser = { name: credentials.username }; + AppState.isDemoMode = credentials.isDemoMode || false; + showIssueSection(); + updateDemoIndicator(); + } catch (e) { + localStorage.removeItem('jira-credentials'); + } + } +} + +// Handle demo mode activation +function handleDemoMode() { + // Set demo mode state + AppState.isDemoMode = true; + AppState.jiraUrl = 'https://demo.atlassian.net'; + AppState.projectKey = 'DEMO'; + AppState.currentUser = { name: 'demo-user', displayName: 'Demo User' }; + + // Save demo session + localStorage.setItem('jira-credentials', JSON.stringify({ + jiraUrl: AppState.jiraUrl, + auth: 'demo', + projectKey: AppState.projectKey, + username: AppState.currentUser.name, + isDemoMode: true, + })); + + // Initialize labels display for demo mode + updateLabelsDisplay(); + + showIssueSection(); + updateDemoIndicator(); + showSuccess('Demo mode activated! You can now explore the interface. No data will be sent to JIRA.'); +} + +// Handle login form submission +async function handleLogin(event) { + event.preventDefault(); + + const formData = new FormData(Elements.loginForm); + const jiraUrl = formData.get('jira-url'); + const username = formData.get('username'); + const password = formData.get('password'); + const projectKey = formData.get('project-key'); + + // Validate inputs + if (!jiraUrl || !username || !password || !projectKey) { + showError('Please fill in all required fields.'); + return; + } + + showLoading(); + + try { + // Create basic auth credentials + const credentials = btoa(`${username}:${password}`); + console.log('🔐 Created credentials for:', username); + console.log('🔑 Credentials length:', credentials.length); + console.log('🔑 First 20 chars:', `${credentials.substring(0, 20)}...`); + + // Set credentials in AppState for proxy to use + AppState.jiraUrl = jiraUrl; + AppState.credentials = credentials; + AppState.projectKey = projectKey; + + console.log('🌐 JIRA URL:', jiraUrl); + console.log('📁 Project Key:', projectKey); + + // Test authentication by getting current user + const userResponse = await fetch(getProxyApiUrl('myself'), { + method: 'GET', + headers: createFetchHeaders(), + }); + + console.log('📊 Auth response status:', userResponse.status); + + if (!userResponse.ok) { + const errorText = await userResponse.text(); + console.log('❌ Auth error response:', errorText); + throw new Error('Authentication failed. Please check your credentials.'); + } + + const userData = await userResponse.json(); + console.log('✅ Authenticated user:', userData.displayName); + + // Verify project exists + const projectResponse = await fetch(getProxyApiUrl(`project/${projectKey}`), { + method: 'GET', + headers: createFetchHeaders(), + }); + + if (!projectResponse.ok) { + throw new Error(`Project "${projectKey}" not found or you don't have access.`); + } + + // Store user info + AppState.currentUser = userData; + + // Save to localStorage for session persistence + localStorage.setItem('jira-credentials', JSON.stringify({ + jiraUrl, + auth: credentials, + projectKey, + username: userData.name, + isDemoMode: false, + })); + + hideLoading(); + showIssueSection(); + updateDemoIndicator(); + showSuccess('Successfully logged in!'); + } catch (error) { + hideLoading(); + showError(error.message); + // Clear credentials on error + AppState.jiraUrl = ''; + AppState.credentials = ''; + AppState.projectKey = ''; + AppState.currentUser = null; + } +} + +// Handle logout +function handleLogout() { + AppState.jiraUrl = ''; + AppState.credentials = ''; + AppState.projectKey = ''; + AppState.currentUser = null; + AppState.isDemoMode = false; + + localStorage.removeItem('jira-credentials'); + + // Reset forms + Elements.loginForm.reset(); + Elements.issueForm.reset(); + + showLoginSection(); + updateDemoIndicator(); + hideMessages(); +} + +// Handle issue type change +function handleIssueTypeChange(event) { + const selectedType = event.target.value; + + // Show/hide conditional fields based on issue type + showConditionalFields(selectedType); + updateRequiredFields(selectedType); +} + +// Update required fields based on issue type +// (moved above) + +// Show/hide conditional fields based on issue type +// (moved above) + +// Handle label selection +function handleLabelClick(event) { + if (event.target.classList.contains('label-option')) { + const { label } = event.target.dataset; + const isSelected = event.target.classList.contains('selected'); + + if (isSelected) { + // Remove label from selection + AppState.selectedLabels = AppState.selectedLabels.filter((l) => l !== label); + event.target.classList.remove('selected'); + } else { + // Add label to selection + AppState.selectedLabels.push(label); + event.target.classList.add('selected'); + } + + updateLabelsDisplay(); + } +} + +// Validate issue form +function validateIssueForm(formData) { + const issueType = formData.get('issue-type'); + const title = formData.get('title'); + const description = formData.get('description'); + + if (!issueType) { + return { isValid: false, error: 'Please select an issue type.' }; + } + + if (!title || title.trim().length < CONFIG.VALIDATION.MIN_TITLE_LENGTH) { + return { isValid: false, error: `Title must be at least ${CONFIG.VALIDATION.MIN_TITLE_LENGTH} characters long.` }; + } + + if (title.length > CONFIG.VALIDATION.MAX_TITLE_LENGTH) { + return { isValid: false, error: `Title must be less than ${CONFIG.VALIDATION.MAX_TITLE_LENGTH} characters.` }; + } + + if (!description || description.trim().length < CONFIG.VALIDATION.MIN_DESCRIPTION_LENGTH) { + return { isValid: false, error: `Description must be at least ${CONFIG.VALIDATION.MIN_DESCRIPTION_LENGTH} characters long.` }; + } + + if (description.length > CONFIG.VALIDATION.MAX_DESCRIPTION_LENGTH) { + return { isValid: false, error: `Description must be less than ${CONFIG.VALIDATION.MAX_DESCRIPTION_LENGTH} characters.` }; + } + + // Validate issue-type specific fields + if (issueType === CONFIG.JIRA.ISSUE_TYPES.CO_INNOVATION) { + const customerNames = formData.get('customer-names'); + if (!customerNames || customerNames.trim().length === 0) { + return { isValid: false, error: 'Customer Names is required for Co-Innovation issues.' }; + } + } + + return { isValid: true }; +} + +// Build issue data for JIRA API +function buildIssueData(formData) { + const issueType = formData.get('issue-type'); + const title = formData.get('title'); + let description = formData.get('description'); + + // Get the proper issue type name for JIRA + const issueTypeName = CONFIG.JIRA.ISSUE_TYPE_NAMES[issueType] || issueType; + + // Add conditional field data to description + if (issueType === 'Co-Innovation') { + const customerNames = formData.get('customer-names'); + if (customerNames && customerNames.trim()) { + description += `\n\n*Customer Names:* ${customerNames.trim()}`; + } + } else if (issueType === 'Experiment') { + const hypothesis = formData.get('hypothesis'); + const successCriteria = formData.get('success-criteria'); + + if (hypothesis && hypothesis.trim()) { + description += `\n\n*Hypothesis:* ${hypothesis.trim()}`; + } + if (successCriteria && successCriteria.trim()) { + description += `\n\n*Success Criteria:* ${successCriteria.trim()}`; + } + } + + // Get current user name safely + let assigneeName; + if (AppState.currentUser) { + if (typeof AppState.currentUser === 'string') { + assigneeName = AppState.currentUser; + } else if (AppState.currentUser.name) { + assigneeName = AppState.currentUser.name; + } + } + + // Base issue data structure + const issueData = { + fields: { + project: { key: AppState.projectKey }, + summary: title.trim(), + description: description.trim(), + issuetype: { name: issueTypeName }, + }, + }; + + // Only add assignee if we have a valid user + if (assigneeName) { + issueData.fields.assignee = { name: assigneeName }; + } + + // Add labels if any are selected + if (AppState.selectedLabels.length > 0) { + issueData.fields.labels = AppState.selectedLabels; + } + + return issueData; +} + +// Clear issue form for next creation +// (moved above) + +// Clear labels selection +// (moved above) + +// UI Helper Functions +// (all moved above) + +// Update demo indicator visibility +// (moved above) + +// Create demo issue data +function createDemoIssueData(formData, issueKey) { + const issueType = formData.get('issue-type'); + const title = formData.get('title'); + const description = formData.get('description'); + + // Get the proper issue type display name + const issueTypeDisplayName = CONFIG.JIRA.ISSUE_TYPE_NAMES[issueType] || issueType; + + // Base demo fields that all issues have + const demoFields = [ + { name: 'Key', value: issueKey, userSet: false }, + { name: 'Project', value: 'KAN Project (KAN)', userSet: false }, + { name: 'Issue Type', value: issueTypeDisplayName, userSet: true }, + { name: 'Summary', value: title, userSet: true }, + { name: 'Description', value: description, userSet: true }, + { name: 'Status', value: 'To Do', userSet: false }, + { name: 'Priority', value: 'Medium', userSet: false }, + { name: 'Assignee', value: 'Demo User (demo-user)', userSet: false }, + { name: 'Reporter', value: 'Demo User (demo-user)', userSet: false }, + { name: 'Created', value: new Date().toLocaleString(), userSet: false }, + { name: 'Updated', value: new Date().toLocaleString(), userSet: false }, + { name: 'Labels', value: AppState.selectedLabels.length > 0 ? AppState.selectedLabels.join(', ') : 'None', userSet: AppState.selectedLabels.length > 0 }, + { name: 'Components', value: 'None', userSet: false }, + { name: 'Fix Version/s', value: 'None', userSet: false }, + { name: 'Affects Version/s', value: 'None', userSet: false }, + ]; + + // Add some demo-specific fields based on issue type + if (issueType === 'Co-Innovation') { + const customerNames = formData.get('customer-names') || 'Customer A, Customer B'; + demoFields.push({ name: 'Customer Names', value: customerNames, userSet: !!formData.get('customer-names') }); + demoFields.push({ name: 'Innovation Status', value: 'In Progress', userSet: false }); + demoFields.push({ name: 'Expected Outcome', value: 'Proof of Concept', userSet: false }); + } else if (issueType === 'Experiment') { + const hypothesis = formData.get('hypothesis') || 'Testing new approach'; + const successCriteria = formData.get('success-criteria') || 'Metric improvement by 20%'; + demoFields.push({ name: 'Hypothesis', value: hypothesis, userSet: !!formData.get('hypothesis') }); + demoFields.push({ name: 'Success Criteria', value: successCriteria, userSet: !!formData.get('success-criteria') }); + demoFields.push({ name: 'Experiment Duration', value: '2 weeks', userSet: false }); + } + + return { fields: demoFields }; +} + +// Parse JIRA issue data into our display format +function parseJiraIssueData(jiraIssue) { + const { fields } = jiraIssue; + // Fields that user directly set + // not used, so comment out for linting pass + // const userSetFields = ['summary', 'description', 'issuetype']; + + const displayFields = [ + { name: 'Key', value: jiraIssue.key, userSet: false }, + { name: 'Project', value: fields.project ? `${fields.project.name} (${fields.project.key})` : 'Unknown', userSet: false }, + { name: 'Issue Type', value: fields.issuetype ? fields.issuetype.name : 'Unknown', userSet: true }, + { name: 'Summary', value: fields.summary || '', userSet: true }, + { name: 'Description', value: fields.description || '', userSet: true }, + { name: 'Status', value: fields.status ? fields.status.name : 'Unknown', userSet: false }, + { name: 'Priority', value: fields.priority ? fields.priority.name : 'None', userSet: false }, + { name: 'Assignee', value: fields.assignee ? `${fields.assignee.displayName} (${fields.assignee.name})` : 'Unassigned', userSet: false }, + { name: 'Reporter', value: fields.reporter ? `${fields.reporter.displayName} (${fields.reporter.name})` : 'Unknown', userSet: false }, + { name: 'Created', value: fields.created ? new Date(fields.created).toLocaleString() : 'Unknown', userSet: false }, + { name: 'Updated', value: fields.updated ? new Date(fields.updated).toLocaleString() : 'Unknown', userSet: false }, + ]; + + // Add custom fields if they exist + Object.keys(fields).forEach((fieldKey) => { + if (fieldKey.startsWith('customfield_')) { + const fieldValue = fields[fieldKey]; + if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') { + const isUserSet = Object.values(CONFIG.JIRA.CUSTOM_FIELDS).includes(fieldKey); + displayFields.push({ + name: `Custom Field (${fieldKey})`, + value: typeof fieldValue === 'object' ? JSON.stringify(fieldValue) : fieldValue, + userSet: isUserSet, + }); + } + } + }); + + return { fields: displayFields }; +} + +// Fetch real issue details from JIRA +async function fetchIssueDetails(issueKey) { + try { + const response = await fetch(getProxyApiUrl(`issue/${issueKey}`), { + method: 'GET', + headers: createFetchHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to fetch issue details'); + } + + const issueData = await response.json(); + return parseJiraIssueData(issueData); + } catch (error) { + console.error('Error fetching issue details:', error); + // Return basic structure if fetch fails + return { + fields: [ + { name: 'Key', value: issueKey, userSet: false }, + { name: 'Status', value: 'Created', userSet: false }, + ], + }; + } +} + +// Show issue details modal +function showIssueDetailsModal(issueData, issueKey, isDemo = false) { + // Set issue key + Elements.createdIssueKey.textContent = issueKey; + + // Set JIRA link + if (isDemo) { + Elements.jiraIssueLink.href = '#'; + Elements.jiraIssueLink.style.opacity = '0.6'; + Elements.jiraIssueLink.title = 'Demo mode - no actual JIRA link'; + } else { + Elements.jiraIssueLink.href = `${AppState.jiraUrl}/browse/${issueKey}`; + Elements.jiraIssueLink.style.opacity = '1'; + Elements.jiraIssueLink.title = 'Open issue in JIRA'; + } + + // Populate fields + populateIssueFields(issueData.fields); + + // Show modal + Elements.issueDetailsModal.classList.remove('hidden'); + + // Clear form for next issue + clearIssueForm(); +} + +// Populate issue fields in modal +// (moved above) + +// Close issue details modal +function closeIssueModal() { + Elements.issueDetailsModal.classList.add('hidden'); +} + +// Create another issue (close modal and clear form) +function createAnotherIssue() { + closeIssueModal(); + // Form is already cleared by showIssueDetailsModal +} + +// Handle demo issue creation (no API call) +async function handleDemoIssueCreation(formData) { + // Simulate API delay + await new Promise((resolve) => { + setTimeout(resolve, 1500); + }); + + // const title = formData.get('title'); // not used, so commented out for linting pass + // const issueType = formData.get('issue-type'); // not used, so commented out for linting pass + + // Generate a fake issue key + const issueNumber = Math.floor(Math.random() * 900) + 100; // Random 3-digit number + const fakeIssueKey = `DEMO-${issueNumber}`; + + hideLoading(); + + // Create fake issue data for demo + const demoIssueData = createDemoIssueData(formData, fakeIssueKey); + + // Show issue details modal + showIssueDetailsModal(demoIssueData, fakeIssueKey, true); +} + +// Handle real JIRA issue creation +async function handleRealIssueCreation(formData) { + // Build issue data + const issueData = buildIssueData(formData); + + // Create issue via JIRA API + const response = await fetch(getProxyApiUrl('issue'), { + method: 'POST', + headers: createFetchHeaders(), + body: JSON.stringify(issueData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.errorMessages + ? errorData.errorMessages.join(', ') + : `Failed to create issue: ${response.status}`); + } + + const result = await response.json(); + + // Get full issue details + const issueDetails = await fetchIssueDetails(result.key); + + hideLoading(); + + // Show issue details modal + showIssueDetailsModal(issueDetails, result.key, false); +} + +// Handle issue creation +async function handleIssueCreation(event) { + event.preventDefault(); + + const formData = new FormData(Elements.issueForm); + + // Validate form + const validationResult = validateIssueForm(formData); + if (!validationResult.isValid) { + showError(validationResult.error); + return; + } + + showLoading(); + hideMessages(); + + try { + if (AppState.isDemoMode) { + // Handle demo mode - simulate success without API call + await handleDemoIssueCreation(formData); + } else { + // Real JIRA API call + await handleRealIssueCreation(formData); + } + } catch (error) { + hideLoading(); + showError(`Failed to create issue: ${error.message}`); + } +} + +// Bind event listeners +function bindEventListeners() { + Elements.loginForm.addEventListener('submit', handleLogin); + Elements.issueForm.addEventListener('submit', handleIssueCreation); + Elements.issueType.addEventListener('change', handleIssueTypeChange); + Elements.logoutBtn.addEventListener('click', handleLogout); + Elements.demoBtn.addEventListener('click', handleDemoMode); + Elements.closeModal.addEventListener('click', closeIssueModal); + Elements.closeModalFooter.addEventListener('click', closeIssueModal); + Elements.createAnother.addEventListener('click', createAnotherIssue); + + // Label selection event listeners + if (Elements.labelsContainer) { + Elements.labelsContainer.addEventListener('click', handleLabelClick); + } + + // Close modal when clicking outside + Elements.issueDetailsModal.addEventListener('click', (e) => { + if (e.target === Elements.issueDetailsModal) { + closeIssueModal(); + } + }); + + // Close modal with Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !Elements.issueDetailsModal.classList.contains('hidden')) { + closeIssueModal(); + } + }); +} + +// Initialize the application +document.addEventListener('DOMContentLoaded', () => { + initializeElements(); + bindEventListeners(); + checkExistingSession(); +}); diff --git a/tools/jira-ui/styles.css b/tools/jira-ui/styles.css new file mode 100644 index 0000000..74a5abc --- /dev/null +++ b/tools/jira-ui/styles.css @@ -0,0 +1,676 @@ +/* Reset and Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +/* Container and Layout */ +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.section { + width: 100%; +} + +.card { + background: white; + border-radius: 12px; + padding: 40px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + position: relative; +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.header h1 { + margin: 0; +} + +.header-actions { + display: flex; + align-items: center; + gap: 15px; +} + +/* Typography */ +h1 { + color: #2c3e50; + font-size: 2rem; + font-weight: 600; + margin-bottom: 10px; + text-align: center; +} + +.subtitle { + color: #7f8c8d; + text-align: center; + margin-bottom: 30px; + font-size: 1.1rem; +} + +/* Form Styles */ +.form-group { + margin-bottom: 24px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #2c3e50; + font-size: 0.95rem; +} + +input[type="text"], +input[type="url"], +input[type="password"], +textarea, +select { + width: 100%; + padding: 12px 16px; + border: 2px solid #e1e8ed; + border-radius: 8px; + font-size: 1rem; + font-family: inherit; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + background: #fff; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +textarea { + resize: vertical; + min-height: 120px; +} + +/* Button Styles */ +.btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: inline-block; + text-align: center; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #f8f9fa; + color: #6c757d; + border: 1px solid #dee2e6; +} + +.btn-secondary:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.btn-large { + width: 100%; + padding: 16px 24px; + font-size: 1.1rem; + margin-top: 20px; +} + +.btn-small { + padding: 8px 16px; + font-size: 0.9rem; +} + +.btn-demo { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; + width: 100%; +} + +.btn-demo:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4); +} + +/* Conditional Fields */ +.conditional-fields { + margin-top: 30px; + padding-top: 30px; + border-top: 2px solid #f8f9fa; +} + +.conditional-fields h3 { + color: #495057; + margin-bottom: 20px; + font-size: 1.2rem; +} + +/* Field Help Text */ +.field-help { + display: block; + margin-top: 5px; + font-size: 0.85rem; + color: #6c757d; + font-style: italic; +} + +/* Dynamic Slack URLs */ +#slack-urls-container .form-group { + position: relative; + margin-bottom: 16px; +} + +.slack-url-input { + margin-bottom: 10px; +} + +.remove-slack-url { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: #dc3545; + color: white; + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + cursor: pointer; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +.remove-slack-url:hover { + background: #c82333; +} + +/* Labels Field Styles */ +.labels-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + padding: 12px; + border: 2px solid #e1e8ed; + border-radius: 8px; + background: #f8f9fa; +} + +.label-option { + padding: 6px 12px; + background: #fff; + border: 2px solid #dee2e6; + border-radius: 20px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + color: #495057; + transition: all 0.2s ease; + user-select: none; +} + +.label-option:hover { + border-color: #667eea; + background: #f0f2ff; + transform: translateY(-1px); +} + +.label-option.selected { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: #667eea; +} + +.label-option.selected:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); + transform: translateY(-1px); +} + +.selected-labels-display { + min-height: 20px; + font-size: 0.9rem; + color: #6c757d; + font-style: italic; +} + +.selected-labels-display:not(:empty) { + font-style: normal; + color: #495057; +} + +.selected-labels-display:not(:empty):before { + content: "Selected: "; + font-weight: 500; +} + +/* Messages */ +.success-message, +.error-message { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 20px; + font-weight: 500; +} + +.success-message { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.error-message { + background: #f8d7da; + color: #721c24; + border: 1px solid #f1b0b7; +} + +/* Loading Overlay */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid #f3f3f3; + border-top: 5px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +.loading-overlay p { + color: white; + font-size: 1.1rem; + font-weight: 500; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Demo Section */ +.demo-section { + margin-top: 30px; + text-align: center; +} + +.divider { + position: relative; + margin: 20px 0; + text-align: center; +} + +.divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #e1e8ed; +} + +.divider span { + background: white; + padding: 0 20px; + color: #7f8c8d; + font-size: 0.9rem; + position: relative; + z-index: 1; +} + +.demo-description { + margin-top: 10px; + color: #7f8c8d; + font-size: 0.9rem; + line-height: 1.4; +} + +.demo-indicator { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 5px; +} + +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + backdrop-filter: blur(5px); +} + +.modal-content { + background: white; + border-radius: 12px; + max-width: 700px; + width: 90%; + max-height: 90vh; + overflow: hidden; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + animation: modalSlideIn 0.3s ease-out; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 25px 30px; + border-bottom: 2px solid #f8f9fa; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.modal-header h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.close-modal { + background: none; + border: none; + color: white; + font-size: 2rem; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s ease; +} + +.close-modal:hover { + background: rgba(255, 255, 255, 0.2); +} + +.modal-body { + padding: 30px; + max-height: 60vh; + overflow-y: auto; +} + +.issue-link-section { + margin-bottom: 30px; + text-align: center; + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 8px; +} + +.issue-key-display { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +.issue-key { + font-size: 1.8rem; + font-weight: 700; + color: #2c3e50; + background: white; + padding: 12px 20px; + border-radius: 8px; + border: 2px solid #667eea; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2); +} + +.issue-details-section h3 { + margin-bottom: 20px; + color: #2c3e50; + font-size: 1.3rem; + font-weight: 600; + border-bottom: 2px solid #f8f9fa; + padding-bottom: 10px; +} + +.fields-container { + display: grid; + gap: 15px; +} + +.field-row { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 15px; + padding: 15px; + border-radius: 8px; + transition: background-color 0.3s ease; +} + +.field-row:hover { + background: #f8f9fa; +} + +.field-row.user-set { + background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); + border-left: 4px solid #667eea; +} + +.field-row.user-set:hover { + background: linear-gradient(135deg, #bbdefb 0%, #e1bee7 100%); +} + +.field-label { + font-weight: 600; + color: #495057; + display: flex; + align-items: center; +} + +.field-label.user-set { + color: #2c3e50; +} + +.field-label .user-set-indicator { + margin-left: 8px; + color: #667eea; + font-size: 0.8rem; +} + +.field-value { + color: #6c757d; + word-break: break-word; +} + +.field-value.user-set { + color: #2c3e50; + font-weight: 500; +} + +.field-value.empty { + font-style: italic; + color: #adb5bd; +} + +.field-value.multi-line { + white-space: pre-wrap; + max-height: 150px; + overflow-y: auto; + padding: 8px 12px; + background: #f8f9fa; + border-radius: 4px; +} + +.field-value ul { + margin: 0; + padding-left: 20px; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 15px; + padding: 25px 30px; + border-top: 2px solid #f8f9fa; + background: #f8f9fa; +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +.required { + color: #dc3545; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .card { + padding: 25px; + } + + h1 { + font-size: 1.6rem; + } + + .header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .header h1 { + margin-bottom: 0; + } + + .header-actions { + flex-direction: column; + gap: 10px; + } + + .demo-indicator { + font-size: 0.8rem; + padding: 4px 10px; + } + + .modal-content { + width: 95%; + max-height: 95vh; + } + + .modal-body { + padding: 20px; + max-height: 70vh; + } + + .modal-header { + padding: 20px; + } + + .modal-footer { + padding: 20px; + flex-direction: column; + gap: 10px; + } + + .issue-key-display { + flex-direction: column; + gap: 15px; + } + + .field-row { + grid-template-columns: 1fr; + gap: 8px; + } +} + +@media (max-width: 480px) { + .card { + padding: 20px; + } + + .btn { + padding: 10px 20px; + } + + .btn-large { + padding: 14px 20px; + } +} \ No newline at end of file