diff --git a/app/Domain/Tickets/Js/ticketsController.js b/app/Domain/Tickets/Js/ticketsController.js index 73c53dc618..2af84b45ba 100644 --- a/app/Domain/Tickets/Js/ticketsController.js +++ b/app/Domain/Tickets/Js/ticketsController.js @@ -1285,6 +1285,10 @@ leantime.ticketsController = (function () { var $lastCard = $column.children('.ticketBox.moveable').last(); if ($lastCard.length) { + requestData.afterPinnedRank = parseInt($lastCard.attr('data-kanban-pinned-rank'), 10); + if (isNaN(requestData.afterPinnedRank)) { + requestData.afterPinnedRank = 1; + } requestData.afterSortIndex = parseInt($lastCard.attr('data-kanban-sort-index'), 10) || 0; requestData.afterTicketId = parseInt($lastCard.attr('data-ticket-id'), 10) || 0; } diff --git a/app/Domain/Tickets/Repositories/Tickets.php b/app/Domain/Tickets/Repositories/Tickets.php index 70e7dfb117..933de13b75 100644 --- a/app/Domain/Tickets/Repositories/Tickets.php +++ b/app/Domain/Tickets/Repositories/Tickets.php @@ -638,10 +638,19 @@ public function getAllBySearchCriteria(array $searchCriteria, string $sort = 'st public function getKanbanPageBySearchCriteria(array $searchCriteria, int $limit, int $offset = 0): array { + $pinnedTicketIds = array_values(array_unique(array_map('intval', $searchCriteria['pinnedTicketIds'] ?? []))); + $pinnedRankExpression = '1'; + if (! empty($pinnedTicketIds)) { + $pinnedIdIn = DbCore::arrayToPdoBindingString('pinnedTicket', count($pinnedTicketIds)); + $pinnedRankExpression = "CASE WHEN zp_tickets.id IN({$pinnedIdIn}) THEN 0 ELSE 1 END"; + } + $kanbanCursorCondition = ''; + $query = " SELECT zp_tickets.id, - zp_tickets.kanbanSortIndex + zp_tickets.kanbanSortIndex, + {$pinnedRankExpression} AS pinnedRank FROM zp_tickets LEFT JOIN zp_projects ON zp_tickets.projectId = zp_projects.id @@ -712,23 +721,33 @@ public function getKanbanPageBySearchCriteria(array $searchCriteria, int $limit, } if ( - isset($searchCriteria['afterSortIndex'], $searchCriteria['afterTicketId']) + isset($searchCriteria['afterPinnedRank'], $searchCriteria['afterSortIndex'], $searchCriteria['afterTicketId']) + && $searchCriteria['afterPinnedRank'] !== null && $searchCriteria['afterSortIndex'] !== null && $searchCriteria['afterTicketId'] !== null ) { - $query .= ' AND (zp_tickets.kanbanSortIndex > :afterSortIndex OR (zp_tickets.kanbanSortIndex = :afterSortIndex AND zp_tickets.id < :afterTicketId))'; + $kanbanCursorCondition = ' + HAVING ( + pinnedRank > :afterPinnedRank + OR (pinnedRank = :afterPinnedRank AND kanbanSortIndex > :afterSortIndex) + OR (pinnedRank = :afterPinnedRank AND kanbanSortIndex = :afterSortIndex AND id < :afterTicketId) + ) + '; } - $query .= ' + $query .= " GROUP BY zp_tickets.id - ORDER BY zp_tickets.kanbanSortIndex ASC, zp_tickets.id DESC + {$kanbanCursorCondition} + ORDER BY pinnedRank ASC, zp_tickets.kanbanSortIndex ASC, zp_tickets.id DESC LIMIT :limit - '; + "; $query = " SELECT page.id, page.kanbanSortIndex, + page.pinnedRank, + IF(page.pinnedRank = 0, 1, 0) AS pinned, ticket.headline, ticket.description, ticket.date, @@ -775,7 +794,7 @@ public function getKanbanPageBySearchCriteria(array $searchCriteria, int $limit, LEFT JOIN zp_user AS editor ON ticket.editorId = editor.id LEFT JOIN zp_tickets AS milestone ON ticket.milestoneid = milestone.id AND ticket.milestoneid > 0 AND milestone.type = 'milestone' LEFT JOIN zp_tickets AS parent ON ticket.dependingTicketId = parent.id - ORDER BY page.kanbanSortIndex ASC, ticket.id DESC + ORDER BY page.pinnedRank ASC, page.kanbanSortIndex ASC, ticket.id DESC "; $stmn = $this->db->database->prepare($query); @@ -849,14 +868,20 @@ public function getKanbanPageBySearchCriteria(array $searchCriteria, int $limit, } if ( - isset($searchCriteria['afterSortIndex'], $searchCriteria['afterTicketId']) + isset($searchCriteria['afterPinnedRank'], $searchCriteria['afterSortIndex'], $searchCriteria['afterTicketId']) + && $searchCriteria['afterPinnedRank'] !== null && $searchCriteria['afterSortIndex'] !== null && $searchCriteria['afterTicketId'] !== null ) { + $stmn->bindValue(':afterPinnedRank', $searchCriteria['afterPinnedRank'], PDO::PARAM_INT); $stmn->bindValue(':afterSortIndex', $searchCriteria['afterSortIndex'], PDO::PARAM_INT); $stmn->bindValue(':afterTicketId', $searchCriteria['afterTicketId'], PDO::PARAM_INT); } + foreach ($pinnedTicketIds as $key => $ticketId) { + $stmn->bindValue(':pinnedTicket'.$key, $ticketId, PDO::PARAM_INT); + } + $stmn->bindValue(':requestorId', session('userdata.id') ?? '-1', PDO::PARAM_INT); $stmn->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); $stmn->execute(); diff --git a/app/Domain/Tickets/Services/Tickets.php b/app/Domain/Tickets/Services/Tickets.php index 85ea4f40bb..88c4144c7c 100644 --- a/app/Domain/Tickets/Services/Tickets.php +++ b/app/Domain/Tickets/Services/Tickets.php @@ -765,6 +765,10 @@ public function getKanbanTicketsForStatus(array $params, int $status, int $limit $searchCriteria['status'] = (string) $status; $searchCriteria['afterSortIndex'] = isset($params['afterSortIndex']) ? (int) $params['afterSortIndex'] : null; $searchCriteria['afterTicketId'] = isset($params['afterTicketId']) ? (int) $params['afterTicketId'] : null; + $searchCriteria['afterPinnedRank'] = isset($params['afterPinnedRank']) ? (int) $params['afterPinnedRank'] : null; + $searchCriteria['pinnedTicketIds'] = session()->exists('currentProject') + ? $this->getPinnedTickets((int) session('currentProject')) + : []; $tickets = $this->ticketRepository->getKanbanPageBySearchCriteria( $searchCriteria, diff --git a/app/Domain/Tickets/Templates/partials/kanbanTicketCard.tpl.php b/app/Domain/Tickets/Templates/partials/kanbanTicketCard.tpl.php index f3d1b101ca..a566863a30 100644 --- a/app/Domain/Tickets/Templates/partials/kanbanTicketCard.tpl.php +++ b/app/Domain/Tickets/Templates/partials/kanbanTicketCard.tpl.php @@ -8,7 +8,7 @@ $isLastColumn = $key === end($columnKeys); ?>
diff --git a/app/Domain/Tickets/Templates/submodules/kanbanSearchBar.sub.php b/app/Domain/Tickets/Templates/submodules/kanbanSearchBar.sub.php index 26c07b4337..5457255f31 100644 --- a/app/Domain/Tickets/Templates/submodules/kanbanSearchBar.sub.php +++ b/app/Domain/Tickets/Templates/submodules/kanbanSearchBar.sub.php @@ -33,6 +33,7 @@ class="modern-search-input" (function () { var baseUrl = ''; var initialQuery = ; + var enterHint = __('label.kanban_search_enter_hint')) ?>; console.log('[KanbanSearch] Base URL:', baseUrl); console.log('[KanbanSearch] Initial query:', initialQuery); @@ -94,7 +95,8 @@ function tryInit() { leantime.kanbanSearch.init({ inputSelector: '#kanbanSearch', wrapperSelector: '#kanbanSearchWrapper', - initialQuery: initialQuery + initialQuery: initialQuery, + enterHint: enterHint }); console.log('[KanbanSearch] Init completed'); return; diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index d207681b34..61e62885c1 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -221,7 +221,7 @@ function restoreFilters() {
-
+
displayNotification() ?> -