From 2a224e67c9f303c7641eeea15916951b81761061 Mon Sep 17 00:00:00 2001 From: ay-bh Date: Thu, 24 Oct 2024 18:03:29 -0400 Subject: [PATCH 1/7] Add assignees and watchers data to batch mode endpoint --- modules/issue_tracker/php/edit.class.inc | 185 ++++++++++++++++++++--- 1 file changed, 161 insertions(+), 24 deletions(-) diff --git a/modules/issue_tracker/php/edit.class.inc b/modules/issue_tracker/php/edit.class.inc index bf958e48acb..2b1fd81af65 100644 --- a/modules/issue_tracker/php/edit.class.inc +++ b/modules/issue_tracker/php/edit.class.inc @@ -285,50 +285,187 @@ class Edit extends \NDB_Page implements ETagCalculator } /** - * Fetches all issues and includes top 3 comments per issue. + * Fetches all issues including related data and top comments. * * @param \User $user The current user. * - * @return array List of issues with top comments. + * @return array Contains issues, assignees, and other watchers information. */ private function _getAllIssues(\User $user): array { - $db = $this->loris->getDatabaseConnection(); + $db = $this->loris->getDatabaseConnection(); + $assignees = []; + $otherWatchers = []; + + // Initialize variables based on permissions + if (!$user->hasPermission('access_all_profiles')) { + $centerIDsArray = array_map( + fn($centerID) => (int)$centerID->getID(), + $user->getCenterIDs() + ); + + if (empty($centerIDsArray)) { + return [ + 'issues' => [], + 'assignees' => [], + 'otherWatchers' => [], + ]; + } + + // Fetch DCC ID + $dccRow = $db->pselectOne( + "SELECT CenterID FROM psc WHERE Name = :name", + ['name' => 'DCC'] + ); + $dccID = $dccRow ? $dccRow['CenterID'] : null; + + // Prepare placeholders + $centerPlaceholders = implode( + ',', + array_fill(0, count($centerIDsArray), '?') + ); + } + + // Prepare assignee query + if ($user->hasPermission('access_all_profiles')) { + $assigneeQuery = " + SELECT Real_name, UserID, Active + FROM users + WHERE Pending_approval = 'N' + "; + $assigneeParams = []; + } else { + $assigneeQuery = " + SELECT DISTINCT u.Real_name, u.UserID, u.Active + FROM users u + INNER JOIN user_psc_rel upr ON upr.UserID = u.ID + WHERE (upr.CenterID IN ($centerPlaceholders) OR upr.CenterID = ?) + AND u.Pending_approval = 'N' + "; + $assigneeParams = array_merge($centerIDsArray, [$dccID]); + } + + // Fetch assignees + $assigneeRows = iterator_to_array( + $db->pselect($assigneeQuery, $assigneeParams) + ); + + foreach ($assigneeRows as $a_row) { + if (!empty($a_row['UserID'])) { + $assignees[$a_row['UserID']] = $this->formatUserInformation( + $a_row['UserID'] + ); + } + } + + // Fetch potential watchers + $currentUsername = $user->getUsername(); + + $potentialWatchers = iterator_to_array( + $db->pselect( + "SELECT Real_name, UserID + FROM users + WHERE Active = 'Y' AND Pending_approval = 'N'", + [] + ) + ); - $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)"; + foreach ($potentialWatchers as $w_row) { + if (!empty($w_row['UserID']) && $w_row['UserID'] != $currentUsername) { + $otherWatchers[$w_row['UserID']] = $this->formatUserInformation( + $w_row['UserID'] + ); + } + } + + // Fetch issues + $baseQuery = " + 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 + $issueParams = []; if (!$user->hasPermission('access_all_profiles')) { - $query .= " WHERE i.centerID IN (" . - implode(',', $user->getCenterIDs()) . ")"; + $baseQuery .= " WHERE i.centerID IN ($centerPlaceholders)"; + $issueParams = $centerIDsArray; } - $query .= " ORDER BY i.issueID DESC"; + $baseQuery .= " ORDER BY i.issueID DESC"; - $issues = $db->pselect($query, []); + $issues = iterator_to_array($db->pselect($baseQuery, $issueParams)); - // Ensure $issues is an array - if (!is_array($issues)) { - $issues = iterator_to_array($issues); + if (empty($issues)) { + return [ + 'issues' => [], + 'assignees' => $assignees, + 'otherWatchers' => $otherWatchers, + ]; + } + + // Get all issue IDs + $issueIDs = array_column($issues, 'issueID'); + + // Fetch watchers for these issues + $watchingPlaceholders = implode(',', array_fill(0, count($issueIDs), '?')); + $watchersRows = iterator_to_array( + $db->pselect( + "SELECT issueID, userID + FROM issues_watching + WHERE issueID IN ($watchingPlaceholders)", + $issueIDs + ) + ); + + // Organize watchers by issue + $othersWatchingByIssue = []; + foreach ($watchersRows as $row) { + if (!empty($row['userID'])) { + $issueID = (int)$row['issueID']; + $othersWatchingByIssue[$issueID][] = $row['userID']; + } } // Format the issues data foreach ($issues as &$issue) { - $issue['reporter'] = $this->formatUserInformation( - $issue['reporter'] - ); - $issue['lastUpdatedBy'] = $this->formatUserInformation( - $issue['lastUpdatedBy'] - ); - $issue['topComments'] = $this->getTopComments( - (int)$issue['issueID'] + $issueID = (int)$issue['issueID']; + + // Set watching status + $isWatching = in_array( + $currentUsername, + $othersWatchingByIssue[$issueID] ?? [] ); + $issue['watching'] = $isWatching ? 'Yes' : 'No'; + + $assigneeUserID = $issue['assignee'] ?? null; + + // Exclude assignee from othersWatching + if (!empty($assigneeUserID) && isset($othersWatchingByIssue[$issueID])) { + $filteredWatchers = array_filter( + $othersWatchingByIssue[$issueID], + fn($watcherUserID) => $watcherUserID !== $assigneeUserID + ); + $othersWatchingByIssue[$issueID] = array_values($filteredWatchers); + } + + $issue['othersWatching'] = $othersWatchingByIssue[$issueID] ?? []; + + $issue['reporter'] = !empty($issue['reporter']) + ? $this->formatUserInformation($issue['reporter']) + : null; + $issue['lastUpdatedBy'] = !empty($issue['lastUpdatedBy']) + ? $this->formatUserInformation($issue['lastUpdatedBy']) + : null; + + $issue['topComments'] = $this->getTopComments($issueID); } - return $issues; + return [ + 'issues' => $issues, + 'assignees' => $assignees, + 'otherWatchers' => $otherWatchers, + ]; } /** From 6ec5957a2d5f7c804bde357e0c1b7d779b3e3618 Mon Sep 17 00:00:00 2001 From: ay-bh Date: Thu, 24 Oct 2024 18:06:11 -0400 Subject: [PATCH 2/7] Add assignee and watcher fields to issue comment modal --- modules/issue_tracker/jsx/IssueCard.js | 88 +++++++++++++++++-- .../jsx/IssueTrackerBatchMode.js | 10 ++- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js index 8804f5ba482..a1788945a5d 100644 --- a/modules/issue_tracker/jsx/IssueCard.js +++ b/modules/issue_tracker/jsx/IssueCard.js @@ -11,6 +11,8 @@ const IssueCard = React.memo(function IssueCard({ priorities, categories, sites, + assignees, + otherWatchers, }) { const [isEditing, setIsEditing] = useState(false); const [editedIssue, setEditedIssue] = useState({...issue}); @@ -21,6 +23,9 @@ const IssueCard = React.memo(function IssueCard({ const [newComment, setNewComment] = useState(''); const [isSubmittingComment, setIsSubmittingComment] = useState(false); + const [newAssignee, setNewAssignee] = useState(issue.assignee || ''); + const [newWatchers, setNewWatchers] = useState(issue.othersWatching || []); + const handleInputChange = (field, value) => { setTempEditedIssue((prev) => ({ ...prev, @@ -100,23 +105,46 @@ const IssueCard = React.memo(function IssueCard({ }; const handleOpenAddCommentModal = () => { + setNewAssignee(issue.assignee || ''); + setNewWatchers(issue.othersWatching || []); setShowAddCommentModal(true); }; const handleCloseAddCommentModal = () => { setShowAddCommentModal(false); setNewComment(''); + setNewAssignee(issue.assignee || ''); + setNewWatchers(issue.othersWatching || []); }; const handleAddCommentChange = (e) => { setNewComment(e.target.value); }; + const handleNewAssigneeChange = (e) => { + setNewAssignee(e.target.value); + }; + + const handleNewWatchersChange = (e) => { + const options = e.target.options; + const selectedWatchers = []; + for (let i = 0; i < options.length; i++) { + if (options[i].selected) { + selectedWatchers.push(options[i].value); + } + } + setNewWatchers(selectedWatchers); + }; + const handleAddCommentSubmit = (e) => { e.preventDefault(); - if (!newComment.trim()) { - showAlertMessage('error', 'Comment cannot be empty'); + const trimmedComment = newComment.trim(); + const hasAssigneeChanged = newAssignee !== issue.assignee; + const hasWatchersChanged = JSON.stringify(newWatchers) !== + JSON.stringify(issue.othersWatching); + if (!trimmedComment && !hasAssigneeChanged && !hasWatchersChanged) { + showAlertMessage('info', 'Please add a comment or make changes'); return; } @@ -129,7 +157,16 @@ const IssueCard = React.memo(function IssueCard({ formData.append(key, value === null ? 'null' : value); }); - formData.append('comment', newComment.trim()); + // Only append comment if it's not empty + if (trimmedComment) { + formData.append('comment', newComment.trim()); + } + + formData.append('assignee', newAssignee || 'null'); + formData.append( + 'othersWatching', + newWatchers.length > 0 ? newWatchers.join(',') : '' + ); fetch(`${loris.BaseURL}/issue_tracker/Edit/`, { method: 'POST', @@ -145,7 +182,7 @@ const IssueCard = React.memo(function IssueCard({ return response.json(); }) .then((data) => { - showAlertMessage('success', 'Comment added successfully'); + showAlertMessage('success', 'Issue updated successfully'); handleCloseAddCommentModal(); onUpdate(); }) @@ -174,10 +211,48 @@ const IssueCard = React.memo(function IssueCard({ value={newComment} onChange={handleAddCommentChange} className="textarea" - required disabled={isSubmittingComment} /> +
+ + +
+
+ + +
+
@@ -371,6 +366,29 @@ const IssueCard = React.memo(function IssueCard({ ))} +
+ + +
) : ( <> @@ -395,6 +413,13 @@ const IssueCard = React.memo(function IssueCard({ 'Uncategorized'} +
+ + + {sites[String(tempEditedIssue.centerID)] || + 'No Site'} + +
)} From 448b5eaab7387a59dfd19e1e479e75bd846d37ff Mon Sep 17 00:00:00 2001 From: ay-bh Date: Sat, 26 Oct 2024 14:29:12 -0400 Subject: [PATCH 4/7] Fix Phan static analysis errors --- modules/issue_tracker/php/edit.class.inc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/issue_tracker/php/edit.class.inc b/modules/issue_tracker/php/edit.class.inc index 2b1fd81af65..6f21cc54ee3 100644 --- a/modules/issue_tracker/php/edit.class.inc +++ b/modules/issue_tracker/php/edit.class.inc @@ -296,11 +296,14 @@ class Edit extends \NDB_Page implements ETagCalculator $db = $this->loris->getDatabaseConnection(); $assignees = []; $otherWatchers = []; + $centerIDsArray = []; + $dccID = null; + $centerPlaceholders = ''; // Initialize variables based on permissions if (!$user->hasPermission('access_all_profiles')) { $centerIDsArray = array_map( - fn($centerID) => (int)$centerID->getID(), + fn($centerID) => (int)$centerID->__toString(), $user->getCenterIDs() ); @@ -317,7 +320,7 @@ class Edit extends \NDB_Page implements ETagCalculator "SELECT CenterID FROM psc WHERE Name = :name", ['name' => 'DCC'] ); - $dccID = $dccRow ? $dccRow['CenterID'] : null; + $dccID = $dccRow ? (int)$dccRow: null; // Prepare placeholders $centerPlaceholders = implode( From 5fbc74f135ef86bf61aea2c6b98100bf9459f919 Mon Sep 17 00:00:00 2001 From: ay-bh Date: Sat, 2 Nov 2024 10:36:45 -0400 Subject: [PATCH 5/7] Update site dropdown to map empty value to null for All Sites option --- modules/issue_tracker/jsx/IssueCard.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/issue_tracker/jsx/IssueCard.js b/modules/issue_tracker/jsx/IssueCard.js index 187db3498c2..4b7266af3f2 100644 --- a/modules/issue_tracker/jsx/IssueCard.js +++ b/modules/issue_tracker/jsx/IssueCard.js @@ -29,7 +29,7 @@ const IssueCard = React.memo(function IssueCard({ const handleInputChange = (field, value) => { setTempEditedIssue((prev) => ({ ...prev, - [field]: value, + [field]: value === '' ? null : value, })); }; @@ -376,9 +376,8 @@ const IssueCard = React.memo(function IssueCard({ onChange={(e) => handleInputChange('centerID', e.target.value) } - required > - + {Object.entries(sites).map(([id, name]) => (