From 254bdbfbc06564ec50061c945c0931a8e51e20e6 Mon Sep 17 00:00:00 2001 From: Tuan Phan Date: Thu, 19 Jun 2025 23:08:05 -0700 Subject: [PATCH 1/3] Add https://git.corp.adobe.com/Blackbird/jira-ui as a tool under tools. Temporary remove tools/jira-ui/test-auth.js due to existence of apiToken which violates GitHub policy --- tools/jira-ui/README.md | 195 +++++++ tools/jira-ui/config.js | 47 ++ tools/jira-ui/index.html | 195 +++++++ tools/jira-ui/package-lock.json | 899 ++++++++++++++++++++++++++++++++ tools/jira-ui/package.json | 18 + tools/jira-ui/proxy-server.js | 92 ++++ tools/jira-ui/script.js | 803 ++++++++++++++++++++++++++++ tools/jira-ui/styles.css | 676 ++++++++++++++++++++++++ 8 files changed, 2925 insertions(+) create mode 100644 tools/jira-ui/README.md create mode 100644 tools/jira-ui/config.js create mode 100644 tools/jira-ui/index.html create mode 100644 tools/jira-ui/package-lock.json create mode 100644 tools/jira-ui/package.json create mode 100644 tools/jira-ui/proxy-server.js create mode 100644 tools/jira-ui/script.js create mode 100644 tools/jira-ui/styles.css diff --git a/tools/jira-ui/README.md b/tools/jira-ui/README.md new file mode 100644 index 0000000..c790885 --- /dev/null +++ b/tools/jira-ui/README.md @@ -0,0 +1,195 @@ +# JIRA Issue Creator + +A simplified, minimal web interface for creating issues in an on-premise JIRA instance. Built with vanilla HTML, CSS, and JavaScript according to enterprise requirements. +![image](https://git.corp.adobe.com/Blackbird/jira-ui/assets/371/7a579bb2-53a8-491f-8639-5d73d135a9b9) + +## Features + +- **Secure Authentication**: Login with corporate JIRA credentials +- **Project-Focused**: Automatic selection of predetermined default project +- **Issue Type Support**: Create "Experiment" or "Co-Innovation" issues +- **Conditional Fields**: Dynamic field display based on issue type +- **Auto-Assignment**: Issues are automatically assigned to the logged-in user +- **Session Persistence**: Stay logged in across browser sessions +- **Modern UI**: Clean, responsive design with professional styling + +## File Structure + +``` +jira-ui/ +├── index.html # Main application interface +├── styles.css # Styling and responsive design +├── script.js # Core functionality and JIRA API integration +├── config.js # Configuration constants and settings +└── README.md # This file +``` + +## Setup and Configuration + +### 1. JIRA Instance Configuration + +Before using this application, you need to configure it for your specific JIRA instance: + +#### Custom Field IDs +In `config.js`, update the `CUSTOM_FIELDS` section with your actual JIRA custom field IDs: + +```javascript +CUSTOM_FIELDS: { + CUSTOMER_NAMES: 'customfield_10001', // Replace with actual field ID + SLACK_CHANNELS: 'customfield_10002', // Replace with actual field ID + GIT_REPO_URL: 'customfield_10003' // Replace with actual field ID +} +``` + +To find your custom field IDs: +1. Go to JIRA Administration → Issues → Custom Fields +2. Find the relevant fields and note their IDs +3. Or use the JIRA API: `GET /rest/api/2/field` to list all fields + +#### Issue Types +Ensure your JIRA project has issue types named: +- "Experiment" +- "Co-Innovation" + +If your issue types have different names, update them in `config.js`: + +```javascript +ISSUE_TYPES: { + EXPERIMENT: 'your-experiment-type-name', + CO_INNOVATION: 'your-co-innovation-type-name' +} +``` + +### 2. Web Server Deployment + +This application needs to be served from a web server due to CORS restrictions when making API calls to JIRA. + +#### Option A: Simple HTTP Server (Development) +```bash +# Using Python 3 +python -m http.server 8000 + +# Using Node.js (if you have http-server installed) +npx http-server + +# Using PHP +php -S localhost:8000 +``` + +#### Option B: Production Web Server +Deploy the files to your preferred web server (Apache, Nginx, IIS, etc.) + +### 3. JIRA API Token (Recommended) + +For enhanced security, use API tokens instead of passwords: + +1. Go to https://id.atlassian.com/manage-profile/security/api-tokens +2. Create a new API token +3. Use your email address as the username and the API token as the password + +## Usage + +### Login Process + +1. **JIRA Instance URL**: Enter your JIRA base URL (e.g., `https://company.atlassian.net`) +2. **Username**: Your JIRA username or email address +3. **Password/API Token**: Your password or API token +4. **Project Key**: The key of your default project (e.g., "PROJ") + +### Creating Issues + +1. **Select Issue Type**: Choose between "Experiment" or "Co-Innovation" +2. **Fill Required Fields**: Title and Description are always required +3. **Conditional Fields**: + - **Co-Innovation**: Customer Names (required) + optional Slack Channel URLs + - **Experiment**: Optional Git Repository URL +4. **Submit**: Click "Create Issue" to submit to JIRA + +### Dynamic Fields + +- **Slack Channel URLs**: Use "Add Channel URL" button to add multiple Slack channels +- **Field Validation**: All fields are validated before submission +- **Auto-Clear**: Form clears automatically after successful issue creation + +## Technical Details + +### Browser Compatibility +- Modern browsers with ES6+ support +- Chrome 60+, Firefox 55+, Safari 12+, Edge 79+ + +### Security Considerations +- Credentials are stored in browser localStorage for session persistence +- Basic Authentication is used for JIRA API calls +- No credentials are transmitted to external servers +- HTTPS is recommended for production deployment + +### API Integration +- Uses JIRA REST API v2 +- Authenticates via Basic Auth +- Validates user permissions and project access +- Creates issues with proper field mapping + +## Troubleshooting + +### Common Issues + +1. **CORS Errors**: Ensure the application is served from a web server, not opened as a file +2. **Authentication Failed**: Check username/password and JIRA URL +3. **Project Not Found**: Verify the project key and user permissions +4. **Custom Fields Not Saving**: Update custom field IDs in `config.js` + +### Error Messages +The application provides detailed error messages for: +- Authentication failures +- Network connectivity issues +- Validation errors +- JIRA API errors + +### Browser Console +Check the browser console for detailed error information during development. + +## Customization + +### Adding New Fields +1. Add the field to the HTML form in `index.html` +2. Update validation logic in `script.js` +3. Add field mapping in the `buildIssueData()` function +4. Update custom field IDs in `config.js` + +### Styling Changes +Modify `styles.css` to customize: +- Color scheme +- Layout and spacing +- Responsive breakpoints +- Animation effects + +### Configuration Options +Update `config.js` to modify: +- Validation rules +- UI behavior +- API settings +- Custom field mappings + +## Requirements Compliance + +This application fulfills all specified functional and non-functional requirements: + +✅ **Functional Requirements** +- FR-1: User authentication with corporate JIRA credentials +- FR-2: Automatic default project selection +- FR-3: Issue type selection (Experiment/Co-Innovation) +- FR-4: Mandatory Title and Description fields +- FR-5: Conditional fields based on issue type +- FR-6: Dynamic Slack URL field addition +- FR-7: Automatic issue assignment to logged-in user +- FR-8: No file attachment functionality +- FR-9: Success message with issue key and form clearing + +✅ **Non-Functional Requirements** +- NFR-1: Vanilla HTML, CSS, JavaScript only (no frameworks) +- Clean, fast, and minimal interface +- JIRA API integration + +## License + +This application is developed for internal enterprise use according to the specified requirements. diff --git a/tools/jira-ui/config.js b/tools/jira-ui/config.js new file mode 100644 index 0000000..fcfd611 --- /dev/null +++ b/tools/jira-ui/config.js @@ -0,0 +1,47 @@ +// 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 }; +} \ No newline at end of file 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/package-lock.json b/tools/jira-ui/package-lock.json new file mode 100644 index 0000000..f97d284 --- /dev/null +++ b/tools/jira-ui/package-lock.json @@ -0,0 +1,899 @@ +{ + "name": "jira-ui-proxy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jira-ui-proxy", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/tools/jira-ui/package.json b/tools/jira-ui/package.json new file mode 100644 index 0000000..89998b0 --- /dev/null +++ b/tools/jira-ui/package.json @@ -0,0 +1,18 @@ +{ + "name": "jira-ui-proxy", + "version": "1.0.0", + "description": "Proxy server for JIRA UI to handle CORS", + "main": "proxy-server.js", + "scripts": { + "start": "node proxy-server.js", + "dev": "node proxy-server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "node-fetch": "^2.6.7" + }, + "keywords": ["jira", "proxy", "cors"], + "author": "", + "license": "MIT" +} \ No newline at end of file diff --git a/tools/jira-ui/proxy-server.js b/tools/jira-ui/proxy-server.js new file mode 100644 index 0000000..9a6a3dd --- /dev/null +++ b/tools/jira-ui/proxy-server.js @@ -0,0 +1,92 @@ +const express = require('express'); +const cors = require('cors'); +const fetch = require('node-fetch'); +const path = require('path'); + +const app = express(); +const PORT = 3001; + +// Enable CORS for all routes +app.use(cors()); + +// Parse JSON bodies +app.use(express.json()); + +// Serve static files from the current directory +app.use(express.static('.')); + +// JIRA API Proxy endpoint +app.all('/api/jira/*', async (req, res) => { + try { + // Extract the JIRA URL and endpoint from the request + const jiraUrl = req.headers['x-jira-url']; + const jiraEndpoint = req.params[0]; // Everything after /api/jira/ + + console.log(`🔍 Proxy request: ${req.method} ${jiraEndpoint}`); + console.log(`🌐 JIRA URL: ${jiraUrl}`); + console.log(`🔑 Has Authorization: ${!!req.headers.authorization}`); + + if (!jiraUrl) { + console.log('❌ Missing X-JIRA-URL header'); + return res.status(400).json({ error: 'Missing X-JIRA-URL header' }); + } + + // Construct the full JIRA API URL + const fullUrl = `${jiraUrl}/rest/api/2/${jiraEndpoint}`; + console.log(`📍 Full URL: ${fullUrl}`); + + // Forward the authorization header + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + if (req.headers.authorization) { + headers.Authorization = req.headers.authorization; + console.log(`🔐 Authorization header: ${req.headers.authorization.substring(0, 20)}...`); + } else { + console.log('⚠️ No Authorization header found'); + } + + // Make the request to JIRA + const response = await fetch(fullUrl, { + method: req.method, + headers: headers, + body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined + }); + + console.log(`📊 JIRA Response: ${response.status} ${response.statusText}`); + + const data = await response.text(); + + // Forward the response status and data + res.status(response.status); + + // Try to parse as JSON, fallback to text + try { + const jsonData = JSON.parse(data); + res.json(jsonData); + } catch (e) { + res.send(data); + } + + } catch (error) { + console.error('Proxy error:', error); + res.status(500).json({ + error: 'Proxy server error', + message: error.message + }); + } +}); + +// Health check endpoint +app.get('/api/health', (req, res) => { + res.json({ status: 'OK', message: 'JIRA Proxy Server is running' }); +}); + +// Start the server +app.listen(PORT, () => { + console.log(`🚀 JIRA Proxy Server running on http://localhost:${PORT}`); + console.log(`📝 JIRA UI available at: http://localhost:${PORT}`); + console.log(`🔧 Proxy endpoint: http://localhost:${PORT}/api/jira/*`); +}); \ 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..4a8ce7a --- /dev/null +++ b/tools/jira-ui/script.js @@ -0,0 +1,803 @@ +// 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 +}; + +// Initialize the application +document.addEventListener('DOMContentLoaded', function() { + initializeElements(); + bindEventListeners(); + checkExistingSession(); +}); + +// Initialize DOM element references +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(); +} + +// 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(); + } + }); +} + +// 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 +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'); + } + } +} + +// Show/hide conditional fields based on issue type +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'); + } +} + +// Handle label selection +function handleLabelClick(event) { + if (event.target.classList.contains('label-option')) { + const label = event.target.dataset.label; + 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(); + } +} + +// Update the display of selected labels +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'; + } + } +} + +// 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}`); + } +} + +// 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'); + const issueType = formData.get('issue-type'); + + // 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); +} + +// 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 +function clearIssueForm() { + Elements.issueForm.reset(); + clearLabelsSelection(); + hideMessages(); + + // Hide all conditional fields + showConditionalFields(''); +} + +// Clear labels selection +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(); +} + +// UI Helper Functions +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 showLoading() { + Elements.loadingOverlay.classList.remove('hidden'); +} + +function hideLoading() { + Elements.loadingOverlay.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 hideMessages() { + Elements.successMessage.classList.add('hidden'); + Elements.errorMessage.classList.add('hidden'); +} + +// Update demo indicator visibility +function updateDemoIndicator() { + if (AppState.isDemoMode) { + Elements.demoIndicator.classList.remove('hidden'); + } else { + Elements.demoIndicator.classList.add('hidden'); + } +} + +// 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 }; +} + +// 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 } + ] + }; + } +} + +// Parse JIRA issue data into our display format +function parseJiraIssueData(jiraIssue) { + const fields = jiraIssue.fields; + const userSetFields = ['summary', 'description', 'issuetype']; // Fields that user directly set + + 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 }; +} + +// 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 +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); + }); +} + +// 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 +} + +// Utility function to get proxy API endpoint URL +function getProxyApiUrl(endpoint) { + if (AppState.useProxy) { + return `${AppState.proxyUrl}/${endpoint}`; + } else { + return getJiraApiUrl(AppState.jiraUrl, endpoint); + } +} + +// Utility function to create fetch headers +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; +} \ No newline at end of file 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 From 7eb4b15297489c8cd5ef23766201ac3ca96e0b7d Mon Sep 17 00:00:00 2001 From: Tuan Phan Date: Thu, 19 Jun 2025 23:35:25 -0700 Subject: [PATCH 2/3] Remove backend files --- tools/jira-ui/README.md | 195 ------- tools/jira-ui/package-lock.json | 899 -------------------------------- tools/jira-ui/package.json | 18 - tools/jira-ui/proxy-server.js | 92 ---- 4 files changed, 1204 deletions(-) delete mode 100644 tools/jira-ui/README.md delete mode 100644 tools/jira-ui/package-lock.json delete mode 100644 tools/jira-ui/package.json delete mode 100644 tools/jira-ui/proxy-server.js diff --git a/tools/jira-ui/README.md b/tools/jira-ui/README.md deleted file mode 100644 index c790885..0000000 --- a/tools/jira-ui/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# JIRA Issue Creator - -A simplified, minimal web interface for creating issues in an on-premise JIRA instance. Built with vanilla HTML, CSS, and JavaScript according to enterprise requirements. -![image](https://git.corp.adobe.com/Blackbird/jira-ui/assets/371/7a579bb2-53a8-491f-8639-5d73d135a9b9) - -## Features - -- **Secure Authentication**: Login with corporate JIRA credentials -- **Project-Focused**: Automatic selection of predetermined default project -- **Issue Type Support**: Create "Experiment" or "Co-Innovation" issues -- **Conditional Fields**: Dynamic field display based on issue type -- **Auto-Assignment**: Issues are automatically assigned to the logged-in user -- **Session Persistence**: Stay logged in across browser sessions -- **Modern UI**: Clean, responsive design with professional styling - -## File Structure - -``` -jira-ui/ -├── index.html # Main application interface -├── styles.css # Styling and responsive design -├── script.js # Core functionality and JIRA API integration -├── config.js # Configuration constants and settings -└── README.md # This file -``` - -## Setup and Configuration - -### 1. JIRA Instance Configuration - -Before using this application, you need to configure it for your specific JIRA instance: - -#### Custom Field IDs -In `config.js`, update the `CUSTOM_FIELDS` section with your actual JIRA custom field IDs: - -```javascript -CUSTOM_FIELDS: { - CUSTOMER_NAMES: 'customfield_10001', // Replace with actual field ID - SLACK_CHANNELS: 'customfield_10002', // Replace with actual field ID - GIT_REPO_URL: 'customfield_10003' // Replace with actual field ID -} -``` - -To find your custom field IDs: -1. Go to JIRA Administration → Issues → Custom Fields -2. Find the relevant fields and note their IDs -3. Or use the JIRA API: `GET /rest/api/2/field` to list all fields - -#### Issue Types -Ensure your JIRA project has issue types named: -- "Experiment" -- "Co-Innovation" - -If your issue types have different names, update them in `config.js`: - -```javascript -ISSUE_TYPES: { - EXPERIMENT: 'your-experiment-type-name', - CO_INNOVATION: 'your-co-innovation-type-name' -} -``` - -### 2. Web Server Deployment - -This application needs to be served from a web server due to CORS restrictions when making API calls to JIRA. - -#### Option A: Simple HTTP Server (Development) -```bash -# Using Python 3 -python -m http.server 8000 - -# Using Node.js (if you have http-server installed) -npx http-server - -# Using PHP -php -S localhost:8000 -``` - -#### Option B: Production Web Server -Deploy the files to your preferred web server (Apache, Nginx, IIS, etc.) - -### 3. JIRA API Token (Recommended) - -For enhanced security, use API tokens instead of passwords: - -1. Go to https://id.atlassian.com/manage-profile/security/api-tokens -2. Create a new API token -3. Use your email address as the username and the API token as the password - -## Usage - -### Login Process - -1. **JIRA Instance URL**: Enter your JIRA base URL (e.g., `https://company.atlassian.net`) -2. **Username**: Your JIRA username or email address -3. **Password/API Token**: Your password or API token -4. **Project Key**: The key of your default project (e.g., "PROJ") - -### Creating Issues - -1. **Select Issue Type**: Choose between "Experiment" or "Co-Innovation" -2. **Fill Required Fields**: Title and Description are always required -3. **Conditional Fields**: - - **Co-Innovation**: Customer Names (required) + optional Slack Channel URLs - - **Experiment**: Optional Git Repository URL -4. **Submit**: Click "Create Issue" to submit to JIRA - -### Dynamic Fields - -- **Slack Channel URLs**: Use "Add Channel URL" button to add multiple Slack channels -- **Field Validation**: All fields are validated before submission -- **Auto-Clear**: Form clears automatically after successful issue creation - -## Technical Details - -### Browser Compatibility -- Modern browsers with ES6+ support -- Chrome 60+, Firefox 55+, Safari 12+, Edge 79+ - -### Security Considerations -- Credentials are stored in browser localStorage for session persistence -- Basic Authentication is used for JIRA API calls -- No credentials are transmitted to external servers -- HTTPS is recommended for production deployment - -### API Integration -- Uses JIRA REST API v2 -- Authenticates via Basic Auth -- Validates user permissions and project access -- Creates issues with proper field mapping - -## Troubleshooting - -### Common Issues - -1. **CORS Errors**: Ensure the application is served from a web server, not opened as a file -2. **Authentication Failed**: Check username/password and JIRA URL -3. **Project Not Found**: Verify the project key and user permissions -4. **Custom Fields Not Saving**: Update custom field IDs in `config.js` - -### Error Messages -The application provides detailed error messages for: -- Authentication failures -- Network connectivity issues -- Validation errors -- JIRA API errors - -### Browser Console -Check the browser console for detailed error information during development. - -## Customization - -### Adding New Fields -1. Add the field to the HTML form in `index.html` -2. Update validation logic in `script.js` -3. Add field mapping in the `buildIssueData()` function -4. Update custom field IDs in `config.js` - -### Styling Changes -Modify `styles.css` to customize: -- Color scheme -- Layout and spacing -- Responsive breakpoints -- Animation effects - -### Configuration Options -Update `config.js` to modify: -- Validation rules -- UI behavior -- API settings -- Custom field mappings - -## Requirements Compliance - -This application fulfills all specified functional and non-functional requirements: - -✅ **Functional Requirements** -- FR-1: User authentication with corporate JIRA credentials -- FR-2: Automatic default project selection -- FR-3: Issue type selection (Experiment/Co-Innovation) -- FR-4: Mandatory Title and Description fields -- FR-5: Conditional fields based on issue type -- FR-6: Dynamic Slack URL field addition -- FR-7: Automatic issue assignment to logged-in user -- FR-8: No file attachment functionality -- FR-9: Success message with issue key and form clearing - -✅ **Non-Functional Requirements** -- NFR-1: Vanilla HTML, CSS, JavaScript only (no frameworks) -- Clean, fast, and minimal interface -- JIRA API integration - -## License - -This application is developed for internal enterprise use according to the specified requirements. diff --git a/tools/jira-ui/package-lock.json b/tools/jira-ui/package-lock.json deleted file mode 100644 index f97d284..0000000 --- a/tools/jira-ui/package-lock.json +++ /dev/null @@ -1,899 +0,0 @@ -{ - "name": "jira-ui-proxy", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "jira-ui-proxy", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "cors": "^2.8.5", - "express": "^4.18.2", - "node-fetch": "^2.6.7" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/tools/jira-ui/package.json b/tools/jira-ui/package.json deleted file mode 100644 index 89998b0..0000000 --- a/tools/jira-ui/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "jira-ui-proxy", - "version": "1.0.0", - "description": "Proxy server for JIRA UI to handle CORS", - "main": "proxy-server.js", - "scripts": { - "start": "node proxy-server.js", - "dev": "node proxy-server.js" - }, - "dependencies": { - "express": "^4.18.2", - "cors": "^2.8.5", - "node-fetch": "^2.6.7" - }, - "keywords": ["jira", "proxy", "cors"], - "author": "", - "license": "MIT" -} \ No newline at end of file diff --git a/tools/jira-ui/proxy-server.js b/tools/jira-ui/proxy-server.js deleted file mode 100644 index 9a6a3dd..0000000 --- a/tools/jira-ui/proxy-server.js +++ /dev/null @@ -1,92 +0,0 @@ -const express = require('express'); -const cors = require('cors'); -const fetch = require('node-fetch'); -const path = require('path'); - -const app = express(); -const PORT = 3001; - -// Enable CORS for all routes -app.use(cors()); - -// Parse JSON bodies -app.use(express.json()); - -// Serve static files from the current directory -app.use(express.static('.')); - -// JIRA API Proxy endpoint -app.all('/api/jira/*', async (req, res) => { - try { - // Extract the JIRA URL and endpoint from the request - const jiraUrl = req.headers['x-jira-url']; - const jiraEndpoint = req.params[0]; // Everything after /api/jira/ - - console.log(`🔍 Proxy request: ${req.method} ${jiraEndpoint}`); - console.log(`🌐 JIRA URL: ${jiraUrl}`); - console.log(`🔑 Has Authorization: ${!!req.headers.authorization}`); - - if (!jiraUrl) { - console.log('❌ Missing X-JIRA-URL header'); - return res.status(400).json({ error: 'Missing X-JIRA-URL header' }); - } - - // Construct the full JIRA API URL - const fullUrl = `${jiraUrl}/rest/api/2/${jiraEndpoint}`; - console.log(`📍 Full URL: ${fullUrl}`); - - // Forward the authorization header - const headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }; - - if (req.headers.authorization) { - headers.Authorization = req.headers.authorization; - console.log(`🔐 Authorization header: ${req.headers.authorization.substring(0, 20)}...`); - } else { - console.log('⚠️ No Authorization header found'); - } - - // Make the request to JIRA - const response = await fetch(fullUrl, { - method: req.method, - headers: headers, - body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined - }); - - console.log(`📊 JIRA Response: ${response.status} ${response.statusText}`); - - const data = await response.text(); - - // Forward the response status and data - res.status(response.status); - - // Try to parse as JSON, fallback to text - try { - const jsonData = JSON.parse(data); - res.json(jsonData); - } catch (e) { - res.send(data); - } - - } catch (error) { - console.error('Proxy error:', error); - res.status(500).json({ - error: 'Proxy server error', - message: error.message - }); - } -}); - -// Health check endpoint -app.get('/api/health', (req, res) => { - res.json({ status: 'OK', message: 'JIRA Proxy Server is running' }); -}); - -// Start the server -app.listen(PORT, () => { - console.log(`🚀 JIRA Proxy Server running on http://localhost:${PORT}`); - console.log(`📝 JIRA UI available at: http://localhost:${PORT}`); - console.log(`🔧 Proxy endpoint: http://localhost:${PORT}/api/jira/*`); -}); \ No newline at end of file From e2dc87ca4154bc7087737a01c61390b193fedca5 Mon Sep 17 00:00:00 2001 From: Tuan Phan Date: Fri, 20 Jun 2025 10:00:19 -0700 Subject: [PATCH 3/3] Fix lint errors --- tools/jira-ui/config.js | 72 +- tools/jira-ui/script.js | 1416 ++++++++++++++++++++------------------- 2 files changed, 751 insertions(+), 737 deletions(-) diff --git a/tools/jira-ui/config.js b/tools/jira-ui/config.js index fcfd611..b0576aa 100644 --- a/tools/jira-ui/config.js +++ b/tools/jira-ui/config.js @@ -1,47 +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' - } + // 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', }, - - // UI Constants - UI: { - SUCCESS_MESSAGE_TIMEOUT: 5000, // 5 seconds - MAX_SLACK_URLS: 10 + + // Issue type display names + ISSUE_TYPE_NAMES: { + 'Co-Innovation': 'Co-Innovation', + Experiment: 'Experiment', }, - - // Validation - VALIDATION: { - MIN_TITLE_LENGTH: 3, - MIN_DESCRIPTION_LENGTH: 10, - MAX_TITLE_LENGTH: 255, - MAX_DESCRIPTION_LENGTH: 32767 - } + }, + + // 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}`; + 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 }; -} \ No newline at end of file + module.exports = { CONFIG, getJiraApiUrl }; +} + +export default CONFIG; diff --git a/tools/jira-ui/script.js b/tools/jira-ui/script.js index 4a8ce7a..b78a504 100644 --- a/tools/jira-ui/script.js +++ b/tools/jira-ui/script.js @@ -1,803 +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' + 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 + 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, }; -// Initialize the application -document.addEventListener('DOMContentLoaded', function() { - initializeElements(); - bindEventListeners(); - checkExistingSession(); -}); +// === Move helpers / used-by-others FIRST === -// Initialize DOM element references -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(); +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'; + } + } } -// 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); +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; } - - // 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(); - } + + 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'); - } + 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.'); + // 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; + 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.'); } - - 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; + + 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.`); } -} -// Handle logout -function handleLogout() { + // 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; - AppState.isDemoMode = false; - - localStorage.removeItem('jira-credentials'); - - // Reset forms - Elements.loginForm.reset(); - Elements.issueForm.reset(); - - showLoginSection(); - updateDemoIndicator(); - hideMessages(); + } +} + +// 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); + const selectedType = event.target.value; + + // Show/hide conditional fields based on issue type + showConditionalFields(selectedType); + updateRequiredFields(selectedType); } // Update required fields based on issue type -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'); - } - } -} +// (moved above) // Show/hide conditional fields based on issue type -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'); - } -} +// (moved above) // Handle label selection function handleLabelClick(event) { - if (event.target.classList.contains('label-option')) { - const label = event.target.dataset.label; - 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(); - } -} - -// Update the display of selected labels -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'; - } - } -} - -// 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}`); + 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'); } -} - -// 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'); - const issueType = formData.get('issue-type'); - - // 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); + 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.` }; + 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.' }; } - - 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 }; + } + + 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()}`; - } + 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()}`; } - - // 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; - } + } 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()}`; } - - // 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 }; + if (successCriteria && successCriteria.trim()) { + description += `\n\n*Success Criteria:* ${successCriteria.trim()}`; } - - // Add labels if any are selected - if (AppState.selectedLabels.length > 0) { - issueData.fields.labels = AppState.selectedLabels; + } + + // 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; } - - return issueData; -} + } -// Clear issue form for next creation -function clearIssueForm() { - Elements.issueForm.reset(); - clearLabelsSelection(); - hideMessages(); - - // Hide all conditional fields - showConditionalFields(''); -} + // Base issue data structure + const issueData = { + fields: { + project: { key: AppState.projectKey }, + summary: title.trim(), + description: description.trim(), + issuetype: { name: issueTypeName }, + }, + }; -// Clear labels selection -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(); -} - -// UI Helper Functions -function showLoginSection() { - Elements.loginSection.classList.remove('hidden'); - Elements.issueSection.classList.add('hidden'); -} - -function showIssueSection() { - Elements.loginSection.classList.add('hidden'); - Elements.issueSection.classList.remove('hidden'); -} + // Only add assignee if we have a valid user + if (assigneeName) { + issueData.fields.assignee = { name: assigneeName }; + } -function showLoading() { - Elements.loadingOverlay.classList.remove('hidden'); -} + // Add labels if any are selected + if (AppState.selectedLabels.length > 0) { + issueData.fields.labels = AppState.selectedLabels; + } -function hideLoading() { - Elements.loadingOverlay.classList.add('hidden'); + return issueData; } -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); -} +// Clear issue form for next creation +// (moved above) -function showError(message) { - hideMessages(); - Elements.errorMessage.textContent = message; - Elements.errorMessage.classList.remove('hidden'); -} +// Clear labels selection +// (moved above) -function hideMessages() { - Elements.successMessage.classList.add('hidden'); - Elements.errorMessage.classList.add('hidden'); -} +// UI Helper Functions +// (all moved above) // Update demo indicator visibility -function updateDemoIndicator() { - if (AppState.isDemoMode) { - Elements.demoIndicator.classList.remove('hidden'); - } else { - Elements.demoIndicator.classList.add('hidden'); - } -} +// (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 }; + 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 }; } -// Fetch real issue details from JIRA -async function fetchIssueDetails(issueKey) { - try { - const response = await fetch(getProxyApiUrl(`issue/${issueKey}`), { - method: 'GET', - headers: createFetchHeaders() +// 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, }); - - 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 } - ] - }; + } } + }); + + return { fields: displayFields }; } -// Parse JIRA issue data into our display format -function parseJiraIssueData(jiraIssue) { - const fields = jiraIssue.fields; - const userSetFields = ['summary', 'description', 'issuetype']; // Fields that user directly set - - 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 - }); - } - } +// Fetch real issue details from JIRA +async function fetchIssueDetails(issueKey) { + try { + const response = await fetch(getProxyApiUrl(`issue/${issueKey}`), { + method: 'GET', + headers: createFetchHeaders(), }); - - return { fields: displayFields }; + + 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(); + // 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 -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); - }); -} +// (moved above) // Close issue details modal function closeIssueModal() { - Elements.issueDetailsModal.classList.add('hidden'); + Elements.issueDetailsModal.classList.add('hidden'); } // Create another issue (close modal and clear form) function createAnotherIssue() { - closeIssueModal(); - // Form is already cleared by showIssueDetailsModal + closeIssueModal(); + // Form is already cleared by showIssueDetailsModal } -// Utility function to get proxy API endpoint URL -function getProxyApiUrl(endpoint) { - if (AppState.useProxy) { - return `${AppState.proxyUrl}/${endpoint}`; +// 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 { - return getJiraApiUrl(AppState.jiraUrl, endpoint); + // Real JIRA API call + await handleRealIssueCreation(formData); } + } catch (error) { + hideLoading(); + showError(`Failed to create issue: ${error.message}`); + } } -// Utility function to create fetch headers -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}`; - } +// 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(); } - - return headers; -} \ No newline at end of file + }); +} + +// Initialize the application +document.addEventListener('DOMContentLoaded', () => { + initializeElements(); + bindEventListeners(); + checkExistingSession(); +});