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 = '= BASE_URL ?>';
var initialQuery = = json_encode($initialSearchTerm) ?>;
+ var enterHint = = json_encode($tpl->__('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() ?>