feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
This commit is contained in:
@@ -20,7 +20,10 @@ body {
|
||||
.admin-navbar {
|
||||
border-bottom: 1px solid rgba(255,255,255,.07);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.4);
|
||||
height: 52px;
|
||||
min-height: 52px;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.admin-navbar .navbar-brand {
|
||||
@@ -29,6 +32,27 @@ body {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.admin-api-documentation {
|
||||
margin-left: auto;
|
||||
padding-block: .25rem;
|
||||
color: #f8f9fa;
|
||||
font-size: .85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-api-documentation:hover { color: #fff; }
|
||||
|
||||
.admin-api-documentation:focus-visible {
|
||||
outline: 2px solid #f8f9fa;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.admin-navbar { padding-inline: .5rem; }
|
||||
.admin-navbar .navbar-brand { margin-right: 0; font-size: .85rem; }
|
||||
.admin-api-documentation { font-size: .8rem; }
|
||||
}
|
||||
|
||||
/* ── Sidebar ──────────────────────────────────────────── */
|
||||
.sidebar {
|
||||
min-height: calc(100vh - 52px);
|
||||
@@ -109,21 +133,44 @@ body {
|
||||
.table tbody tr td { vertical-align: middle; }
|
||||
.table thead th { font-size: .75rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
|
||||
|
||||
/* ── Administration list grids ────────────────────────── */
|
||||
.admin-data-table { border: 1px solid #b8c4d3; }
|
||||
|
||||
.admin-data-table :is(a, button, input, select, textarea) { scroll-margin-inline: .5rem; }
|
||||
|
||||
.admin-data-table > thead > tr > th {
|
||||
--bs-table-bg: #dce7f6;
|
||||
--bs-table-color: #243a55;
|
||||
border-bottom: 2px solid #aec0db;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-data-table > tbody > tr > td:first-child { overflow-wrap: anywhere; }
|
||||
|
||||
.admin-data-table > :not(caption) > * > * + * {
|
||||
border-left: 1px solid #b8c4d3;
|
||||
}
|
||||
|
||||
.admin-data-table .list-action-links { display: inline-flex; align-items: center; gap: .375rem; }
|
||||
.admin-delete-confirmation { overflow-wrap: anywhere; }
|
||||
|
||||
/* ── List actions ─────────────────────────────────────── */
|
||||
.table .template-actions,
|
||||
.table .service-actions {
|
||||
.admin-data-table .template-actions,
|
||||
.admin-data-table .service-actions,
|
||||
.admin-data-table .channel-actions {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ── Service template catalog and filters ─────────────── */
|
||||
.service-template-grid > thead > tr > th {
|
||||
--bs-table-bg: #dce7f6;
|
||||
--bs-table-color: #243a55;
|
||||
border-bottom: 2px solid #aec0db;
|
||||
.page-header .service-template-service-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.service-template-service-name + .btn { flex-shrink: 0; }
|
||||
|
||||
/* ── Service template catalog and filters ─────────────── */
|
||||
.service-template-grid .template-key-filter .btn-outline-secondary {
|
||||
--bs-btn-bg: #fff;
|
||||
}
|
||||
@@ -154,6 +201,7 @@ body {
|
||||
|
||||
.template-key-filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: .375rem;
|
||||
}
|
||||
@@ -302,7 +350,9 @@ body {
|
||||
}
|
||||
|
||||
/* ── Template service context ─────────────────────────── */
|
||||
.template-editor-breadcrumbs .breadcrumb-item {
|
||||
.template-editor-breadcrumbs .breadcrumb-item,
|
||||
.service-template-breadcrumbs .breadcrumb-item,
|
||||
.service-editor-breadcrumbs .breadcrumb-item {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -473,7 +523,7 @@ body {
|
||||
|
||||
.template-preview-frame {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
height: 80px;
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: .5rem;
|
||||
background: #eef2f7;
|
||||
@@ -495,9 +545,6 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.template-preview-frame {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Form-section divider ─────────────────────────────── */
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Reveal keyboard-focused controls only within their nearest horizontal table scroller.
|
||||
(() => {
|
||||
let keyboardFocus = false;
|
||||
document.addEventListener('keydown', () => { keyboardFocus = true; }, true);
|
||||
document.addEventListener('pointerdown', () => { keyboardFocus = false; }, true);
|
||||
document.addEventListener('focusin', event => {
|
||||
const target = event.target;
|
||||
if (!keyboardFocus || !(target instanceof HTMLElement) || !target.closest('.admin-data-table')) return;
|
||||
const wrapper = target.closest('.table-responsive');
|
||||
if (!wrapper) return;
|
||||
const control = target.getBoundingClientRect();
|
||||
const viewport = wrapper.getBoundingClientRect();
|
||||
// Leave space for the focus ring without moving the document or vertical ancestors.
|
||||
const left = viewport.left + wrapper.clientLeft + 8;
|
||||
const right = left + wrapper.clientWidth - 16;
|
||||
if (control.left < left) wrapper.scrollLeft += control.left - left;
|
||||
else if (control.right > right) wrapper.scrollLeft += control.right - right;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,40 @@
|
||||
// Trusted parent-side measurement; template scripts stay disabled by sandbox and CSP.
|
||||
(() => {
|
||||
const frame = document.getElementById('previewHtmlFrame');
|
||||
let observer;
|
||||
let scheduled = 0;
|
||||
|
||||
function resizePreview() {
|
||||
scheduled = 0;
|
||||
const document = frame.contentDocument;
|
||||
if (!document?.body) return;
|
||||
frame.style.height = '0px';
|
||||
const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
frame.style.height = `${Math.max(80, Math.ceil(height) + 2)}px`;
|
||||
}
|
||||
|
||||
function scheduleResize() {
|
||||
cancelAnimationFrame(scheduled);
|
||||
scheduled = requestAnimationFrame(resizePreview);
|
||||
}
|
||||
|
||||
function preventLinkNavigation(event) {
|
||||
if (event.target?.closest?.('a, area')) event.preventDefault();
|
||||
}
|
||||
|
||||
frame.addEventListener('load', () => {
|
||||
observer?.disconnect();
|
||||
const document = frame.contentDocument;
|
||||
if (!document?.body) return;
|
||||
document.addEventListener('click', preventLinkNavigation, true);
|
||||
document.addEventListener('submit', event => event.preventDefault(), true);
|
||||
observer = new ResizeObserver(scheduleResize);
|
||||
observer.observe(document.body);
|
||||
scheduleResize();
|
||||
});
|
||||
window.addEventListener('resize', scheduleResize);
|
||||
window.addEventListener('pagehide', () => {
|
||||
observer?.disconnect();
|
||||
cancelAnimationFrame(scheduled);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user