diff --git a/modules/issue_tracker/css/issue_card.css b/modules/issue_tracker/css/issue_card.css new file mode 100644 index 00000000000..e059ff246e0 --- /dev/null +++ b/modules/issue_tracker/css/issue_card.css @@ -0,0 +1,142 @@ +.issue-card { + border: 1px solid #ccc; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + background-color: #fff; +} + +.issue-header { + display: flex; + justify-content: space-between; + margin-bottom: 16px; +} + +.issue-title-section { + flex: 1; +} + +.issue-dates { + flex: 1; + text-align: right; + font-size: 0.9em; + color: #555; +} + +.issue-form { + width: 100%; +} + +.issue-controls { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.issue-content { + display: flex; + gap: 16px; + flex: 1; +} + +.description-section { + flex: 1; + display: flex; + flex-direction: column; +} + +.comments-section { + flex: 1; + border-left: 1px solid #eee; + padding-left: 16px; + display: flex; + flex-direction: column; +} + +.description-container { + flex: 1; + max-height: 200px; + overflow-y: auto; + margin-top: 8px; + padding-right: 8px; + overflow-y: auto; + overflow-x: hidden; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.comments-container { + flex: 1; + max-height: 200px; + overflow-y: auto; + margin-top: 8px; + padding-right: 8px; +} + +.description-text { + margin: 8px 0; + white-space: pre-wrap; +} + +.comment { + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.comment-text { + margin: 4px 0; +} + +.comment-meta { + font-size: 0.8em; + color: #777; +} + +.no-comments { + font-style: italic; + color: #777; +} + +.issue-actions { + display: flex; + gap: 8px; +} + +.title-input { + width: 100%; + padding: 4px; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 4px; +} + +.textarea { + width: 100%; + height: 200px; + resize: none; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +@media (max-width: 600px) { + .issue-content { + flex-direction: column; + } + + .comments-section { + border-left: none; + padding-left: 0; + margin-top: 16px; + } + + .description-container, + .comments-container { + max-height: 150px; + } +} diff --git a/modules/issue_tracker/css/issue_tracker_batchmode.css b/modules/issue_tracker/css/issue_tracker_batchmode.css new file mode 100644 index 00000000000..727795e7b2c --- /dev/null +++ b/modules/issue_tracker/css/issue_tracker_batchmode.css @@ -0,0 +1,205 @@ +.issue-tracker-batch-mode { + display: flex; + flex-direction: column; + max-width: 1000px; + margin: 0 auto; +} + +.filter-tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 1px solid #ddd; +} + +.filter-tabs button { + padding: 10px 20px; + border: none; + background-color: transparent; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.3s ease; +} + +.filter-tabs button.active { + border-bottom: 2px solid #074785; + color: #074785; +} + +.filter-section { + margin-bottom: 20px; +} + +.filter-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.filter-list label { + display: flex; + align-items: center; + margin-bottom: 0; + font-weight: normal; +} + +.filter-list label span { + margin-left: 3px; +} + +.issues-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.issue-card { + border: 1px solid #ddd; + padding: 20px; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; +} + +.issue-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.issue-title-section { + display: flex; + align-items: center; +} + +.issue-header h3 { + margin: 0; + display: flex; + align-items: baseline; + font-size: 1.5em; +} + +.issue-header a { + color: #074785; + text-decoration: none; + display: flex; + align-items: baseline; +} + +.issue-id { + font-size: 0.8em; + color: #666; + background-color: #f0f0f0; + padding: 2px 6px; + border-radius: 12px; + margin-right: 8px; +} + +.issue-dates { + display: flex; + flex-direction: column; + align-items: flex-end; + font-size: 0.8em; + color: #666; +} + +.issue-controls { + display: flex; + gap: 15px; + margin-bottom: 15px; +} + +.issue-controls select { + padding: 5px 10px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f8f8f8; +} + +.issue-description { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +.issue-metadata { + margin-top: 15px; + font-size: 0.9em; + color: #666; +} + +.issue-actions { + margin-top: 15px; + display: flex; + justify-content: flex-start; + gap: 10px; +} + +.issue-card a { + color: #074785; + text-decoration: none; +} + +.issue-card a:hover { + text-decoration: underline; +} + +.issue-card input[type="text"], .issue-card textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 10px; +} + +.title-input { + font-size: 1em; + width: calc(100% - 40px); + margin-left: 8px; +} + +.checkbox { + margin-right: 8px; +} + +.no-results-message { + text-align: center; + padding: 20px; + font-size: 1.2em; + color: #666; + background-color: #f8f8f8; + border: 1px solid #ddd; + border-radius: 8px; + margin-top: 20px; +} + +.pagination-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-top: 10px; + margin-bottom: 10px; +} + +.pagination-controls { + margin-left: auto; +} + +.perPage { + margin-left: 5px; + margin-right: 5px; +} + +.panel-title-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.filter-reset-button { + margin-left: auto; + margin-right: 10px; +} diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js new file mode 100644 index 00000000000..8804f5ba482 --- /dev/null +++ b/modules/issue_tracker/jsx/IssueCard.js @@ -0,0 +1,439 @@ +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import swal from 'sweetalert2'; +import Modal from 'jsx/Modal'; +import '../css/issue_card.css'; + +const IssueCard = React.memo(function IssueCard({ + issue, + onUpdate, + statuses, + priorities, + categories, + sites, +}) { + const [isEditing, setIsEditing] = useState(false); + const [editedIssue, setEditedIssue] = useState({...issue}); + const [tempEditedIssue, setTempEditedIssue] = useState({...issue}); + + // State variables for adding comments + const [showAddCommentModal, setShowAddCommentModal] = useState(false); + const [newComment, setNewComment] = useState(''); + const [isSubmittingComment, setIsSubmittingComment] = useState(false); + + const handleInputChange = (field, value) => { + setTempEditedIssue((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!tempEditedIssue.title.trim()) { + showAlertMessage('error', 'Title cannot be empty'); + return; + } + + const formData = new FormData(); + + Object.entries(tempEditedIssue).forEach(([key, value]) => { + formData.append(key, value === null ? 'null' : value); + }); + + const hasChanges = Object.entries(tempEditedIssue).some(([key, value]) => + value !== issue[key] + ); + + if (!hasChanges) { + showAlertMessage('info', 'No changes were made'); + return; + } + + fetch(`${loris.BaseURL}/issue_tracker/Edit/`, { + method: 'POST', + body: formData, + }).then((response) => { + if (!response.ok) { + return response.json().then((data) => { + throw new Error(data.error || 'Network response was not ok'); + }); + } + return response.json(); + }).then((data) => { + showAlertMessage('success', 'Issue updated successfully'); + setEditedIssue(tempEditedIssue); + onUpdate(); + setIsEditing(false); + }).catch((error) => { + console.error('Error:', error); + showAlertMessage('error', error.message || 'Failed to update issue'); + setTempEditedIssue({...editedIssue}); + }); + }; + + const showAlertMessage = (msgType, message) => { + let type = 'success'; + let title = 'Issue updated!'; + let text = message || ''; + let timer = null; + let confirmation = true; + + if (msgType === 'error') { + type = 'error'; + title = 'Error!'; + } else if (msgType === 'info') { + type = 'info'; + title = 'Information'; + } + + swal.fire({ + title: title, + type: type, + text: text, + timer: timer, + allowOutsideClick: false, + allowEscapeKey: false, + showConfirmButton: confirmation, + }); + }; + + const handleOpenAddCommentModal = () => { + setShowAddCommentModal(true); + }; + + const handleCloseAddCommentModal = () => { + setShowAddCommentModal(false); + setNewComment(''); + }; + + const handleAddCommentChange = (e) => { + setNewComment(e.target.value); + }; + + const handleAddCommentSubmit = (e) => { + e.preventDefault(); + + if (!newComment.trim()) { + showAlertMessage('error', 'Comment cannot be empty'); + return; + } + + setIsSubmittingComment(true); + + const formData = new FormData(); + + // Prefill all existing issue fields to prevent NULL updates + Object.entries(tempEditedIssue).forEach(([key, value]) => { + formData.append(key, value === null ? 'null' : value); + }); + + formData.append('comment', newComment.trim()); + + fetch(`${loris.BaseURL}/issue_tracker/Edit/`, { + method: 'POST', + body: formData, + }) + .then((response) => { + setIsSubmittingComment(false); + if (!response.ok) { + return response.json().then((data) => { + throw new Error(data.error || 'Network response was not ok'); + }); + } + return response.json(); + }) + .then((data) => { + showAlertMessage('success', 'Comment added successfully'); + handleCloseAddCommentModal(); + onUpdate(); + }) + .catch((error) => { + console.error('Error:', error); + showAlertMessage('error', error.message || 'Failed to add comment'); + }); + }; + + const description = editedIssue.description || ''; + + return ( +
+ +
+
+ +