Fix views/controllers/router: command palette, create form, admin views
- Consolidate the duplicated command palette to a single overlay + init in the footer; fix New Ticket to route to /ticket/create (was a 404 /create); keep the CSP nonce and all commands - TicketController create(): trim title, require a non-empty description, and honor the posted status (validated against the canonical list) instead of silently discarding it - UserActivityView: 'Active Users' counts only users active in the selected range, not every registered user - layout_footer/DashboardView: local esc() now escapes quotes so values used in HTML attributes can't break out - TicketView: comments tab badge shows the true total, not just page one - layout_header: gate the 'View activity log' link behind the admin flag - index.php: validate /admin/user-activity date params; anchor the legacy /ticket.php route; align the audit action-type whitelist with the dropdown - ApiKeysView: correct the external API sample to /create_ticket_api.php Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -93,19 +93,27 @@ class TicketController
|
|||||||
$visibilityGroups = implode(',', array_map('trim', $_POST['visibility_groups']));
|
$visibilityGroups = implode(',', array_map('trim', $_POST['visibility_groups']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Honor the posted status, validated against the app's canonical list
|
||||||
|
$validStatuses = $GLOBALS['config']['TICKET_STATUSES'] ?? ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||||
|
$status = $_POST['status'] ?? 'Open';
|
||||||
|
if (!in_array($status, $validStatuses, true)) {
|
||||||
|
$status = 'Open';
|
||||||
|
}
|
||||||
|
|
||||||
$ticketData = [
|
$ticketData = [
|
||||||
'title' => $_POST['title'] ?? '',
|
'title' => trim($_POST['title'] ?? ''),
|
||||||
'description' => $_POST['description'] ?? '',
|
'description' => $_POST['description'] ?? '',
|
||||||
'priority' => $_POST['priority'] ?? '4',
|
'priority' => $_POST['priority'] ?? '4',
|
||||||
'category' => $_POST['category'] ?? 'General',
|
'category' => $_POST['category'] ?? 'General',
|
||||||
'type' => $_POST['type'] ?? 'Issue',
|
'type' => $_POST['type'] ?? 'Issue',
|
||||||
|
'status' => $status,
|
||||||
'visibility' => $_POST['visibility'] ?? 'public',
|
'visibility' => $_POST['visibility'] ?? 'public',
|
||||||
'visibility_groups' => $visibilityGroups,
|
'visibility_groups' => $visibilityGroups,
|
||||||
'assigned_to' => !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null
|
'assigned_to' => !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null
|
||||||
];
|
];
|
||||||
|
|
||||||
// Validate input
|
// Validate input (server-side; form is novalidate)
|
||||||
if (empty($ticketData['title'])) {
|
if ($ticketData['title'] === '') {
|
||||||
$error = "Title is required";
|
$error = "Title is required";
|
||||||
$templates = $this->templateModel->getAllTemplates();
|
$templates = $this->templateModel->getAllTemplates();
|
||||||
$allUsers = $this->userModel->getAllUsers();
|
$allUsers = $this->userModel->getAllUsers();
|
||||||
@@ -114,6 +122,15 @@ class TicketController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trim($ticketData['description']) === '') {
|
||||||
|
$error = "Description is required";
|
||||||
|
$templates = $this->templateModel->getAllTemplates();
|
||||||
|
$allUsers = $this->userModel->getAllUsers();
|
||||||
|
$conn = $this->conn; // Make $conn available to view
|
||||||
|
include dirname(__DIR__) . '/views/CreateTicketView.php';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create ticket with user tracking
|
// Create ticket with user tracking
|
||||||
$result = $this->ticketModel->createTicket($ticketData, $userId);
|
$result = $this->ticketModel->createTicket($ticketData, $userId);
|
||||||
|
|
||||||
|
|||||||
@@ -249,8 +249,11 @@ switch (true) {
|
|||||||
$params = [];
|
$params = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
|
|
||||||
$allowedActionTypes = ['create','update','delete','comment','assign','status_change','login','security',
|
// Mirrors AuditLogModel::VALID_ACTION_TYPES so every option offered by the
|
||||||
'ticket_create','ticket_update','ticket_delete','attachment_delete','attachment_upload'];
|
// audit-log filter dropdown is actually accepted here.
|
||||||
|
$allowedActionTypes = ['create','update','delete','view','security_event',
|
||||||
|
'login','logout','assign','unassign','comment','mention',
|
||||||
|
'revoke','attachment_upload','attachment_delete','bulk_update'];
|
||||||
if (!empty($_GET['action_type']) && in_array($_GET['action_type'], $allowedActionTypes, true)) {
|
if (!empty($_GET['action_type']) && in_array($_GET['action_type'], $allowedActionTypes, true)) {
|
||||||
$whereConditions[] = "al.action_type = ?";
|
$whereConditions[] = "al.action_type = ?";
|
||||||
$params[] = $_GET['action_type'];
|
$params[] = $_GET['action_type'];
|
||||||
@@ -335,9 +338,12 @@ switch (true) {
|
|||||||
case $requestPath == '/admin/user-activity':
|
case $requestPath == '/admin/user-activity':
|
||||||
requireAdmin($currentUser);
|
requireAdmin($currentUser);
|
||||||
|
|
||||||
|
// Validate date params (YYYY-MM-DD) like the audit-log route; fall back to defaults on garbage
|
||||||
|
$uaFrom = $_GET['date_from'] ?? '';
|
||||||
|
$uaTo = $_GET['date_to'] ?? '';
|
||||||
$dateRange = [
|
$dateRange = [
|
||||||
'from' => $_GET['date_from'] ?? date('Y-m-d', strtotime('-30 days')),
|
'from' => preg_match('/^\d{4}-\d{2}-\d{2}$/', $uaFrom) ? $uaFrom : date('Y-m-d', strtotime('-30 days')),
|
||||||
'to' => $_GET['date_to'] ?? date('Y-m-d')
|
'to' => preg_match('/^\d{4}-\d{2}-\d{2}$/', $uaTo) ? $uaTo : date('Y-m-d')
|
||||||
];
|
];
|
||||||
|
|
||||||
// Optimized query using LEFT JOINs with aggregated subqueries instead of correlated subqueries
|
// Optimized query using LEFT JOINs with aggregated subqueries instead of correlated subqueries
|
||||||
@@ -410,7 +416,7 @@ switch (true) {
|
|||||||
header("Location: /");
|
header("Location: /");
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
case preg_match('/^\/ticket\.php/', $requestPath) && isset($_GET['id']):
|
case preg_match('/^\/ticket\.php$/', $requestPath) && isset($_GET['id']):
|
||||||
$legacyId = (string)$_GET['id'];
|
$legacyId = (string)$_GET['id'];
|
||||||
if (ctype_digit($legacyId) && (int)$legacyId > 0) {
|
if (ctype_digit($legacyId) && (int)$legacyId > 0) {
|
||||||
header("Location: /ticket/" . $legacyId);
|
header("Location: /ticket/" . $legacyId);
|
||||||
|
|||||||
@@ -1317,7 +1317,7 @@ if (advForm) advForm.addEventListener('submit', function(e) {
|
|||||||
var pLabels = { '1':'P1 — Critical', '2':'P2 — High', '3':'P3 — Medium', '4':'P4 — Low', '5':'P5 — Minimal' };
|
var pLabels = { '1':'P1 — Critical', '2':'P2 — High', '3':'P3 — Medium', '4':'P4 — Low', '5':'P5 — Minimal' };
|
||||||
var dotClass = { 'Open':'lt-dot-up', 'In Progress':'lt-dot-warn', 'Pending':'lt-dot--orange', 'Closed':'lt-dot-idle' };
|
var dotClass = { 'Open':'lt-dot-up', 'In Progress':'lt-dot-warn', 'Pending':'lt-dot--orange', 'Closed':'lt-dot-idle' };
|
||||||
|
|
||||||
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
||||||
|
|
||||||
function fmtAge(dateStr) {
|
function fmtAge(dateStr) {
|
||||||
var d = new Date(dateStr);
|
var d = new Date(dateStr);
|
||||||
|
|||||||
@@ -461,8 +461,8 @@ include __DIR__ . '/layout_header.php';
|
|||||||
<button type="button" class="lt-tab" id="comments-tab-btn"
|
<button type="button" class="lt-tab" id="comments-tab-btn"
|
||||||
role="tab" data-tab="comments-panel" aria-selected="false" aria-controls="comments-panel">
|
role="tab" data-tab="comments-panel" aria-selected="false" aria-controls="comments-panel">
|
||||||
Comments
|
Comments
|
||||||
<?php if (!empty($comments)) : ?>
|
<?php if ($totalComments > 0) : ?>
|
||||||
<span class="lt-badge lt-badge-sm"><?= count($comments) ?></span>
|
<span class="lt-badge lt-badge-sm"><?= (int)$totalComments ?></span>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="lt-tab" id="attachments-tab-btn"
|
<button type="button" class="lt-tab" id="attachments-tab-btn"
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
</p>
|
</p>
|
||||||
<div class="lt-code-block">
|
<div class="lt-code-block">
|
||||||
<div class="lt-code-header"><span class="lt-code-lang">CURL</span></div>
|
<div class="lt-code-header"><span class="lt-code-lang">CURL</span></div>
|
||||||
<pre><code>curl -X POST https://your-instance/api/create_ticket.php \
|
<pre><code>curl -X POST https://your-instance/create_ticket_api.php \
|
||||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"title":"My ticket","category":"General","type":"Issue","priority":3}'</code></pre>
|
-d '{"title":"My ticket","category":"General","type":"Issue","priority":3}'</code></pre>
|
||||||
|
|||||||
@@ -29,9 +29,14 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<label class="lt-label" for="action_type">Action Type</label>
|
<label class="lt-label" for="action_type">Action Type</label>
|
||||||
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
||||||
<option value="">All Actions</option>
|
<option value="">All Actions</option>
|
||||||
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a) : ?>
|
<?php
|
||||||
|
// Mirrors AuditLogModel::VALID_ACTION_TYPES (the backend whitelist of loggable actions)
|
||||||
|
$auditActionTypes = ['create','update','delete','view','security_event',
|
||||||
|
'login','logout','assign','unassign','comment','mention',
|
||||||
|
'revoke','attachment_upload','attachment_delete','bulk_update'];
|
||||||
|
foreach ($auditActionTypes as $a) : ?>
|
||||||
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8') ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $a)), ENT_QUOTES, 'UTF-8') ?></option>
|
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8') ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $a)), ENT_QUOTES, 'UTF-8') ?></option>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-form-group" style="margin:0">
|
<div class="lt-form-group" style="margin:0">
|
||||||
|
|||||||
@@ -43,11 +43,23 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
|
|
||||||
<!-- Summary stats -->
|
<!-- Summary stats -->
|
||||||
<?php if (!empty($userStats)) : ?>
|
<?php if (!empty($userStats)) : ?>
|
||||||
|
<?php
|
||||||
|
// "Active" = users with >=1 tracked action within the selected date range.
|
||||||
|
// The query LEFT JOINs from all users, so $userStats includes zero-activity users.
|
||||||
|
$activeUsers = 0;
|
||||||
|
foreach ($userStats as $_u) {
|
||||||
|
$_activity = ($_u['tickets_created'] ?? 0) + ($_u['tickets_resolved'] ?? 0)
|
||||||
|
+ ($_u['comments_added'] ?? 0) + ($_u['tickets_assigned'] ?? 0);
|
||||||
|
if ($_activity > 0) {
|
||||||
|
$activeUsers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<div class="lt-stats-grid lt-mb-md">
|
<div class="lt-stats-grid lt-mb-md">
|
||||||
<div class="lt-stat-card">
|
<div class="lt-stat-card">
|
||||||
<div class="lt-stat-icon lt-text-cyan">[ # ]</div>
|
<div class="lt-stat-icon lt-text-cyan">[ # ]</div>
|
||||||
<div class="lt-stat-info">
|
<div class="lt-stat-info">
|
||||||
<div class="lt-stat-value"><?= count($userStats) ?></div>
|
<div class="lt-stat-value"><?= (int)$activeUsers ?></div>
|
||||||
<div class="lt-stat-label">Active Users</div>
|
<div class="lt-stat-label">Active Users</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-2
@@ -138,10 +138,13 @@
|
|||||||
var themeBtn = document.getElementById('lt-theme-btn');
|
var themeBtn = document.getElementById('lt-theme-btn');
|
||||||
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
|
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
|
||||||
|
|
||||||
// Command palette — global navigation commands available on all pages
|
// Command palette — single global instance (overlay DOM above; base.js binds Ctrl/Cmd+K)
|
||||||
var _cpCmds = [
|
var _cpCmds = [
|
||||||
{ id: 'nav-dashboard', group: 'Navigation', icon: '~', label: 'Dashboard', kbd: 'G D', action: function() { window.location.href = '/'; } },
|
{ id: 'nav-dashboard', group: 'Navigation', icon: '~', label: 'Dashboard', kbd: 'G D', action: function() { window.location.href = '/'; } },
|
||||||
{ id: 'nav-new-ticket', group: 'Navigation', icon: '+', label: 'New Ticket', kbd: 'N', action: function() { window.location.href = '/ticket/create'; } },
|
{ id: 'nav-new-ticket', group: 'Navigation', icon: '+', label: 'New Ticket', kbd: 'N', action: function() { window.location.href = '/ticket/create'; } },
|
||||||
|
{ id: 'filter-mine', group: 'Filter', icon: '◈', label: 'My Open Tickets', action: function() { window.location.href = '/?assigned_to=me&status=Open,In+Progress,Pending'; } },
|
||||||
|
{ id: 'filter-unassigned', group: 'Filter', icon: '◌', label: 'Unassigned Tickets', action: function() { window.location.href = '/?assigned_to=unassigned'; } },
|
||||||
|
{ id: 'filter-critical', group: 'Filter', icon: '!', label: 'P1 Critical Tickets', action: function() { window.location.href = '/?priority=1'; } },
|
||||||
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
|
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
|
||||||
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
||||||
];
|
];
|
||||||
@@ -156,7 +159,20 @@
|
|||||||
{ id: 'admin-api-keys', group: 'Admin', icon: 'K', label: 'API Keys', action: function() { window.location.href = '/admin/api-keys'; } },
|
{ id: 'admin-api-keys', group: 'Admin', icon: 'K', label: 'API Keys', action: function() { window.location.href = '/admin/api-keys'; } },
|
||||||
]);
|
]);
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
// Recently viewed tickets from localStorage
|
||||||
|
try {
|
||||||
|
var _recent = JSON.parse(localStorage.getItem('lt_recent_tickets') || '[]');
|
||||||
|
_recent.slice(0, 5).forEach(function(id) {
|
||||||
|
_cpCmds.push({ id: 'recent-' + id, group: 'Recent', icon: '◷', label: 'Ticket #' + id, tags: ['ticket'], action: function(tid) { return function() { window.location.href = '/ticket/' + tid; }; }(id) });
|
||||||
|
});
|
||||||
|
} catch (_e) { /* ignore malformed localStorage */ }
|
||||||
lt.cmdPalette.init(_cpCmds);
|
lt.cmdPalette.init(_cpCmds);
|
||||||
|
|
||||||
|
// Bind the header ⌘K trigger button (no inline onclick — CSP blocks inline handlers)
|
||||||
|
var _cmdTrigger = document.getElementById('lt-cmd-trigger');
|
||||||
|
if (_cmdTrigger) {
|
||||||
|
_cmdTrigger.addEventListener('click', function() { lt.cmdPalette.open(); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch lt.api mutating methods to auto-rotate CSRF token when server returns a new one
|
// Patch lt.api mutating methods to auto-rotate CSRF token when server returns a new one
|
||||||
@@ -194,7 +210,7 @@
|
|||||||
return Math.floor(diff / 86400) + 'd ago';
|
return Math.floor(diff / 86400) + 'd ago';
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
||||||
|
|
||||||
function renderNotifications(data) {
|
function renderNotifications(data) {
|
||||||
lt.notif.set(bell, data.unread_count || 0);
|
lt.notif.set(bell, data.unread_count || 0);
|
||||||
|
|||||||
+3
-62
@@ -196,7 +196,9 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
|||||||
<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Loading…</div>
|
<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-notif-panel-footer">
|
<div class="lt-notif-panel-footer">
|
||||||
|
<?php if ($_lt_isAdmin) : ?>
|
||||||
<a href="/admin/audit-log" class="lt-btn lt-btn-ghost lt-btn-sm lt-w-full lt-text-center">View activity log</a>
|
<a href="/admin/audit-log" class="lt-btn lt-btn-ghost lt-btn-sm lt-w-full lt-text-center">View activity log</a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,67 +214,6 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
|||||||
|
|
||||||
</header><!-- /.lt-header -->
|
</header><!-- /.lt-header -->
|
||||||
|
|
||||||
<!-- ── COMMAND PALETTE OVERLAY (Ctrl+K / ⌘K) ──────────────────── -->
|
<!-- Command palette overlay + init live in layout_footer.php (single instance) -->
|
||||||
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
|
|
||||||
<div id="lt-cmd-palette" class="lt-cmd-palette" role="combobox" aria-expanded="true" aria-haspopup="listbox">
|
|
||||||
<div class="lt-cmd-input-wrap">
|
|
||||||
<span aria-hidden="true" style="opacity:0.45;margin-right:0.4rem;font-size:0.9em">⌕</span>
|
|
||||||
<input class="lt-cmd-input" type="text" placeholder="Type a command or search…"
|
|
||||||
autocomplete="off" spellcheck="false" aria-label="Command search" aria-autocomplete="list"
|
|
||||||
aria-controls="lt-cmd-results-list">
|
|
||||||
<kbd style="font-size:0.6rem;opacity:0.4;white-space:nowrap">ESC</kbd>
|
|
||||||
</div>
|
|
||||||
<div class="lt-cmd-results" id="lt-cmd-results-list" role="listbox"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
|
||||||
(function() {
|
|
||||||
var isAdmin = <?= json_encode($_lt_isAdmin) ?>;
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var commands = [
|
|
||||||
{ id: 'nav-dashboard', label: 'Dashboard', icon: '⌂', group: 'Navigate', action: function(){ location.href = '/'; } },
|
|
||||||
{ id: 'nav-new-ticket', label: 'New Ticket', icon: '+', group: 'Navigate', kbd: 'N', action: function(){ location.href = '/create'; } },
|
|
||||||
{ id: 'filter-mine', label: 'My Open Tickets', icon: '◈', group: 'Filter', action: function(){ location.href = '/?assigned_to=me&status=Open,In+Progress,Pending'; } },
|
|
||||||
{ id: 'filter-unassigned', label: 'Unassigned Tickets', icon: '◌', group: 'Filter', action: function(){ location.href = '/?assigned_to=unassigned'; } },
|
|
||||||
{ id: 'filter-critical', label: 'P1 Critical Tickets', icon: '!', group: 'Filter', action: function(){ location.href = '/?priority=1'; } },
|
|
||||||
];
|
|
||||||
if (isAdmin) {
|
|
||||||
[
|
|
||||||
{ id: 'admin-templates', label: 'Admin: Templates', icon: '▤', href: '/admin/templates' },
|
|
||||||
{ id: 'admin-workflow', label: 'Admin: Workflow', icon: '⇌', href: '/admin/workflow' },
|
|
||||||
{ id: 'admin-audit', label: 'Admin: Audit Log', icon: '📋', href: '/admin/audit-log' },
|
|
||||||
{ id: 'admin-api-keys', label: 'Admin: API Keys', icon: '🔑', href: '/admin/api-keys' },
|
|
||||||
{ id: 'admin-users', label: 'Admin: User Activity', icon: '👤', href: '/admin/user-activity' },
|
|
||||||
{ id: 'admin-recurring', label: 'Admin: Recurring', icon: '↻', href: '/admin/recurring-tickets' },
|
|
||||||
{ id: 'admin-fields', label: 'Admin: Custom Fields', icon: '⊞', href: '/admin/custom-fields' },
|
|
||||||
].forEach(function(c) {
|
|
||||||
commands.push({ id: c.id, label: c.label, icon: c.icon, group: 'Admin', action: function(href){ return function(){ location.href = href; }; }(c.href) });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Inject recent ticket IDs from localStorage
|
|
||||||
try {
|
|
||||||
var recent = JSON.parse(localStorage.getItem('lt_recent_tickets') || '[]');
|
|
||||||
recent.slice(0, 5).forEach(function(id) {
|
|
||||||
commands.push({ id: 'recent-' + id, label: 'Ticket #' + id, icon: '◷', group: 'Recent', tags: ['ticket'], action: function(tid){ return function(){ location.href = '/ticket/' + tid; }; }(id) });
|
|
||||||
});
|
|
||||||
} catch(_) {}
|
|
||||||
if (window.lt && lt.cmdPalette) lt.cmdPalette.init(commands);
|
|
||||||
// Bind the header ⌘K trigger here (no inline onclick — CSP blocks inline handlers)
|
|
||||||
var cmdTrigger = document.getElementById('lt-cmd-trigger');
|
|
||||||
if (cmdTrigger) {
|
|
||||||
cmdTrigger.addEventListener('click', function() {
|
|
||||||
if (window.lt && lt.cmdPalette) lt.cmdPalette.open();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Keyboard shortcut: Ctrl+K / Cmd+K
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (window.lt && lt.cmdPalette) lt.cmdPalette.open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main class="lt-main lt-container" id="main-content" style="padding-top: calc(var(--header-height, 56px) + var(--space-lg, 1.5rem))">
|
<main class="lt-main lt-container" id="main-content" style="padding-top: calc(var(--header-height, 56px) + var(--space-lg, 1.5rem))">
|
||||||
|
|||||||
Reference in New Issue
Block a user