Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Domain/Tickets/Js/ticketsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
41 changes: 33 additions & 8 deletions app/Domain/Tickets/Repositories/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions app/Domain/Tickets/Services/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
$isLastColumn = $key === end($columnKeys);
?>
<div
class="ticketBox moveable container priority-border-<?= $row['priority']?>"
class="ticketBox moveable container priority-border-<?= $row['priority']?> <?= ! empty($row['pinned']) ? 'pinned-ticket' : '' ?>"
id="ticket_<?php echo $row['id']; ?>"
data-ticket-id="<?= $row['id']; ?>"
data-headline="<?= $tpl->escape($row['headline']); ?>"
data-description="<?= $tpl->escape($descriptionText); ?>"
data-tags="<?= $tpl->escape($tagsText); ?>"
data-editor-name="<?= $tpl->escape($editorFullName); ?>"
data-editor-id="<?= $tpl->escape($row['editorId'] ?? ''); ?>"
data-kanban-pinned-rank="<?= (int) ($row['pinnedRank'] ?? (! empty($row['pinned']) ? 0 : 1)); ?>"
data-kanban-sort-index="<?= (int) ($row['kanbanSortIndex'] ?? 0); ?>"
>
<div class="row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions app/Domain/Timesheets/Templates/showAll.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function restoreFilters() {

<!--this is MAIN content-->
<div class="maincontent">
<div class="maincontentinner" style="overflow-x:auto;">
<div class="maincontentinner">
<div style="float:left;">
<?php echo $tpl->displayNotification() ?>
<button type="submit" form="form" formaction="<?= BASE_URL ?>/timesheets/slackMonthlyReportController/sendCsvFromUsersProfilesWhichHaveTickboxTrue" class="dt-button" id="exportToSlackBtn" style="padding: 4px 14px;">
Expand Down Expand Up @@ -259,7 +259,7 @@ function restoreFilters() {
<td style="vertical-align: top;">
<label><?php echo $tpl->__('label.project'); ?></label>
<div class="project-dropdown-container" style="position: relative; width: 200px;">
<button type="button" class="project-dropdown-toggle" style="width: 100%; padding: 4px 14px; text-align: left; background: #fff; border: 1px solid #ccc; cursor: pointer; border-radius: 20px; font-size: 14px; line-height: 20px; height: 30px; display: flex; align-items: center; justify-content: space-between; gap: 8px; color: #555; box-sizing: border-box;" onclick="document.getElementById('projectCheckboxDropdown').style.display = document.getElementById('projectCheckboxDropdown').style.display === 'none' ? 'block' : 'none';">
<button type="button" class="project-dropdown-toggle" style="width: 100%; text-align: left; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 8px; box-sizing: border-box;" onclick="document.getElementById('projectCheckboxDropdown').style.display = document.getElementById('projectCheckboxDropdown').style.display === 'none' ? 'block' : 'none';">
<span class="selected-count" id="projectSelectedCount" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<?php
$projectFilter = $tpl->get('projectFilter');
Expand Down Expand Up @@ -294,8 +294,8 @@ function restoreFilters() {
</span>
<i class="fa fa-chevron-down" style="font-size: 10px; flex-shrink: 0;"></i>
</button>
<div id="projectCheckboxDropdown" class="project-checkbox-dropdown" style="display: none; position: absolute; z-index: 1000; background: white; border: 1px solid #d0d5dd; border-radius: 14px; width: 100%; max-height: 250px; overflow-y: auto; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.1); margin-top: 6px;">
<label style="display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #eef2f7; background: #f7f9fc; font-weight: bold; border-top-left-radius: 14px; border-top-right-radius: 14px; cursor: pointer;" onclick="event.stopPropagation();">
<div id="projectCheckboxDropdown" class="project-checkbox-dropdown" style="display: none; position: absolute; z-index: 1000; width: 100%; max-height: 250px; overflow-y: auto; margin-top: 6px;">
<label style="display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-weight: bold; cursor: pointer;" onclick="event.stopPropagation();">
<input type="checkbox" name="project[]" value="-1" class="project-checkbox-all" id="projectCheckboxAll" style="margin: 0; vertical-align: middle;"
onchange="if(this.checked) { document.querySelectorAll('.project-checkbox').forEach(function(cb){cb.checked=false;}); } updateProjectCountInline(); document.getElementById('form').submit();"
<?php if (!is_array($tpl->get('projectFilter')) || $tpl->get('projectFilter') == -1) {
Expand All @@ -307,7 +307,7 @@ function restoreFilters() {
$projectFilter = $tpl->get('projectFilter');
$selectedProjects = is_array($projectFilter) ? $projectFilter : ($projectFilter != -1 ? [$projectFilter] : []);
foreach ($tpl->get('allProjects') as $project) { ?>
<label style="display: flex; align-items: center; gap: 8px; padding: 10px 12px; cursor: pointer; color: #333; transition: background-color 0.2s ease;" class="project-checkbox-label" onmouseover="this.style.background='#eef2f7'; this.style.color='#333';" onmouseout="this.style.background='white'; this.style.color='#333';" onclick="event.stopPropagation();">
<label style="display: flex; align-items: center; gap: 8px; padding: 10px 12px; cursor: pointer; transition: background-color 0.2s ease;" class="project-checkbox-label" onclick="event.stopPropagation();">
<input type="checkbox" name="project[]" value="<?= $project['id'] ?>" class="project-checkbox" style="margin: 0; vertical-align: middle;"
onchange="if(this.checked) { document.getElementById('projectCheckboxAll').checked=false; } else { var anyChecked = document.querySelectorAll('.project-checkbox:checked').length > 0; if(!anyChecked) { document.getElementById('projectCheckboxAll').checked=true; } } updateProjectCountInline(); document.getElementById('form').submit();"
<?php if (is_array($selectedProjects) && in_array($project['id'], $selectedProjects)) {
Expand Down Expand Up @@ -421,7 +421,7 @@ function restoreFilters() {
</tr>
</table>
</div>
<div>
<div style="overflow-x:auto;">
<table cellpadding="0" cellspacing="0" border="0" class="table table-bordered display" id="allTimesheetsTable" data-hours-format="<?= $tpl->escape($hoursFormat); ?>">
<colgroup>
<col class="con0" width="100px"/>
Expand Down
1 change: 1 addition & 0 deletions app/Language/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions public/assets/css/components/modernSearch.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 14 additions & 1 deletion public/assets/js/app/core/modernSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -71,6 +73,17 @@ leantime.modernSearch = (function () {
suggestions = getSuggestions(query);

if (suggestions.length === 0) {
if (showHintWhenEmpty && query.trim() !== '') {
dropdown.innerHTML =
'<div class="modern-search-empty-hint">' + emptyStateHint + '</div>' +
'<div class="modern-search-footer">' +
'<kbd>Enter</kbd> to search' +
'</div>';
dropdown.classList.add('active');
selectedIndex = -1;
return;
}

dropdown.classList.remove('active');
return;
}
Expand Down
29 changes: 23 additions & 6 deletions public/assets/js/app/tickets/kanbanSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
});
}
Expand All @@ -278,15 +295,16 @@ leantime.kanbanSearch = (function () {
wrapper.classList.remove('has-value');
currentSearchType = 'all';
currentSearchQuery = '';
applyFilter('all', '');
input.focus();
submitServerSearch('');
});
}

leantime.modernSearch.init({
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;
Expand All @@ -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', '');
}
Expand Down
Loading
Loading