feat: duplicate link action, watcher migration, fulltext search migration
- CreateTicketView: "Link as duplicate" button on each duplicate result; stores chosen ticket ID in hidden field, auto-creates duplicates dependency after ticket is saved (TicketController) - migrations/004: ticket_watchers table (ticket_id, user_id primary key) - migrations/005: FULLTEXT index on tickets(title, description) for fast relevance search replacing LIKE scan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,17 @@ class TicketController {
|
||||
$GLOBALS['auditLog']->logTicketCreate($userId, $result['ticket_id'], $ticketData);
|
||||
}
|
||||
|
||||
// Auto-link as duplicate if requested from create form
|
||||
$linkDupOf = isset($_POST['link_duplicate_of']) ? (int)$_POST['link_duplicate_of'] : 0;
|
||||
if ($linkDupOf > 0) {
|
||||
$depSql = "INSERT IGNORE INTO ticket_dependencies (ticket_id, depends_on_ticket_id, dependency_type, created_by)
|
||||
VALUES (?, ?, 'duplicates', ?)";
|
||||
$depStmt = $this->conn->prepare($depSql);
|
||||
$depStmt->bind_param("iii", $result['ticket_id'], $linkDupOf, $userId);
|
||||
$depStmt->execute();
|
||||
$depStmt->close();
|
||||
}
|
||||
|
||||
// Send Matrix notification for new ticket
|
||||
NotificationHelper::sendTicketNotification($result['ticket_id'], $ticketData, 'manual');
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Migration: Add ticket watchers table
|
||||
-- Allows users to subscribe to ticket updates and receive Matrix notifications.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_watchers (
|
||||
ticket_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (ticket_id, user_id),
|
||||
INDEX idx_watcher_user (user_id)
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Migration: Add FULLTEXT index on ticket title and description
|
||||
-- Replaces LIKE '%term%' with MATCH ... AGAINST for dramatically better search
|
||||
-- performance and relevance ranking at scale.
|
||||
-- MyISAM not needed — InnoDB supports FULLTEXT since MySQL 5.6.
|
||||
|
||||
ALTER TABLE tickets ADD FULLTEXT INDEX ft_title_description (title, description);
|
||||
@@ -37,6 +37,7 @@ include __DIR__ . '/layout_header.php';
|
||||
|
||||
<input type="hidden" name="csrf_token"
|
||||
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<input type="hidden" name="link_duplicate_of" id="linkDuplicateOf" value="">
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
||||
@@ -269,6 +270,7 @@ include __DIR__ . '/layout_header.php';
|
||||
ul.className = 'duplicate-list lt-text-sm';
|
||||
data.duplicates.forEach(function (dup) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'lt-flex lt-flex-align-center lt-flex-gap-sm lt-mb-xs';
|
||||
var a = document.createElement('a');
|
||||
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
|
||||
a.target = '_blank';
|
||||
@@ -277,14 +279,30 @@ include __DIR__ . '/layout_header.php';
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'lt-text-muted';
|
||||
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
|
||||
var linkBtn = document.createElement('button');
|
||||
linkBtn.type = 'button';
|
||||
linkBtn.className = 'lt-btn lt-btn-ghost lt-btn-xs';
|
||||
linkBtn.dataset.dupId = dup.ticket_id;
|
||||
linkBtn.textContent = 'Link as duplicate';
|
||||
linkBtn.title = 'After creating, this ticket will be linked as a duplicate of #' + dup.ticket_id;
|
||||
linkBtn.addEventListener('click', function () {
|
||||
var chosen = this.dataset.dupId;
|
||||
document.getElementById('linkDuplicateOf').value = chosen;
|
||||
// Update all buttons to show current selection
|
||||
ul.querySelectorAll('[data-dup-id]').forEach(function (b) {
|
||||
b.textContent = b.dataset.dupId === chosen ? '\u2713 Will link' : 'Link as duplicate';
|
||||
b.classList.toggle('lt-btn-primary', b.dataset.dupId === chosen);
|
||||
});
|
||||
});
|
||||
li.appendChild(a);
|
||||
li.appendChild(dash);
|
||||
li.appendChild(badge);
|
||||
li.appendChild(linkBtn);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
var hint = document.createElement('p');
|
||||
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
|
||||
hint.textContent = 'Check these before creating a new ticket.';
|
||||
hint.textContent = 'Check these before creating. Use "Link as duplicate" to auto-link after create.';
|
||||
list.innerHTML = '';
|
||||
list.appendChild(ul);
|
||||
list.appendChild(hint);
|
||||
|
||||
Reference in New Issue
Block a user