Merge development into main: quick/contained cleanup batch (#43, #44, #45, #109)
Lint / PHP (phpcs PSR-12) (push) Successful in 26s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 31s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m34s
Lint / Deploy (push) Successful in 2s
Lint / PHP (phpcs PSR-12) (push) Successful in 26s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 31s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m34s
Lint / Deploy (push) Successful in 2s
- Document intentional empty catches, fix one real gap, allow == null in eqeqeq (#44) - Remove unused lt.markdown module (#43) - Update stale README Project Structure tree and migrations docs (#45) - Add a table-insert toolbar button to the markdown editor (#109)
This commit is contained in:
+1
-1
@@ -20,6 +20,6 @@
|
|||||||
"no-useless-escape": "warn",
|
"no-useless-escape": "warn",
|
||||||
"no-regex-spaces": "warn",
|
"no-regex-spaces": "warn",
|
||||||
"semi": ["error", "always"],
|
"semi": ["error", "always"],
|
||||||
"eqeqeq": "warn"
|
"eqeqeq": ["warn", "smart"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
- `migrations/000_baseline.sql` is the full schema baseline for the whole database. It is written to be safe to re-run (idempotent) and is the source of truth for a fresh install.
|
- `migrations/000_baseline.sql` is the full schema baseline for the whole database. It is written to be safe to re-run (idempotent) and is the source of truth for a fresh install.
|
||||||
- `php migrations/migrate.php` applies any pending migration files in `migrations/` in order, tracking applied files in the `migrations` table. Use `--status` to list state and `--dry-run` to preview without executing.
|
- `php migrations/migrate.php` applies any pending migration files in `migrations/` in order, tracking applied files in the `migrations` table. Use `--status` to list state and `--dry-run` to preview without executing.
|
||||||
|
- Numbered migrations on top of the baseline (all idempotent, safe to re-run): `001_widen_bulk_operations_status.sql`, `002_fix_collation_consistency.sql`, `003_fk_on_delete_set_null.sql`, `004_fix_ticket_watchers_type.sql`. A fresh install via `000_baseline.sql` already includes all of these; they only matter for upgrading an existing database.
|
||||||
|
|
||||||
### API Endpoints
|
### API Endpoints
|
||||||
|
|
||||||
@@ -348,7 +349,9 @@ tinker_tickets/
|
|||||||
│ └── images/
|
│ └── images/
|
||||||
│ └── favicon.png
|
│ └── favicon.png
|
||||||
├── config/
|
├── config/
|
||||||
│ └── config.php # Config + .env loading
|
│ ├── config.php # Config + .env loading
|
||||||
|
│ └── requirements.php # PHP version/extension requirements (single source of
|
||||||
|
│ # truth for scripts/check_requirements.php + api/health.php)
|
||||||
├── controllers/
|
├── controllers/
|
||||||
│ ├── CommentController.php # Comment create/edit/delete + notifications
|
│ ├── CommentController.php # Comment create/edit/delete + notifications
|
||||||
│ ├── DashboardController.php # Dashboard with stats + filters
|
│ ├── DashboardController.php # Dashboard with stats + filters
|
||||||
@@ -389,6 +392,10 @@ tinker_tickets/
|
|||||||
│ └── WorkflowModel.php # Status transition workflows
|
│ └── WorkflowModel.php # Status transition workflows
|
||||||
├── migrations/
|
├── migrations/
|
||||||
│ ├── 000_baseline.sql # Full schema baseline (safe to re-run)
|
│ ├── 000_baseline.sql # Full schema baseline (safe to re-run)
|
||||||
|
│ ├── 001_widen_bulk_operations_status.sql # Upgrade-only (already in baseline for fresh installs)
|
||||||
|
│ ├── 002_fix_collation_consistency.sql # Upgrade-only (already in baseline for fresh installs)
|
||||||
|
│ ├── 003_fk_on_delete_set_null.sql # Upgrade-only (already in baseline for fresh installs)
|
||||||
|
│ ├── 004_fix_ticket_watchers_type.sql # Upgrade-only (already in baseline for fresh installs)
|
||||||
│ └── migrate.php # CLI migration runner (tracks applied migrations)
|
│ └── migrate.php # CLI migration runner (tracks applied migrations)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── check_requirements.php # Verify PHP extensions/config prerequisites
|
│ ├── check_requirements.php # Verify PHP extensions/config prerequisites
|
||||||
@@ -406,6 +413,9 @@ tinker_tickets/
|
|||||||
│ │ └── WorkflowDesignerView.php # Workflow transition designer
|
│ │ └── WorkflowDesignerView.php # Workflow transition designer
|
||||||
│ ├── CreateTicketView.php # Ticket creation with visibility
|
│ ├── CreateTicketView.php # Ticket creation with visibility
|
||||||
│ ├── DashboardView.php # Dashboard with kanban + sidebar + charts
|
│ ├── DashboardView.php # Dashboard with kanban + sidebar + charts
|
||||||
|
│ ├── error_403.php # Access-denied error page
|
||||||
|
│ ├── error_404.php # Not-found error page
|
||||||
|
│ ├── error_500.php # Fatal-error page (self-contained, no app-state deps)
|
||||||
│ ├── layout_footer.php # Shared footer (notification polling, boot sequence)
|
│ ├── layout_footer.php # Shared footer (notification polling, boot sequence)
|
||||||
│ ├── layout_header.php # Shared header (nav, command palette, theme toggle)
|
│ ├── layout_header.php # Shared header (nav, command palette, theme toggle)
|
||||||
│ └── TicketView.php # Ticket view with timeline, SLA, watcher avatars
|
│ └── TicketView.php # Ticket view with timeline, SLA, watcher avatars
|
||||||
|
|||||||
+11
-69
@@ -41,6 +41,16 @@
|
|||||||
* 32. Drag & Drop Upload
|
* 32. Drag & Drop Upload
|
||||||
* 33. Intersection Observer
|
* 33. Intersection Observer
|
||||||
* 34. Full Initialisation
|
* 34. Full Initialisation
|
||||||
|
*
|
||||||
|
* NOTE ON EMPTY CATCH BLOCKS: throughout this file, `try { ... } catch (_) {}`
|
||||||
|
* around localStorage/sessionStorage access (persisted tab/theme/column-
|
||||||
|
* visibility state, recent command-palette entries, etc.) and the terminal
|
||||||
|
* beep's AudioContext calls is intentional, not an oversight — these are
|
||||||
|
* best-effort UX affordances that must silently no-op rather than break the
|
||||||
|
* surrounding feature if storage is disabled/full (private browsing, quota)
|
||||||
|
* or audio is blocked (autoplay policy). Swallowing errors from arbitrary
|
||||||
|
* caller-supplied callbacks (e.g. viewport-change listeners) is handled
|
||||||
|
* separately with real logging, since those can hide genuine bugs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (global) {
|
(function (global) {
|
||||||
@@ -1398,7 +1408,7 @@
|
|||||||
_vpCurrent = bp;
|
_vpCurrent = bp;
|
||||||
if (bp !== prev) {
|
if (bp !== prev) {
|
||||||
const evt = { bp, w, h, prev };
|
const evt = { bp, w, h, prev };
|
||||||
_vpListeners.forEach(cb => { try { cb(evt); } catch (_) {} });
|
_vpListeners.forEach(cb => { try { cb(evt); } catch (e) { console.error('[lt.viewport] listener threw:', e); } });
|
||||||
bus.emit('viewport:change', evt);
|
bus.emit('viewport:change', evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2920,73 +2930,6 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
MODULE 54 — MARKDOWN RENDERER
|
|
||||||
lt.markdown.render(mdString) → HTML string (sanitized)
|
|
||||||
lt.markdown.init(selector) → renders all matching el's .textContent
|
|
||||||
Uses a built-in micro-renderer (no deps) for common syntax.
|
|
||||||
For full GFM, swap in marked.js: window.marked && marked.parse()
|
|
||||||
================================================================ */
|
|
||||||
const markdown = {
|
|
||||||
render(md) {
|
|
||||||
// Always use the built-in XSS-safe micro-renderer. Do NOT delegate to
|
|
||||||
// window.marked / window.markdownit: their raw HTML output is not sanitized
|
|
||||||
// here, so delegating would enable stored XSS if such a lib were ever loaded.
|
|
||||||
// Micro-renderer: covers headings, bold, italic, code, links, lists, blockquote, hr
|
|
||||||
let html = escHtml(md)
|
|
||||||
// Fenced code blocks
|
|
||||||
.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => `<pre class="lt-code-block"><code class="lt-tok tok-${lang || 'plain'}">${code.trim()}</code></pre>`)
|
|
||||||
// Inline code
|
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
||||||
// Headings
|
|
||||||
.replace(/^######\s(.+)$/gm, '<h6>$1</h6>')
|
|
||||||
.replace(/^#####\s(.+)$/gm, '<h5>$1</h5>')
|
|
||||||
.replace(/^####\s(.+)$/gm, '<h4>$1</h4>')
|
|
||||||
.replace(/^###\s(.+)$/gm, '<h3>$1</h3>')
|
|
||||||
.replace(/^##\s(.+)$/gm, '<h2>$1</h2>')
|
|
||||||
.replace(/^#\s(.+)$/gm, '<h1>$1</h1>')
|
|
||||||
// Bold / italic
|
|
||||||
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
||||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
||||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
||||||
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
||||||
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
||||||
// Links — block javascript: and data: URIs
|
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
||||||
const safeUrl = /^(https?:\/\/|\/|#|\.\.?\/)/i.test(url) ? url : '#';
|
|
||||||
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${escHtml(text)}</a>`;
|
|
||||||
})
|
|
||||||
// Images — block javascript: and data: URIs
|
|
||||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
|
|
||||||
const safeSrc = /^(https?:\/\/|\/|\.\.?\/)/i.test(src) ? src : '';
|
|
||||||
return `<img src="${safeSrc}" alt="${escHtml(alt)}" style="max-width:100%">`;
|
|
||||||
})
|
|
||||||
// Blockquote
|
|
||||||
.replace(/^>\s(.+)$/gm, '<blockquote>$1</blockquote>')
|
|
||||||
// Horizontal rule
|
|
||||||
.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '<hr>')
|
|
||||||
// Unordered list items
|
|
||||||
.replace(/^[-*+]\s(.+)$/gm, '<li>$1</li>')
|
|
||||||
.replace(/(<li>[\s\S]+?<\/li>\n?)+/g, m => `<ul>${m}</ul>`)
|
|
||||||
// Ordered list items
|
|
||||||
.replace(/^\d+\.\s(.+)$/gm, '<li>$1</li>')
|
|
||||||
// Paragraphs (double newline)
|
|
||||||
.replace(/\n{2,}/g, '</p><p>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
return `<p>${html}</p>`
|
|
||||||
.replace(/<p>(<(?:pre|ul|ol|h[1-6]|blockquote|hr)[^>]*>)/g, '$1')
|
|
||||||
.replace(/(<\/(?:pre|ul|ol|h[1-6]|blockquote|hr)>)<\/p>/g, '$1');
|
|
||||||
},
|
|
||||||
|
|
||||||
init(selector) {
|
|
||||||
document.querySelectorAll(selector).forEach(el => {
|
|
||||||
const raw = el.getAttribute('data-markdown') || el.textContent;
|
|
||||||
el.innerHTML = markdown.render(raw);
|
|
||||||
el.classList.add('lt-markdown');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
MODULE 55 — PAGINATION
|
MODULE 55 — PAGINATION
|
||||||
lt.pagination.init(navEl, opts)
|
lt.pagination.init(navEl, opts)
|
||||||
@@ -3149,7 +3092,6 @@
|
|||||||
timer,
|
timer,
|
||||||
lightbox,
|
lightbox,
|
||||||
auth,
|
auth,
|
||||||
markdown,
|
|
||||||
ticketStatus,
|
ticketStatus,
|
||||||
pagination,
|
pagination,
|
||||||
sidebarSubmenus: { init: initSidebarSubmenus },
|
sidebarSubmenus: { init: initSidebarSubmenus },
|
||||||
|
|||||||
@@ -506,6 +506,25 @@ function toolbarHeading(textareaId) {
|
|||||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toolbarTable(textareaId) {
|
||||||
|
const textarea = document.getElementById(textareaId);
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const text = textarea.value;
|
||||||
|
|
||||||
|
// Insert on its own line(s), matching the blank-line-before convention
|
||||||
|
// toolbarList/toolbarHeading rely on the surrounding text for — a table
|
||||||
|
// needs a full line to itself both before and after the separator row.
|
||||||
|
const needsLeadingNewline = start > 0 && text[start - 1] !== '\n';
|
||||||
|
const template = (needsLeadingNewline ? '\n' : '')
|
||||||
|
+ '| Header 1 | Header 2 |\n'
|
||||||
|
+ '| --- | --- |\n'
|
||||||
|
+ '| Cell 1 | Cell 2 |\n';
|
||||||
|
|
||||||
|
insertMarkdownText(textareaId, template);
|
||||||
|
}
|
||||||
|
|
||||||
function toolbarQuote(textareaId) {
|
function toolbarQuote(textareaId) {
|
||||||
const textarea = document.getElementById(textareaId);
|
const textarea = document.getElementById(textareaId);
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
@@ -544,6 +563,7 @@ function createEditorToolbar(textareaId, containerId) {
|
|||||||
<button type="button" data-toolbar-action="heading" data-textarea="${textareaId}" title="Heading">H</button>
|
<button type="button" data-toolbar-action="heading" data-textarea="${textareaId}" title="Heading">H</button>
|
||||||
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
|
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
|
||||||
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
|
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
|
||||||
|
<button type="button" data-toolbar-action="table" data-textarea="${textareaId}" title="Table">▦</button>
|
||||||
<span class="toolbar-separator"></span>
|
<span class="toolbar-separator"></span>
|
||||||
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">[ @ ]</button>
|
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">[ @ ]</button>
|
||||||
`;
|
`;
|
||||||
@@ -563,6 +583,7 @@ function createEditorToolbar(textareaId, containerId) {
|
|||||||
case 'heading': toolbarHeading(targetId); break;
|
case 'heading': toolbarHeading(targetId); break;
|
||||||
case 'list': toolbarList(targetId); break;
|
case 'list': toolbarList(targetId); break;
|
||||||
case 'quote': toolbarQuote(targetId); break;
|
case 'quote': toolbarQuote(targetId); break;
|
||||||
|
case 'table': toolbarTable(targetId); break;
|
||||||
case 'link': toolbarLink(targetId); break;
|
case 'link': toolbarLink(targetId); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -578,6 +599,7 @@ window.toolbarLink = toolbarLink;
|
|||||||
window.toolbarList = toolbarList;
|
window.toolbarList = toolbarList;
|
||||||
window.toolbarHeading = toolbarHeading;
|
window.toolbarHeading = toolbarHeading;
|
||||||
window.toolbarQuote = toolbarQuote;
|
window.toolbarQuote = toolbarQuote;
|
||||||
|
window.toolbarTable = toolbarTable;
|
||||||
window.createEditorToolbar = createEditorToolbar;
|
window.createEditorToolbar = createEditorToolbar;
|
||||||
window.insertMarkdownFormat = insertMarkdownFormat;
|
window.insertMarkdownFormat = insertMarkdownFormat;
|
||||||
window.insertMarkdownText = insertMarkdownText;
|
window.insertMarkdownText = insertMarkdownText;
|
||||||
|
|||||||
Reference in New Issue
Block a user