- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.0 KiB
JavaScript
81 lines
2.0 KiB
JavaScript
const readStored = (storageKey) => {
|
|
try {
|
|
const raw = window.localStorage.getItem(storageKey);
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const writeStored = (storageKey, openKeys) => {
|
|
try {
|
|
window.localStorage.setItem(storageKey, JSON.stringify(openKeys));
|
|
} catch (error) {
|
|
// ignore storage errors
|
|
}
|
|
};
|
|
|
|
const initDetailsGroup = (group) => {
|
|
if (!group || group.dataset.detailsStorageBound === '1') {
|
|
return;
|
|
}
|
|
|
|
const storageKey = (group.dataset.detailsStorage || '').trim();
|
|
if (!storageKey) {
|
|
return;
|
|
}
|
|
|
|
const managedDetails = Array.from(group.querySelectorAll('details[data-details-key]'));
|
|
const alwaysOpenDetails = Array.from(group.querySelectorAll('details[data-details-always-open]'));
|
|
|
|
if (!managedDetails.length && !alwaysOpenDetails.length) {
|
|
return;
|
|
}
|
|
|
|
const storedOpenKeys = readStored(storageKey);
|
|
if (storedOpenKeys.length > 0) {
|
|
managedDetails.forEach((details) => {
|
|
const key = (details.dataset.detailsKey || '').trim();
|
|
if (key !== '') {
|
|
details.open = storedOpenKeys.includes(key);
|
|
}
|
|
});
|
|
}
|
|
|
|
alwaysOpenDetails.forEach((details) => {
|
|
details.open = true;
|
|
details.addEventListener('toggle', () => {
|
|
if (!details.open) {
|
|
details.open = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
managedDetails.forEach((details) => {
|
|
details.addEventListener('toggle', () => {
|
|
const openKeys = managedDetails
|
|
.filter((item) => item.open)
|
|
.map((item) => (item.dataset.detailsKey || '').trim())
|
|
.filter(Boolean);
|
|
writeStored(storageKey, openKeys);
|
|
});
|
|
});
|
|
|
|
group.dataset.detailsStorageBound = '1';
|
|
};
|
|
|
|
const initDetailsState = () => {
|
|
const groups = Array.from(document.querySelectorAll('[data-details-storage]'));
|
|
groups.forEach(initDetailsGroup);
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initDetailsState);
|
|
} else {
|
|
initDetailsState();
|
|
}
|