From a7435304b24b3dfa839f07cf74ffa74090960b4d Mon Sep 17 00:00:00 2001 From: ay-bh Date: Tue, 16 Jul 2024 11:02:45 -0400 Subject: [PATCH 01/27] add new issue tracker view --- modules/issue_tracker/css/issue_card.css | 123 ++++++++++++++ modules/issue_tracker/jsx/IssueCard.js | 151 ++++++++++++++++++ .../jsx/IssueTrackerDetailView.js | 126 +++++++++++++++ .../issue_tracker/jsx/issueTrackerIndex.js | 48 ++++-- 4 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 modules/issue_tracker/css/issue_card.css create mode 100644 modules/issue_tracker/jsx/IssueCard.js create mode 100644 modules/issue_tracker/jsx/IssueTrackerDetailView.js diff --git a/modules/issue_tracker/css/issue_card.css b/modules/issue_tracker/css/issue_card.css new file mode 100644 index 00000000000..e3833faf8c9 --- /dev/null +++ b/modules/issue_tracker/css/issue_card.css @@ -0,0 +1,123 @@ +.issue-tracker-detail-view { + display: flex; + flex-direction: column; + max-width: 800px; + 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 #007bff; + color: #007bff; +} + +.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; +} + +.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); +} + +.issue-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.issue-header h3 { + margin: 0; +} + +.issue-header a { + color: #007bff; + text-decoration: none; +} + +.issue-title { + margin-bottom: 15px; +} + +.issue-title h4 { + margin: 0; + font-size: 1.2em; +} + +.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-card button { + padding: 8px 15px; + border: none; + border-radius: 4px; + background-color: #007bff; + color: white; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.issue-card button:hover { + background-color: #0056b3; +} + +.issue-card input[type="text"], +.issue-card textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js new file mode 100644 index 00000000000..d9694e00c02 --- /dev/null +++ b/modules/issue_tracker/jsx/IssueCard.js @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import swal from 'sweetalert2'; + +const IssueCard = React.memo(function IssueCard({ + issue, + onUpdate, + statuses, + priorities, + categories, +}) { + const [isEditing, setIsEditing] = useState(false); + const [editedIssue, setEditedIssue] = useState({ + title: issue[1], + status: issue[6], + priority: issue[7], + category: issue[3] || '' + }); + + const handleInputChange = (field, value) => { + setEditedIssue(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!editedIssue.title.trim()) { + swal.fire({ + title: 'Error!', + text: 'Title cannot be empty', + icon: 'error', + confirmButtonText: 'OK' + }); + return; + } + + const formData = new FormData(); + + formData.append('issueID', issue[0]); + formData.append('title', editedIssue.title); + formData.append('status', editedIssue.status); + formData.append('priority', editedIssue.priority); + // formData.append('category', editedIssue.category); + + fetch(`${loris.BaseURL}/issue_tracker/Edit/?issueID=${issue[0]}`, { + 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) => { + swal.fire({ + title: 'Success!', + text: 'Issue updated successfully', + icon: 'success', + confirmButtonText: 'OK' + }); + onUpdate(issue[0], editedIssue); + setIsEditing(false); + }).catch((error) => { + console.error('Error:', error); + swal.fire({ + title: 'Error!', + text: error.message || 'Failed to update issue', + icon: 'error', + confirmButtonText: 'OK' + }); + }); + }; + + return ( +
+
+

+ Issue ID: {issue[0]} +

+
+
+
+ {isEditing ? ( + handleInputChange('title', e.target.value)} + /> + ) : ( +

{issue[1]}

+ )} +
+
+ + + +
+
+

{issue[2]}

+
+ {isEditing ? ( + <> + + + + ) : ( + + )} +
+
+ ); +}); + +IssueCard.propTypes = { + issue: PropTypes.array.isRequired, + onUpdate: PropTypes.func.isRequired, + statuses: PropTypes.object.isRequired, + priorities: PropTypes.object.isRequired, + categories: PropTypes.object.isRequired, +}; + +export default IssueCard; \ No newline at end of file diff --git a/modules/issue_tracker/jsx/IssueTrackerDetailView.js b/modules/issue_tracker/jsx/IssueTrackerDetailView.js new file mode 100644 index 00000000000..0f508df6319 --- /dev/null +++ b/modules/issue_tracker/jsx/IssueTrackerDetailView.js @@ -0,0 +1,126 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import IssueCard from './IssueCard'; +import '../css/issue_card.css'; + +function IssueTrackerDetailView({ issues, options, baseURL }) { + const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedPriorities, setSelectedPriorities] = useState([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [filteredIssues, setFilteredIssues] = useState([]); + const [activeTab, setActiveTab] = useState('category'); + + const priorities = options.priorities || {}; + const statuses = options.statuses || {}; + const categories = options.categories || {}; // Use categories from options + + useEffect(() => { + filterIssues(); + }, [selectedCategories, selectedPriorities, selectedStatuses, issues]); + + function filterIssues() { + setFilteredIssues(issues.filter(issue => + (selectedCategories.length === 0 || selectedCategories.includes(issue[3])) && // Update index to 3 for category + (selectedPriorities.length === 0 || selectedPriorities.includes(issue[7])) && + (selectedStatuses.length === 0 || selectedStatuses.includes(issue[6])) + )); + } + + function toggleFilter(array, setArray, item) { + setArray(prev => + prev.includes(item) + ? prev.filter(i => i !== item) + : [...prev, item] + ); + } + + function handleIssueUpdate(issueId, updatedIssue) { + const updatedIssues = issues.map(issue => { + if (issue[0] === issueId) { + return [ + issue[0], + updatedIssue.title, + issue[2], // module remains unchanged + updatedIssue.category, + issue[4], // reporter remains unchanged + issue[5], // assignee remains unchanged + updatedIssue.status, + updatedIssue.priority, + ...issue.slice(8) + ]; + } + return issue; + }); + + setFilteredIssues(updatedIssues); + } + + + return ( +
+
+ + + +
+
+

Selected {activeTab}

+
+ {activeTab === 'category' && Object.entries(categories).map(([value, label]) => ( + + ))} + {activeTab === 'priority' && Object.entries(priorities).map(([value, label]) => ( + + ))} + {activeTab === 'status' && Object.entries(statuses).map(([value, label]) => ( + + ))} +
+
+
+ {filteredIssues.map(issue => ( + + ))} +
+
+ ); +} + +IssueTrackerDetailView.propTypes = { + issues: PropTypes.arrayOf(PropTypes.array).isRequired, + options: PropTypes.shape({ + priorities: PropTypes.object, + statuses: PropTypes.object, + categories: PropTypes.object, + }).isRequired, +}; + +export default IssueTrackerDetailView; \ No newline at end of file diff --git a/modules/issue_tracker/jsx/issueTrackerIndex.js b/modules/issue_tracker/jsx/issueTrackerIndex.js index 80e8dd88ae5..4dc2b6f447b 100644 --- a/modules/issue_tracker/jsx/issueTrackerIndex.js +++ b/modules/issue_tracker/jsx/issueTrackerIndex.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Loader from 'Loader'; import FilterableDataTable from 'FilterableDataTable'; +import IssueTrackerDetailView from './IssueTrackerDetailView'; /** * Issue Tracker Index component @@ -20,10 +21,12 @@ class IssueTrackerIndex extends Component { data: {}, error: false, isLoaded: false, + view: 'table', // 'table' for FilterableDataTable, 'detail' for IssueTrackerDetailView }; this.fetchData = this.fetchData.bind(this); this.formatColumn = this.formatColumn.bind(this); + this.toggleView = this.toggleView.bind(this); } /** @@ -51,6 +54,15 @@ class IssueTrackerIndex extends Component { }); } + /** + * Toggle between table and detail view + */ + toggleView() { + this.setState(prevState => ({ + view: prevState.view === 'table' ? 'detail' : 'table', + })); +} + /** * Modify behaviour of specified column cells in the Data Table component * @@ -261,16 +273,34 @@ class IssueTrackerIndex extends Component { const actions = [ {label: 'New Issue', action: addIssue}, ]; - + console.log(this.state.data.data) return ( - +
+
+ +
+ {this.state.view === 'table' ? ( + + ) : ( + + )} +
); } } From e5105263bf06657ffe0d7633e4a649070d840b0a Mon Sep 17 00:00:00 2001 From: ay-bh Date: Tue, 27 Aug 2024 12:48:29 -0400 Subject: [PATCH 02/27] Add debug mode endpoint --- modules/issue_tracker/php/edit.class.inc | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/modules/issue_tracker/php/edit.class.inc b/modules/issue_tracker/php/edit.class.inc index 559a3564153..9b29ea4af84 100644 --- a/modules/issue_tracker/php/edit.class.inc +++ b/modules/issue_tracker/php/edit.class.inc @@ -58,6 +58,15 @@ class Edit extends \NDB_Page implements ETagCalculator $user = $request->getAttribute('user'); $db = $this->loris->getDatabaseConnection(); + // Check if debug mode is enabled + $debug = filter_input(INPUT_GET, 'debug', FILTER_VALIDATE_BOOLEAN); + + if ($debug) { + // Fetch all issues + $allIssues = $this->getAllIssues($user); + return new \LORIS\Http\Response\JsonResponse($allIssues); + } + // get field options $sites = Issue_Tracker::getSites(false, true); @@ -275,6 +284,39 @@ class Edit extends \NDB_Page implements ETagCalculator ); } + /** + * Fetches all issues from the database + * + * @param \User $user The current user + * + * @return array + */ + private function getAllIssues(\User $user): array + { + $db = $this->loris->getDatabaseConnection(); + + $query = "SELECT i.*, c.PSCID, s.Visit_label AS visitLabel + FROM issues AS i + LEFT JOIN candidate c ON (i.candID = c.CandID) + LEFT JOIN session s ON (i.sessionID = s.ID)"; + + // Add permission check if needed + if (!$user->hasPermission('access_all_profiles')) { + $query .= " WHERE i.centerID IN (" . implode(',', $user->getCenterIDs()) . ")"; + } + + $query .= " ORDER BY i.issueID DESC"; + + $issues = $db->pselect($query, []); + + // Format the issues data + foreach ($issues as &$issue) { + $issue['reporter'] = $this->formatUserInformation($issue['reporter']); + $issue['lastUpdatedBy'] = $this->formatUserInformation($issue['lastUpdatedBy']); + } + + return $issues; + } /** * If issueID is passed retrieves issue data from database, * otherwise return empty issue data object From c4c379d7f72cb10ec3f1b7b97d6ef67790fb0771 Mon Sep 17 00:00:00 2001 From: ay-bh Date: Tue, 27 Aug 2024 12:55:48 -0400 Subject: [PATCH 03/27] Integrate the debug endpoint --- modules/issue_tracker/jsx/IssueCard.js | 103 ++++++++++++------ .../jsx/IssueTrackerDetailView.js | 63 +++++++---- 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js index d9694e00c02..7a532463718 100644 --- a/modules/issue_tracker/jsx/IssueCard.js +++ b/modules/issue_tracker/jsx/IssueCard.js @@ -8,14 +8,10 @@ const IssueCard = React.memo(function IssueCard({ statuses, priorities, categories, + baseURL, }) { const [isEditing, setIsEditing] = useState(false); - const [editedIssue, setEditedIssue] = useState({ - title: issue[1], - status: issue[6], - priority: issue[7], - category: issue[3] || '' - }); + const [editedIssue, setEditedIssue] = useState({...issue}); const handleInputChange = (field, value) => { setEditedIssue(prev => ({ @@ -28,24 +24,28 @@ const IssueCard = React.memo(function IssueCard({ e.preventDefault(); if (!editedIssue.title.trim()) { - swal.fire({ - title: 'Error!', - text: 'Title cannot be empty', - icon: 'error', - confirmButtonText: 'OK' - }); + showAlertMessage('error', 'Title cannot be empty'); return; } const formData = new FormData(); - formData.append('issueID', issue[0]); - formData.append('title', editedIssue.title); - formData.append('status', editedIssue.status); - formData.append('priority', editedIssue.priority); - // formData.append('category', editedIssue.category); + // Append all fields to the form data + Object.entries(editedIssue).forEach(([key, value]) => { + formData.append(key, value === null ? "null" : value); + }); + + // Check if there are any changes + const hasChanges = Object.entries(editedIssue).some(([key, value]) => + value !== issue[key] + ); - fetch(`${loris.BaseURL}/issue_tracker/Edit/?issueID=${issue[0]}`, { + if (!hasChanges) { + showAlertMessage('info', 'No changes were made'); + return; + } + + fetch(`${loris.BaseURL}/issue_tracker/Edit/`, { method: 'POST', body: formData, }).then((response) => { @@ -56,22 +56,21 @@ const IssueCard = React.memo(function IssueCard({ } return response.json(); }).then((data) => { - swal.fire({ - title: 'Success!', - text: 'Issue updated successfully', - icon: 'success', - confirmButtonText: 'OK' - }); - onUpdate(issue[0], editedIssue); + showAlertMessage('success', 'Issue updated successfully'); + onUpdate(issue.issueID, editedIssue); setIsEditing(false); }).catch((error) => { console.error('Error:', error); - swal.fire({ - title: 'Error!', - text: error.message || 'Failed to update issue', - icon: 'error', - confirmButtonText: 'OK' - }); + showAlertMessage('error', error.message || 'Failed to update issue'); + }); + }; + + const showAlertMessage = (type, message) => { + swal.fire({ + title: type === 'success' ? 'Success!' : 'Error!', + text: message, + icon: type, + confirmButtonText: 'OK' }); }; @@ -79,7 +78,7 @@ const IssueCard = React.memo(function IssueCard({
@@ -91,7 +90,7 @@ const IssueCard = React.memo(function IssueCard({ onChange={(e) => handleInputChange('title', e.target.value)} /> ) : ( -

{issue[1]}

+

{issue.title}

)}
@@ -125,8 +124,22 @@ const IssueCard = React.memo(function IssueCard({
-

{issue[2]}

+ {isEditing ? ( +