From 76c0bc3c67cebb65e505f0bda36f9d0567f2c1fc Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:29:59 +0200 Subject: [PATCH 1/3] #7075 Fix ticket pinning --- app/Domain/Tickets/Js/ticketsController.js | 4 ++ app/Domain/Tickets/Repositories/Tickets.php | 41 +++++++++++++++---- app/Domain/Tickets/Services/Tickets.php | 4 ++ .../partials/kanbanTicketCard.tpl.php | 3 +- 4 files changed, 43 insertions(+), 9 deletions(-) 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); ?>
From 7c34e6d447a8f669df1da83c782f6c972e269366 Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:00:25 +0200 Subject: [PATCH 2/3] #7077 Fix kanban board ticket search --- .../submodules/kanbanSearchBar.sub.php | 4 ++- app/Language/en-US.ini | 1 + public/assets/css/components/modernSearch.css | 8 +++++ public/assets/js/app/core/modernSearch.js | 15 +++++++++- public/assets/js/app/tickets/kanbanSearch.js | 29 +++++++++++++++---- 5 files changed, 49 insertions(+), 8 deletions(-) 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/Language/en-US.ini b/app/Language/en-US.ini index 2e95a4b064..1ae5438251 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -879,6 +879,7 @@ label.client = "Client" label.filename_fileexport = "File-Export" label.progress = "Progress" label.search_term = "Search Term" +label.kanban_search_enter_hint = "Press Enter after typing to search all tickets, not only the loaded cards" label.show = "Show" label.edit = "Edit" label.no = "No" diff --git a/public/assets/css/components/modernSearch.css b/public/assets/css/components/modernSearch.css index 7f00e094b6..b26eec1d75 100644 --- a/public/assets/css/components/modernSearch.css +++ b/public/assets/css/components/modernSearch.css @@ -154,3 +154,11 @@ font-family: monospace; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } + +.modern-search-empty-hint { + padding: 14px 16px; + text-align: center; + font-size: 13px; + color: #6c757d; + line-height: 1.4; +} diff --git a/public/assets/js/app/core/modernSearch.js b/public/assets/js/app/core/modernSearch.js index 59959a31ff..59d28f5f67 100644 --- a/public/assets/js/app/core/modernSearch.js +++ b/public/assets/js/app/core/modernSearch.js @@ -30,7 +30,9 @@ leantime.modernSearch = (function () { onSelect = null, onSearch = null, debounceDelay = 300, - autocompleteDelay = 150 + autocompleteDelay = 150, + showHintWhenEmpty = false, + emptyStateHint = 'Press Enter after typing to search all tickets, not only the loaded cards', } = config; const input = document.querySelector(inputSelector); @@ -71,6 +73,17 @@ leantime.modernSearch = (function () { suggestions = getSuggestions(query); if (suggestions.length === 0) { + if (showHintWhenEmpty && query.trim() !== '') { + dropdown.innerHTML = + '
' + emptyStateHint + '
' + + ''; + dropdown.classList.add('active'); + selectedIndex = -1; + return; + } + dropdown.classList.remove('active'); return; } diff --git a/public/assets/js/app/tickets/kanbanSearch.js b/public/assets/js/app/tickets/kanbanSearch.js index 553f3f82d3..85c1a072a7 100644 --- a/public/assets/js/app/tickets/kanbanSearch.js +++ b/public/assets/js/app/tickets/kanbanSearch.js @@ -205,6 +205,20 @@ leantime.kanbanSearch = (function () { } } + function submitServerSearch(query) { + const normalizedQuery = String(query || '').trim(); + const url = new URL(window.location.href); + + if (normalizedQuery) { + url.searchParams.set('term', normalizedQuery); + } else { + url.searchParams.delete('term'); + } + + url.hash = ''; + window.location.href = url.toString(); + } + function applyFilter(type, query) { // Rebuild searchable dataset so Enter-search includes lazy-loaded cards. collectCards(); @@ -252,7 +266,10 @@ leantime.kanbanSearch = (function () { if (input.value.trim() === '') { currentSearchType = 'all'; currentSearchQuery = ''; - applyFilter('all', ''); + cards.forEach(function (card) { + card.element.classList.remove(HIDDEN_CLASS); + }); + restoreColumnCounts(); } }); } @@ -278,8 +295,7 @@ leantime.kanbanSearch = (function () { wrapper.classList.remove('has-value'); currentSearchType = 'all'; currentSearchQuery = ''; - applyFilter('all', ''); - input.focus(); + submitServerSearch(''); }); } @@ -287,6 +303,8 @@ leantime.kanbanSearch = (function () { inputSelector: inputSelector, containerSelector: wrapperSelector, getSuggestions: buildSuggestions, + showHintWhenEmpty: true, + emptyStateHint: options.enterHint || 'Press Enter after typing to search all tickets, not only the loaded cards', onSelect: function (item) { if (!item) { return; @@ -295,19 +313,18 @@ leantime.kanbanSearch = (function () { wrapper.classList.add('has-value'); currentSearchType = item.filterType || item.type || 'all'; currentSearchQuery = item.value || ''; - applyFilter(currentSearchType, currentSearchQuery); + submitServerSearch(currentSearchQuery); }, onSearch: function (query) { currentSearchType = 'all'; currentSearchQuery = query || ''; - applyFilter('all', currentSearchQuery); + submitServerSearch(currentSearchQuery); }, }); if (options.initialQuery && String(options.initialQuery).trim() !== '') { input.value = options.initialQuery; wrapper.classList.add('has-value'); - applyFilter('all', options.initialQuery); } else { applyFilter('all', ''); } From 2c15efed5f45c6d5d5a8e07d29032a128cd6c0e5 Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:19:52 +0200 Subject: [PATCH 3/3] #7093 Fix All Timesheets dropdown text color on dark theme --- .../Timesheets/Templates/showAll.tpl.php | 12 ++--- public/theme/default/css/custom.css | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) 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() ?> -