sqlinsight-pro/src/SQLInsight.Web/wwwroot/support-menu.js
Søren Eggert Lundsteen Olsen 50bc878776 Customers file support cases in SQLInsight Web and follow their status
- Repository migration 0005: insight.SupportCases. The web UI has only
  db_datareader, so INSERT on this one table is granted to public.
- Web: /support (menu "Support") lists the customer's cases and, with
  Hub sync on and a Professional/Enterprise license, a form (title,
  description, contact; contact prefilled from the Windows user).
  /support?fund=<id> prefills from a finding: instance, rule, severity,
  explanation and non-sensitive measurements.
- "Opret supportsag" on findings: right-click a finding link or a
  finding's heading (support-menu.js, a static file within the CSP;
  Shift+F10/context key, Escape closes), and a visible button on the
  finding page that works without JavaScript. Only when support is
  available.
- Agent: sends pending requests to POST /api/v1/cases, stores the case
  number, backs off on failures and gives up on 400/401/403/422; fetches
  the customer's cases every 5 minutes (at once after sending).
- Docs: support.md (also on the community site), databehandling.md
  (contact details are sent to the hub).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:34:06 +02:00

78 lines
2.9 KiB
JavaScript

// "Opret supportsag" in a context menu on findings: right-click a link to /fund/<id> (any finding list) or the
// heading of a finding's own page. Keyboard: Shift+F10 or the context menu key on a focused finding; Escape closes.
// Only when the layout says support is available (data-support-available). Without JavaScript the finding page has a
// visible "Opret supportsag" button, so nothing depends on this file.
(() => {
const available = () => document.querySelector('[data-support-available]') !== null;
let menu = null;
let opener = null;
const findingId = (element) => {
const marked = element.closest('[data-finding-id]');
if (marked) return marked.dataset.findingId;
const link = element.closest('a[href]');
const match = link && /^\/fund\/(\d+)$/.exec(new URL(link.href, document.baseURI).pathname);
return match ? match[1] : null;
};
const close = (restoreFocus) => {
if (!menu) return;
menu.remove();
menu = null;
if (restoreFocus && opener) opener.focus();
opener = null;
};
const open = (id, x, y, target) => {
close(false);
opener = target.closest('a[href], [tabindex]') || target;
menu = document.createElement('div');
menu.className = 'context-menu';
menu.setAttribute('role', 'menu');
menu.setAttribute('aria-label', 'Fund');
const item = document.createElement('a');
item.setAttribute('role', 'menuitem');
item.href = `/support?fund=${encodeURIComponent(id)}`;
item.textContent = 'Opret supportsag';
menu.appendChild(item);
document.body.appendChild(menu);
// Keep it inside the window. Setting style through the CSSOM is allowed by the page's Content-Security-Policy.
const width = menu.offsetWidth;
const height = menu.offsetHeight;
menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - width - 4)) + window.scrollX}px`;
menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - height - 4)) + window.scrollY}px`;
item.focus();
};
document.addEventListener('contextmenu', (event) => {
if (!available()) return;
const id = findingId(event.target);
if (!id) return;
event.preventDefault();
// From the keyboard there is no pointer position: place the menu under the focused element.
let { clientX: x, clientY: y } = event;
if (x === 0 && y === 0) {
const box = event.target.getBoundingClientRect();
x = box.left;
y = box.bottom + 4;
}
open(id, x, y, event.target);
});
document.addEventListener('keydown', (event) => {
if (!menu) return;
if (event.key === 'Escape') {
event.preventDefault();
close(true);
} else if (event.key === 'Tab') {
close(false);
}
});
document.addEventListener('mousedown', (event) => {
if (menu && !menu.contains(event.target)) close(false);
});
window.addEventListener('scroll', () => close(false), { passive: true });
window.addEventListener('resize', () => close(false));
})();