feat: aggiunta ricerca tramite regex nella pagina ricerca supodrata con modal che fornisce rapide indicazioni.
This commit is contained in:
1 parent
2ca95e062a
commit
d32522bcbb
4 files changed
+233
-22
No files matched your search
@@ -40,6 +40,10 @@ if (dbType === 'mysql' || dbType === 'mariadb') {
|
||||
// 2. ILIKE -> LIKE (MySQL LIKE is case-insensitive by default)
|
||||
translatedSql = translatedSql.replace(/\bILIKE\b/gi, 'LIKE');
|
||||
|
||||
// 2b. PostgreSQL regex ~* / ~ -> REGEXP
|
||||
translatedSql = translatedSql.replace(/~\*/g, 'REGEXP');
|
||||
translatedSql = translatedSql.replace(/~/g, 'REGEXP');
|
||||
|
||||
// 3. PostgreSQL string concatenation '||' -> CONCAT(...) in ticket number generator
|
||||
translatedSql = translatedSql.replace(/md5\(random\(\)::text\s*\|\|\s*clock_timestamp\(\)::text\)/gi, 'MD5(CONCAT(RAND(), NOW()))');
|
||||
|
||||
|
||||
@@ -3063,3 +3063,31 @@ body {
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Ricerca Spudorata - Info Modal / Popover & Regex */
|
||||
#spudorata-info-btn:hover {
|
||||
background: var(--accent-primary) !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 0 10px var(--accent-primary-glow);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.spudorata-rules-popover {
|
||||
animation: popoverFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes popoverFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#spudorata-regex-toggle:hover {
|
||||
border-color: var(--accent-primary) !important;
|
||||
background: var(--bg-card-hover) !important;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
const RicercaSpudorataView = {
|
||||
searchTerm: '',
|
||||
isRegex: false,
|
||||
cachedTickets: null,
|
||||
currentPage: 1,
|
||||
perPage: 50,
|
||||
@@ -29,6 +30,10 @@ const RicercaSpudorataView = {
|
||||
|
||||
const urlParams = new URLSearchParams((window.location.hash.split('?')[1]) || '');
|
||||
const qParam = urlParams.get('q');
|
||||
const regexParam = urlParams.get('regex');
|
||||
if (regexParam !== null) {
|
||||
this.isRegex = regexParam === '1' || regexParam === 'true';
|
||||
}
|
||||
|
||||
if (qParam && qParam !== this.searchTerm) {
|
||||
this.renderContainer(container);
|
||||
@@ -62,12 +67,54 @@ const RicercaSpudorataView = {
|
||||
renderContainer(container) {
|
||||
container.innerHTML = `
|
||||
<!-- Hero Text Search Area -->
|
||||
<div class="ricerca-spudorata-hero" style="background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-lg); margin-bottom: var(--space-lg); box-shadow: var(--shadow-md);">
|
||||
<div class="ricerca-spudorata-hero" style="background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-lg); margin-bottom: var(--space-lg); box-shadow: var(--shadow-md); position: relative;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-sm); flex-wrap:wrap; gap:10px;">
|
||||
<div style="display:flex; align-items:center; gap: 10px;">
|
||||
<div style="display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; background: linear-gradient(135deg, rgba(235, 114, 110, 0.2), rgba(160, 65, 71, 0.3)); border-radius: 50%; color: var(--accent-primary); font-size:1.2rem; font-weight:bold;">🚀</div>
|
||||
<div>
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);">Ricerca Testuale Spudorata</h2>
|
||||
<div style="display:flex; align-items:center; gap: 8px;">
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);">Ricerca Testuale Spudorata</h2>
|
||||
<div style="position: relative; display: inline-flex; align-items: center;">
|
||||
<button type="button" id="spudorata-info-btn" aria-label="Informazioni sulle regole avanzate e Regex" style="background: rgba(79, 70, 229, 0.1); border: 1px solid rgba(79, 70, 229, 0.25); border-radius: 50%; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; color: var(--accent-primary); padding: 0; transition: all 0.2s ease;">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Modal / Popover indicazioni regole avanzate -->
|
||||
<div id="spudorata-info-modal" class="spudorata-rules-popover" style="display:none; position:absolute; top:calc(100% + 10px); left:0; width:460px; max-width:90vw; background:var(--bg-card); border:1px solid var(--border-light); border-radius:var(--radius-lg); box-shadow:0 12px 32px rgba(0,0,0,0.22); z-index:1100; padding:18px; font-size:0.85rem; color:var(--text-primary); backdrop-filter:blur(12px);">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; border-bottom:1px solid var(--border-subtle); padding-bottom:8px;">
|
||||
<div style="display:flex; align-items:center; gap:6px; font-weight:700; font-size:0.95rem; color:var(--accent-primary);">
|
||||
<span>💡</span>
|
||||
<span>Guida Ricerca & Regole Avanzate (Regex)</span>
|
||||
</div>
|
||||
<span style="font-size:0.75rem; color:var(--text-muted); background:var(--bg-tertiary); padding:2px 6px; border-radius:4px;">Regole avanzate</span>
|
||||
</div>
|
||||
|
||||
<div style="line-height:1.45; color:var(--text-secondary); margin-bottom:12px;">
|
||||
Puoi cercare normalmente per parole chiave o attivare l'opzione <strong>Regex</strong> per ricerche con espressioni regolari (POSIX / JavaScript) sia sul database che sui filtri in cache.
|
||||
</div>
|
||||
|
||||
<div style="background:var(--bg-tertiary); border:1px solid var(--border-subtle); border-radius:var(--radius-md); padding:10px 12px; margin-bottom:12px;">
|
||||
<div style="font-weight:600; color:var(--text-primary); margin-bottom:6px; font-size:0.82rem;">Esempi di espressioni regolari:</div>
|
||||
<ul style="margin:0; padding-left:18px; display:flex; flex-direction:column; gap:6px;">
|
||||
<li><code style="background:rgba(79,70,229,0.12); color:var(--accent-primary); padding:1px 5px; border-radius:3px; font-family:monospace;">vpn|firewall</code> — Trova ticket contenenti <em>vpn</em> <strong>oppure</strong> <em>firewall</em></li>
|
||||
<li><code style="background:rgba(79,70,229,0.12); color:var(--accent-primary); padding:1px 5px; border-radius:3px; font-family:monospace;">\b(errore|fatal)\b</code> — Trova parole esatte "errore" o "fatal"</li>
|
||||
<li><code style="background:rgba(79,70,229,0.12); color:var(--accent-primary); padding:1px 5px; border-radius:3px; font-family:monospace;">^INC-[0-9]+</code> — Ticket o codici che iniziano per "INC-" seguito da cifre</li>
|
||||
<li><code style="background:rgba(79,70,229,0.12); color:var(--accent-primary); padding:1px 5px; border-radius:3px; font-family:monospace;">host(name)?.*down</code> — Trova "host down" o "hostname down" anche con parole in mezzo</li>
|
||||
<li><code style="background:rgba(79,70,229,0.12); color:var(--accent-primary); padding:1px 5px; border-radius:3px; font-family:monospace;">\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}</code> — Trova indirizzi IP nel corpo o nell'oggetto</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="font-size:0.78rem; color:var(--text-tertiary); display:flex; flex-direction:column; gap:4px;">
|
||||
<div>⚡ <strong>Case-insensitive:</strong> Le ricerche con Regex non distinguono automaticamente tra maiuscole e minuscole.</div>
|
||||
<div>⚡ <strong>Cache locale:</strong> Anche il campo <em>"Cerca in Cache"</em> rispetta la modalità Regex o il formato <code>/pattern/</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin:2px 0 0 0; font-size:0.82rem; color:var(--text-secondary);">
|
||||
Esegui la ricerca nel testo sui ticket. I filtri applicati successivamente operano <strong>istantaneamente in cache locale</strong> senza fare nuove richieste al server.
|
||||
</p>
|
||||
@@ -78,11 +125,19 @@ const RicercaSpudorataView = {
|
||||
|
||||
<div style="display:flex; align-items:center; gap: var(--space-sm); flex-wrap:wrap; margin-top: var(--space-md);">
|
||||
<div style="position:relative; flex:1; min-width:300px;">
|
||||
<input type="text" id="spudorata-search-input" class="form-input" placeholder="Cerca nel testo (oggetto, corpo, numero ticket, cliente)..." value="${App.escapeHtml(this.searchTerm)}" style="width:100%; height:42px; font-size:0.95rem; padding-left:14px; padding-right:36px; border-radius:var(--radius-md);" />
|
||||
<input type="text" id="spudorata-search-input" class="form-input" placeholder="${this.isRegex ? 'Cerca con espressione regolare (es. vpn|firewall, ^INC-\\d+)...' : 'Cerca nel testo (oggetto, corpo, numero ticket, cliente)...'}" value="${App.escapeHtml(this.searchTerm)}" style="width:100%; height:42px; font-size:0.95rem; padding-left:14px; padding-right:36px; border-radius:var(--radius-md);" />
|
||||
<button id="spudorata-input-clear" style="position:absolute; right:10px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--text-muted); cursor:pointer; display:${this.searchTerm ? 'flex' : 'none'}; align-items:center; justify-content:center;" title="Cancella testo">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Regex -->
|
||||
<label id="spudorata-regex-toggle" style="display:inline-flex; align-items:center; gap:6px; cursor:pointer; user-select:none; height:42px; padding:0 12px; background:var(--bg-secondary); border:1px solid ${this.isRegex ? 'var(--accent-primary)' : 'var(--border-light)'}; border-radius:var(--radius-md); font-size:0.85rem; font-weight:600; color:${this.isRegex ? 'var(--accent-primary)' : 'var(--text-secondary)'}; transition:all 0.15s ease;" title="Abilita ricerca tramite Regular Expression">
|
||||
<input type="checkbox" id="spudorata-regex-checkbox" ${this.isRegex ? 'checked' : ''} style="cursor:pointer; accent-color:var(--accent-primary);" />
|
||||
<span style="font-family:monospace; font-size:0.95rem; font-weight:700;">.*</span>
|
||||
<span>Regex</span>
|
||||
</label>
|
||||
|
||||
<button class="btn btn-primary" id="spudorata-search-btn" style="height:42px; padding:0 var(--space-xl); font-size:0.9rem; font-weight:600; min-width:130px; display:flex; align-items:center; justify-content:center; gap:6px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<span>Cerca</span>
|
||||
@@ -194,6 +249,15 @@ const RicercaSpudorataView = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRegex) {
|
||||
try {
|
||||
new RegExp(query);
|
||||
} catch (e) {
|
||||
Toast.error('Espressione regolare non valida: ' + e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.searchTerm = query;
|
||||
this.isFetching = true;
|
||||
|
||||
@@ -202,13 +266,14 @@ const RicercaSpudorataView = {
|
||||
resultsArea.innerHTML = `
|
||||
<div class="loading-screen" style="padding: 60px 0;">
|
||||
<div class="spinner"></div>
|
||||
<p>Ricerca testuale nel server in corso per "${App.escapeHtml(query)}"...</p>
|
||||
<p>Ricerca testuale ${this.isRegex ? 'con Regex ' : ''}nel server in corso per "${App.escapeHtml(query)}"...</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await App.api(`/api/tickets?search=${encodeURIComponent(query)}&per_page=2000`);
|
||||
const regexParam = this.isRegex ? '&is_regex=1' : '';
|
||||
const data = await App.api(`/api/tickets?search=${encodeURIComponent(query)}${regexParam}&per_page=2000`);
|
||||
this.cachedTickets = data.tickets || [];
|
||||
this.otrsBaseUrl = data.otrsBaseUrl || '';
|
||||
this.hasSearched = true;
|
||||
@@ -216,7 +281,7 @@ const RicercaSpudorataView = {
|
||||
this.selectedIds.clear();
|
||||
this.selectedOrder = [];
|
||||
|
||||
Toast.success(`${this.cachedTickets.length} ticket caricati in cache locale per "${query}"`);
|
||||
Toast.success(`${this.cachedTickets.length} ticket caricati in cache locale per "${query}"${this.isRegex ? ' (Regex)' : ''}`);
|
||||
this.renderContainer(document.getElementById('view-container'));
|
||||
this.bindEvents();
|
||||
this.renderLocalResults();
|
||||
@@ -361,8 +426,22 @@ const RicercaSpudorataView = {
|
||||
const s = Filters.state;
|
||||
|
||||
if (s.sub_search) {
|
||||
const q = String(s.sub_search).toLowerCase().trim();
|
||||
if (q) {
|
||||
const rawQ = String(s.sub_search).trim();
|
||||
if (rawQ) {
|
||||
let regexMatcher = null;
|
||||
if (this.isRegex) {
|
||||
try {
|
||||
regexMatcher = new RegExp(rawQ, 'i');
|
||||
} catch (e) {
|
||||
// If invalid while typing, fallback or ignore
|
||||
}
|
||||
} else if (rawQ.startsWith('/') && rawQ.length > 2 && rawQ.endsWith('/')) {
|
||||
try {
|
||||
regexMatcher = new RegExp(rawQ.slice(1, -1), 'i');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const qLower = rawQ.toLowerCase();
|
||||
list = list.filter(t => {
|
||||
const fields = [
|
||||
t.tn,
|
||||
@@ -379,7 +458,10 @@ const RicercaSpudorataView = {
|
||||
t.customer_id,
|
||||
t.customer_user_id
|
||||
];
|
||||
return fields.some(f => f && String(f).toLowerCase().includes(q));
|
||||
if (regexMatcher) {
|
||||
return fields.some(f => f && regexMatcher.test(String(f)));
|
||||
}
|
||||
return fields.some(f => f && String(f).toLowerCase().includes(qLower));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -529,6 +611,78 @@ const RicercaSpudorataView = {
|
||||
});
|
||||
}
|
||||
|
||||
const regexCheckbox = document.getElementById('spudorata-regex-checkbox');
|
||||
if (regexCheckbox) {
|
||||
regexCheckbox.addEventListener('change', () => {
|
||||
this.isRegex = regexCheckbox.checked;
|
||||
const toggleLabel = document.getElementById('spudorata-regex-toggle');
|
||||
if (toggleLabel) {
|
||||
toggleLabel.style.borderColor = this.isRegex ? 'var(--accent-primary)' : 'var(--border-light)';
|
||||
toggleLabel.style.color = this.isRegex ? 'var(--accent-primary)' : 'var(--text-secondary)';
|
||||
}
|
||||
if (searchInput) {
|
||||
searchInput.placeholder = this.isRegex
|
||||
? 'Cerca con espressione regolare (es. vpn|firewall, ^INC-\\d+)...'
|
||||
: 'Cerca nel testo (oggetto, corpo, numero ticket, cliente)...';
|
||||
searchInput.focus();
|
||||
}
|
||||
// If we already have cached tickets, update the local filtered view immediately
|
||||
if (this.hasSearched && this.cachedTickets) {
|
||||
this.currentPage = 1;
|
||||
this.renderLocalResults();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Info button & hover modal
|
||||
const infoBtn = document.getElementById('spudorata-info-btn');
|
||||
const infoModal = document.getElementById('spudorata-info-modal');
|
||||
if (infoBtn && infoModal) {
|
||||
let closeTimeout = null;
|
||||
|
||||
const openModal = () => {
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout);
|
||||
closeTimeout = null;
|
||||
}
|
||||
infoModal.style.display = 'block';
|
||||
};
|
||||
|
||||
const closeModalWithDelay = () => {
|
||||
if (closeTimeout) clearTimeout(closeTimeout);
|
||||
closeTimeout = setTimeout(() => {
|
||||
infoModal.style.display = 'none';
|
||||
}, 250);
|
||||
};
|
||||
|
||||
infoBtn.addEventListener('mouseenter', openModal);
|
||||
infoBtn.addEventListener('mouseleave', closeModalWithDelay);
|
||||
|
||||
infoModal.addEventListener('mouseenter', () => {
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout);
|
||||
closeTimeout = null;
|
||||
}
|
||||
});
|
||||
infoModal.addEventListener('mouseleave', closeModalWithDelay);
|
||||
|
||||
// Also toggle on click for touch/mobile devices or explicit clicks
|
||||
infoBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (infoModal.style.display === 'block') {
|
||||
infoModal.style.display = 'none';
|
||||
} else {
|
||||
openModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!infoBtn.contains(e.target) && !infoModal.contains(e.target)) {
|
||||
infoModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resetCacheBtn) {
|
||||
resetCacheBtn.addEventListener('click', () => {
|
||||
this.invalidateCache();
|
||||
|
||||
+38
-13
@@ -212,20 +212,45 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
const searchTerm = search || sub_search;
|
||||
if (searchTerm) {
|
||||
conditions.push(`(
|
||||
t.title ILIKE $${paramIdx} OR
|
||||
t.tn ILIKE $${paramIdx} OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM article art
|
||||
LEFT JOIN article_data_mime adm ON art.id = adm.article_id
|
||||
WHERE art.ticket_id = t.id AND (
|
||||
adm.a_subject ILIKE $${paramIdx} OR
|
||||
adm.a_body ILIKE $${paramIdx}
|
||||
const isRegex = req.query.is_regex === 'true' || req.query.is_regex === '1';
|
||||
if (isRegex) {
|
||||
// Validate regex pattern to prevent DB syntax error
|
||||
try {
|
||||
new RegExp(searchTerm);
|
||||
} catch (regexErr) {
|
||||
return res.status(400).json({ error: `Espressione regolare non valida: ${regexErr.message}` });
|
||||
}
|
||||
|
||||
conditions.push(`(
|
||||
t.title ~* $${paramIdx} OR
|
||||
t.tn ~* $${paramIdx} OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM article art
|
||||
LEFT JOIN article_data_mime adm ON art.id = adm.article_id
|
||||
WHERE art.ticket_id = t.id AND (
|
||||
adm.a_subject ~* $${paramIdx} OR
|
||||
adm.a_body ~* $${paramIdx}
|
||||
)
|
||||
)
|
||||
)
|
||||
)`);
|
||||
params.push(`%${searchTerm}%`);
|
||||
paramIdx++;
|
||||
)`);
|
||||
params.push(searchTerm);
|
||||
paramIdx++;
|
||||
} else {
|
||||
conditions.push(`(
|
||||
t.title ILIKE $${paramIdx} OR
|
||||
t.tn ILIKE $${paramIdx} OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM article art
|
||||
LEFT JOIN article_data_mime adm ON art.id = adm.article_id
|
||||
WHERE art.ticket_id = t.id AND (
|
||||
adm.a_subject ILIKE $${paramIdx} OR
|
||||
adm.a_body ILIKE $${paramIdx}
|
||||
)
|
||||
)
|
||||
)`);
|
||||
params.push(`%${searchTerm}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0
|
||||
|
||||
Reference in new issue
Block a user