feat(helpdesk): align module with core list/drawer standards

- add helpdesk module pages, services, settings and tests

- standardize debtor list on drawer/grid contracts and robust filter drawer behavior

- add helpdesk aside panel navigation and settings visibility provider

- switch primary list slug to helpdesk/debitor and remove helpdesk/search compatibility

- include required core contract updates for list contracts and detail/drawer integration
This commit is contained in:
2026-04-02 17:48:27 +02:00
parent 5d07236758
commit a0d7670dd7
55 changed files with 5977 additions and 11 deletions

View File

@@ -36,6 +36,8 @@ export function initFilterDrawer(options = {}) {
const applyBaseLabel = applyButton ? String(applyButton.textContent || '').trim() : '';
const openButtonBaseLabels = new WeakMap();
const panel = drawer.querySelector('[data-filter-drawer-panel]') || drawer;
const originalParent = drawer.parentNode;
const originalNextSibling = drawer.nextSibling;
const cleanupFns = [];
const bind = (target, eventName, handler, bindOptions = undefined) => {
@@ -47,6 +49,29 @@ export function initFilterDrawer(options = {}) {
let lockedScrollY = 0;
let lastTrigger = null;
const ensureDrawerInBody = () => {
if (!document.body || !drawer.isConnected || drawer.parentNode === document.body) {
return;
}
document.body.appendChild(drawer);
};
const restoreDrawerPosition = () => {
if (!(originalParent instanceof Node) || !drawer.isConnected) {
return;
}
if (!originalParent.isConnected || drawer.parentNode === originalParent) {
return;
}
if (originalNextSibling && originalNextSibling.parentNode === originalParent) {
originalParent.insertBefore(drawer, originalNextSibling);
return;
}
originalParent.appendChild(drawer);
};
ensureDrawerInBody();
if (!drawer.hasAttribute('role')) {
drawer.setAttribute('role', 'dialog');
}
@@ -115,6 +140,9 @@ export function initFilterDrawer(options = {}) {
};
const setOpen = (nextOpen) => {
if (nextOpen) {
ensureDrawerInBody();
}
isOpen = nextOpen;
drawer.hidden = !nextOpen;
drawer.setAttribute('aria-hidden', nextOpen ? 'false' : 'true');
@@ -148,6 +176,21 @@ export function initFilterDrawer(options = {}) {
focusFirstField();
};
const normalizeForcedClose = () => {
if (!isOpen) {
return;
}
isOpen = false;
unlockPageScroll();
document.body.classList.remove('filter-drawer-open');
openButtons.forEach((button) => {
button.setAttribute('aria-expanded', 'false');
});
if (typeof onClose === 'function') {
onClose();
}
};
const open = (trigger = null) => {
if (isOpen) {
return;
@@ -254,6 +297,23 @@ export function initFilterDrawer(options = {}) {
};
bind(document, 'keydown', onKeyDown);
const documentObserver = new MutationObserver(() => {
if (!drawer.isConnected || drawer.hidden) {
normalizeForcedClose();
}
});
if (document.body) {
documentObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(() => documentObserver.disconnect());
}
const drawerAttributeObserver = new MutationObserver(() => {
if (drawer.hidden) {
normalizeForcedClose();
}
});
drawerAttributeObserver.observe(drawer, { attributes: true, attributeFilter: ['hidden'] });
cleanupFns.push(() => drawerAttributeObserver.disconnect());
const setApplyEnabled = (enabled) => {
if (!applyButton) {
return;
@@ -314,6 +374,9 @@ export function initFilterDrawer(options = {}) {
if (isOpen) {
close('discard');
}
unlockPageScroll();
document.body.classList.remove('filter-drawer-open');
restoreDrawerPosition();
delete drawer.dataset.filterDrawerBound;
delete drawer._filterDrawerApi;
};

View File

@@ -1370,6 +1370,35 @@ const normalizeListMode = (value) => {
return 'drawer';
};
const resolveDrawerRoot = (gridConfig, filterOptions = {}) => {
const explicitRoot = filterOptions?.drawerRoot;
if (explicitRoot) {
return resolveDomQueryElement(explicitRoot) || document;
}
const containerEl = gridConfig?.container instanceof Element
? gridConfig.container
: null;
if (!containerEl) {
return document;
}
const drawerSelector = String(filterOptions?.drawer?.drawerSelector || '[data-filter-drawer]').trim() || '[data-filter-drawer]';
const openSelector = String(filterOptions?.drawer?.openSelector || '[data-filter-drawer-open]').trim() || '[data-filter-drawer-open]';
let current = containerEl;
while (current && current !== document.documentElement) {
const hasDrawer = Boolean(current.querySelector(drawerSelector));
const hasOpenButton = Boolean(current.querySelector(openSelector));
if (hasDrawer && hasOpenButton) {
return current;
}
current = current.parentElement;
}
return containerEl.parentElement || document;
};
/**
* Standardized list-page bootstrap:
* - builds grid filters from schema (or uses explicit filters)
@@ -1425,7 +1454,7 @@ export function initStandardListPage(options = {}) {
const chipUiConfig = filterOptions.chips && typeof filterOptions.chips === 'object'
? filterOptions.chips
: {};
const drawerRoot = filterOptions.drawerRoot || document;
const drawerRoot = resolveDrawerRoot(gridConfig, filterOptions);
const domLabels = readDomLabelDefaults({
drawerRoot,
chipsSelector: chipUiConfig.container || '[data-active-filter-chips]',