From 92f25cee29a0e8cdab6e6dea91c715964821c53d Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 15 Jul 2026 19:11:34 +0200 Subject: [PATCH] refactor: decouple suite infrastructure --- AGENTS.md | 7 +- CHANGELOG.md | 6 ++ README.md | 11 +- appinfo/info.xml | 3 +- appinfo/routes.php | 3 - css/admin.css | 43 -------- js/admin.js | 78 -------------- js/components/hierarchy-board.js | 114 -------------------- js/components/organization-editor.js | 94 ---------------- lib/AppInfo/Application.php | 6 +- lib/Controller/AdminApiController.php | 75 ------------- lib/Listener/SuiteAssetsListener.php | 23 ++++ lib/Settings/Admin.php | 2 +- templates/admin.php | 40 ------- tests/AdminApiControllerExecutionTest.php | 81 -------------- tests/AdminSettingsContractTest.php | 28 ++--- tests/AdminSettingsExecutionTest.php | 44 ++++++++ tests/ApplicationBootstrapExecutionTest.php | 43 ++++++++ tests/SuiteAssetsListenerExecutionTest.php | 33 ++++++ tests/http-smoke.sh | 6 +- tests/js/admin-settings-smoke.mjs | 28 ----- tests/run-js.mjs | 4 - 22 files changed, 173 insertions(+), 599 deletions(-) delete mode 100644 css/admin.css delete mode 100644 js/admin.js delete mode 100644 js/components/hierarchy-board.js delete mode 100644 js/components/organization-editor.js delete mode 100644 lib/Controller/AdminApiController.php create mode 100644 lib/Listener/SuiteAssetsListener.php delete mode 100644 templates/admin.php delete mode 100644 tests/AdminApiControllerExecutionTest.php create mode 100644 tests/AdminSettingsExecutionTest.php create mode 100644 tests/ApplicationBootstrapExecutionTest.php create mode 100644 tests/SuiteAssetsListenerExecutionTest.php delete mode 100644 tests/js/admin-settings-smoke.mjs diff --git a/AGENTS.md b/AGENTS.md index 8f23ff1..5848c84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,15 +20,16 @@ OrgSuite stellt genau zwei Haupteinstiege im Nextcloud-Appmenue bereit: - `AD` fuer AD Kalender, Assistenzplanung, AD Urlaub und AD Raumplaner. - `BR` fuer BRTop, BR-Stunden und Berechtigungsmatrix. -Die Fachapps bleiben eigenständige Repositories, Datenmodelle und Berechtigungsräume. OrgSuite besitzt keine Fachdaten und erweitert keine fachlichen Rechte. Zielapps erzwingen ihre Berechtigungen weiterhin serverseitig. OrgSuite stellt ausschließlich den Nextcloud-Adminadapter für gemeinsame, in LocalBase persistierte Organisations- und Freigabeverträge bereit. Einstellungen, die nur eine Fachapp betreffen, erhalten einen eigenen Adminabschnitt in dieser Fachapp. +Die Fachapps bleiben eigenständige Repositories, Datenmodelle und Berechtigungsräume. OrgSuite besitzt keine Fachdaten und erweitert keine fachlichen Rechte. Zielapps erzwingen ihre Berechtigungen weiterhin serverseitig. OrgSuite stellt ab zwei AD-Fachprodukten ausschließlich Navigation, gemeinsame Assets und den Nextcloud-Adminadapter für in LocalBase persistierte Organisations- und Freigabeverträge bereit. Einstellungen, die nur eine Fachapp betreffen, erhalten einen eigenen Adminabschnitt in dieser Fachapp. ## Navigationsvertrag - Die Haupteinstiege werden dynamisch registriert und nur angezeigt, wenn mindestens eine Zielapp fuer die angemeldete Person aktiviert ist. - `AD` leitet bevorzugt zum AD Kalender weiter, `BR` bevorzugt zu BRTop. Ist das bevorzugte Ziel nicht aktiviert, wird die erste aktivierte Fachapp der Suite verwendet. -- Jede Fachapp bindet `js/suite-navigation.js` und `css/suite-navigation.css` ein und stellt genau einen Host mit `data-orgsuite`, `data-suite` und `data-current-app` bereit. +- OrgSuite lädt `js/suite-navigation.js` und `css/suite-navigation.css` zentral über `BeforeTemplateRenderedEvent`. Fachapps stellen nur einen wirkungslosen Host mit `data-orgsuite`, `data-suite` und `data-current-app` bereit und besitzen dadurch keine harte Asset-Abhängigkeit. - Die Menuestruktur wird ausschliesslich hier gepflegt. Fachapps duplizieren keine Linklisten oder Menuelogik. - Ein sichtbarer Link ist keine Berechtigung. Jeder Zielcontroller und jede API prueft Zugriffe selbst. +- Der Produktinstaller aktiviert OrgSuite erst ab zwei aktivierten AD-Fachprodukten. Bei einer Einzelinstallation registriert das Fachprodukt stattdessen seinen eigenen Nextcloud-Einstieg. ## Git- und Arbeitsregeln @@ -45,7 +46,7 @@ Die Fachapps bleiben eigenständige Repositories, Datenmodelle und Berechtigungs - Das Menue bleibt kompakt, darf umbrechen und darf den Scrollvertrag der einbettenden App nicht veraendern. - Das Quermenü bleibt innerhalb des jeweiligen App-Scrollcontainers am oberen Rand sticky sichtbar und besitzt dafür einen deckenden Nextcloud-Hintergrund. Es verändert keine globalen Nextcloud-Container. - Keine globalen Nextcloud- oder `body`-Selektoren ueberschreiben. -- App-übergreifende Organisations- und Freigabeeinstellungen werden über `OCP\Settings\ISettings` im OrgSuite-Adminabschnitt angezeigt. App-spezifische Administration bleibt im Adminabschnitt der Fachapp; normale App-Einstellungen sind persönliche Einstellungen des eingeloggten Kontos. +- App-übergreifende Organisations- und Freigabeeinstellungen werden über einen LocalBase-`ISettings`-Adapter im OrgSuite-Adminabschnitt angezeigt. Controller, Assets und Persistenz bleiben in LocalBase. App-spezifische Administration bleibt im Adminabschnitt der Fachapp; normale App-Einstellungen sind persönliche Einstellungen des eingeloggten Kontos. - Administrative API-Endpunkte verzichten auf `NoAdminRequired`, prüfen die aktive Sitzung zusätzlich explizit auf Nextcloud-Adminrechte und behalten den CSRF-Schutz für Schreibzugriffe bei. ## Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 113b546..8d5a14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.0-rc.1 + +- OrgSuite als ab zwei AD-Produkten aktivierte Infrastruktur entkoppelt. +- Gemeinsame Menüassets zentral registriert; Fachapps laden keine OrgSuite-Dateien mehr direkt. +- Organisationsadministration als schlanken Adapter auf die LocalBase-Oberfläche reduziert. + ## 0.2.5-rc.1 - Öffentliche Projekt-, Quellcode- und Fehlerkanäle ergänzt. diff --git a/README.md b/README.md index ce43e44..f6a7f01 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,18 @@ # OrgSuite -Gemeinsame AD-/BR-Navigation und Nextcloud-Adminoberfläche für organisationsweite Gruppen-, Hierarchie- und Freigabeeinstellungen. +Gemeinsame AD-/BR-Navigation und Nextcloud-Adminoberfläche für organisationsweite Gruppen-, Hierarchie- und Freigabeeinstellungen. OrgSuite enthält keine Fachdaten. ## Staging-Kompatibilität - Nextcloud 34 - PHP 8.3 oder neuer innerhalb des von Nextcloud 34 unterstützten Bereichs -- Abhängigkeit: `localbase` +- Laufzeitbasis: `localbase` - App-ID und Installationsordner: `orgsuite` ## Installation -```bash -sudo -u www-data php occ app:enable localbase -sudo -u www-data php occ app:enable orgsuite -``` +OrgSuite ist mitgelieferte Infrastruktur und kein separates AD-Fachprodukt. Der Produktinstaller aktiviert sie automatisch ab zwei aktiven AD-Fachprodukten. Bei einer Einzelinstallation bleibt sie deaktiviert; die Fachapp besitzt dann ihren eigenen Einstieg und Adminabschnitt. -Nach der Aktivierung werden Organisationsdefinition und Freigaben ausschließlich im Nextcloud-Adminbereich der OrgSuite gepflegt. +Nach der Aktivierung werden Organisationsdefinition und Freigaben im Nextcloud-Adminbereich der OrgSuite gepflegt. Persistenz, geschützte Admin-API und Organisationseditor liegen in LocalBase. Installations-, Betriebs- und Abnahmeunterlagen stehen im öffentlichen [AD-Suite-Projekt](https://github.com/Filzmann/ad-suite). diff --git a/appinfo/info.xml b/appinfo/info.xml index 49f5e0b..8fc1c5f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ AD- und BR-Suite Gemeinsame Navigation und Administration für die AD- und BR-Fachapps. Bündelt die Einstiege der eigenständigen AD- und BR-Apps und stellt den administrativen Einstieg für organisationsweite Suite-Einstellungen bereit. - 0.2.5-rc.1 + 0.3.0-rc.1 agpl Simon https://github.com/Filzmann/ad-suite @@ -16,7 +16,6 @@ - localbase OCA\OrgSuite\Settings\Admin diff --git a/appinfo/routes.php b/appinfo/routes.php index 13afe39..3a834bf 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -6,8 +6,5 @@ 'routes' => [ ['name' => 'entry#ad', 'url' => '/ad', 'verb' => 'GET'], ['name' => 'entry#br', 'url' => '/br', 'verb' => 'GET'], - ['name' => 'admin_api#settings', 'url' => '/api/admin/settings', 'verb' => 'GET'], - ['name' => 'admin_api#saveOrganization', 'url' => '/api/admin/organization', 'verb' => 'PUT'], - ['name' => 'admin_api#savePermissions', 'url' => '/api/admin/permissions', 'verb' => 'PUT'], ], ]; diff --git a/css/admin.css b/css/admin.css deleted file mode 100644 index 3acb0f5..0000000 --- a/css/admin.css +++ /dev/null @@ -1,43 +0,0 @@ -.orgs-admin { width: 100%; max-width: none; box-sizing: border-box; } -.orgs-admin [hidden] { display: none !important; } -.orgs-admin > p { max-width: 70rem; } -.orgs-panel { width: 100%; max-width: none; box-sizing: border-box; margin: 18px 0; padding: 14px; border: 1px solid var(--color-border); border-radius: var(--border-radius-large); background: var(--color-main-background); } -.orgs-panel > p { max-width: 80rem; } -.orgs-notice { max-width: 70rem; margin: 10px 0; padding: 8px 12px; border-radius: var(--border-radius); background: var(--color-background-dark); } -.orgs-notice--error { border-inline-start: 4px solid var(--color-error); } -.orgs-notice--success { border-inline-start: 4px solid var(--color-success); } -.orgs-general { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0; padding: 10px; border: 1px solid var(--color-border); border-radius: var(--border-radius); } -.orgs-general label,.orgs-hierarchy-toolbar label { display: grid; gap: 4px; } -.orgs-table-wrap { width: 100%; max-width: 100%; min-width: 0; overflow-x: auto; } -.orgs-table { width: max-content; min-width: 100%; max-width: none; margin: 10px 0; border-collapse: collapse; } -.orgs-table th,.orgs-table td { padding: 5px; border: 1px solid var(--color-border); vertical-align: middle; } -.orgs-table input:not([type="checkbox"]) { min-width: 120px; width: 100%; } -.orgs-table input[type="number"] { min-width: 72px; width: 82px; } -.orgs-hierarchy { min-width: 0; margin: 16px 0; padding: 12px; border: 1px solid var(--color-border); border-radius: var(--border-radius-large); } -.orgs-hierarchy > p { margin: 4px 0 10px; color: var(--color-text-maxcontrast); } -.orgs-hierarchy-toolbar { display: flex; align-items: end; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; } -.orgs-hierarchy-toolbar label { min-width: 13rem; } -.orgs-feedback { min-height: 1.4em; color: var(--color-main-text) !important; } -.orgs-organigram { display: grid; gap: 18px; min-width: min-content; padding: 8px; overflow-x: auto; border-radius: var(--border-radius); background: var(--color-background-dark); } -.orgs-organigram-level { position: relative; min-width: max-content; } -.orgs-organigram-level + .orgs-organigram-level::before { content: "↓"; position: absolute; inset-inline-start: 50%; top: -20px; color: var(--color-text-maxcontrast); font-weight: 700; } -.orgs-level-label { display: block; margin-bottom: 5px; color: var(--color-text-maxcontrast); font-size: var(--font-size-small); font-weight: 600; } -.orgs-level-nodes { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 8px; min-width: max-content; } -.orgs-card { min-width: 15rem; max-width: 22rem; padding: 8px; border: 2px solid var(--color-border); border-radius: var(--border-radius-large); background: var(--color-main-background); transition: border-color .12s ease, box-shadow .12s ease; } -.orgs-card.is-drag-over { border-color: var(--color-primary-element); box-shadow: 0 0 0 3px var(--color-primary-element-light); } -.orgs-card header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } -.orgs-card code { color: var(--color-text-maxcontrast); font-size: var(--font-size-small); } -.orgs-drag-role { min-height: 30px !important; margin: 0 !important; padding: 4px 8px !important; font-weight: 700; cursor: grab; } -.orgs-drag-role.is-dragging { opacity: .55; cursor: grabbing; } -.orgs-edges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; } -.orgs-edge { display: inline-flex; align-items: center; gap: 3px; padding-inline-start: 7px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-background-hover); font-size: var(--font-size-small); } -.orgs-edge button { min-width: 24px !important; min-height: 24px !important; height: 24px; margin: 0 !important; padding: 0 6px !important; border-radius: 50%; } -.orgs-empty { color: var(--color-text-maxcontrast); font-size: var(--font-size-small); } -.orgs-checkbox-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 6px 12px; margin: 8px 0 14px; } -.orgs-checkbox-grid label { display: flex; align-items: center; min-height: 32px; } -.orgs-panel form > button[type="submit"] { margin-top: 10px; } -@media (max-width: 700px) { - .orgs-panel { padding: 10px; } - .orgs-general,.orgs-hierarchy-toolbar { display: grid; } - .orgs-hierarchy-toolbar label { min-width: 0; } -} diff --git a/js/admin.js b/js/admin.js deleted file mode 100644 index e6be90f..0000000 --- a/js/admin.js +++ /dev/null @@ -1,78 +0,0 @@ -(function() { - 'use strict'; - - const client = new window.LocalBase.api.ApiClient({ - appId: 'orgsuite', - errorMessage: (data, status) => data?.error || `HTTP ${status}`, - }); - const notice = new window.LocalBase.ui.Notice('orgs-admin-notice', { baseClass: 'orgs-notice', typeClassPrefix: 'orgs-notice--' }); - const organizationForm = document.getElementById('orgs-organization-form'); - const permissionsForm = document.getElementById('orgs-permissions-form'); - const editor = new window.OrgSuite.components.OrganizationEditor({ - container: document.getElementById('orgs-organization-editor'), - form: organizationForm, - onSave: saveOrganization, - }); - - function renderCheckboxes(containerId, values, options) { - const container = document.getElementById(containerId); - const labels = new Map((options || []).map(option => [option.groupId, option.label])); - container.replaceChildren(...Object.entries(values || {}).map(([groupId, enabled]) => { - const label = document.createElement('label'); - const input = document.createElement('input'); - input.type = 'checkbox'; - input.name = groupId; - input.checked = Boolean(enabled); - label.append(input, document.createTextNode(` ${labels.get(groupId) || groupId}`)); - return label; - })); - } - - function collect(containerId) { - return Object.fromEntries([...document.getElementById(containerId).querySelectorAll('input[type="checkbox"]')].map(input => [input.name, input.checked])); - } - - async function load() { - try { - const data = await client.request('/api/admin/settings'); - editor.set(data.organization); - renderCheckboxes('orgs-calendar-peer-settings', data.calendarPeerEditing, data.calendarPeerOptions); - renderCheckboxes('orgs-vacation-peer-settings', data.vacationPeerApproval, data.vacationPeerOptions); - notice.clear(); - } catch (error) { - notice.error(error); - organizationForm.querySelector('button[type="submit"]').disabled = true; - permissionsForm.querySelector('button[type="submit"]').disabled = true; - } - } - - async function saveOrganization(organization) { - try { - const data = await client.request('/api/admin/organization', { method: 'PUT', body: JSON.stringify({ organization }) }); - editor.set(data.organization); - await load(); - notice.success('AD-Organisation gespeichert.'); - } catch (error) { - notice.error(error); - } - } - - permissionsForm.addEventListener('submit', async event => { - event.preventDefault(); - try { - await client.request('/api/admin/permissions', { - method: 'PUT', - body: JSON.stringify({ - calendarPeerEditing: collect('orgs-calendar-peer-settings'), - vacationPeerApproval: collect('orgs-vacation-peer-settings'), - }), - }); - await load(); - notice.success('Organisationsweite Rechte gespeichert.'); - } catch (error) { - notice.error(error); - } - }); - - load(); -})(); diff --git a/js/components/hierarchy-board.js b/js/components/hierarchy-board.js deleted file mode 100644 index 39acf32..0000000 --- a/js/components/hierarchy-board.js +++ /dev/null @@ -1,114 +0,0 @@ -(function() { - 'use strict'; - - const esc = window.LocalBase.ui.esc; - const clone = value => JSON.parse(JSON.stringify(value)); - - /** - * Zweck: Rendert und bearbeitet direkte Organisationskanten per Tastatur und Drag-and-drop. - * Vertrag: Die clientseitige Zyklusprüfung dient der Rückmeldung; LocalBase validiert den Payload serverseitig erneut. - */ - class HierarchyBoard { - constructor({ container }) { - this.container = container; - this.roles = {}; - this.hierarchy = {}; - this.draggedRole = ''; - container.addEventListener('click', event => this.onClick(event)); - container.addEventListener('dragstart', event => this.startDrag(event)); - container.addEventListener('dragover', event => this.overTarget(event)); - container.addEventListener('dragleave', event => event.target instanceof Element && event.target.closest('[data-manager-key]')?.classList.remove('is-drag-over')); - container.addEventListener('drop', event => this.dropRole(event)); - container.addEventListener('dragend', () => this.clearDrag()); - } - - set(roles, hierarchy) { this.roles = clone(roles); this.hierarchy = clone(hierarchy); this.render(); } - get() { return clone(this.hierarchy); } - - onClick(event) { - const button = event.target instanceof Element ? event.target.closest('button[data-action]') : null; - if (!button) return; - if (button.dataset.action === 'remove-edge') this.removeEdge(button.dataset.managerKey, button.dataset.targetKey); - if (button.dataset.action === 'add-edge') this.addEdge( - this.container.querySelector('[data-hierarchy-manager]').value, - this.container.querySelector('[data-hierarchy-target]').value, - ); - } - - render(message = '') { - const roles = Object.entries(this.roles).sort(([, a], [, b]) => Number(a.sortOrder) - Number(b.sortOrder)); - const roleMap = Object.fromEntries(roles); - this.container.querySelector('[data-hierarchy-board]').innerHTML = this.levels(roles.map(([key]) => key)).map((level, index) => ` -
Ebene ${index + 1}
- ${level.map(key => this.card(key, roleMap[key], roleMap)).join('')} -
`).join(''); - this.container.querySelector('[data-hierarchy-feedback]').textContent = message; - } - - card(key, role, roleMap) { - const children = (this.hierarchy[key] || []).map(target => `${esc(roleMap[target]?.label || target)}`).join('') || 'Keine direkt unterstellte Rolle'; - return `
${esc(key)}
${children}
`; - } - - levels(roleKeys) { - const levels = Object.fromEntries(roleKeys.map(key => [key, 0])); - for (let pass = 0; pass < roleKeys.length; pass += 1) for (const [manager, targets] of Object.entries(this.hierarchy)) for (const target of targets) if (manager in levels && target in levels) levels[target] = Math.max(levels[target], levels[manager] + 1); - const result = []; - for (const key of roleKeys) (result[levels[key]] ||= []).push(key); - return result.filter(Boolean); - } - - addEdge(manager, target) { - if (!this.roles[manager] || !this.roles[target]) return this.render('Bitte wähle zwei gültige Rollen.'); - if (manager === target) return this.render('Eine Rolle kann sich nicht selbst unterstellt sein.'); - if ((this.hierarchy[manager] || []).includes(target)) return this.render('Diese direkte Unterstellung besteht bereits.'); - if (this.reaches(target, manager)) return this.render('Diese Verbindung würde einen Hierarchiezyklus erzeugen.'); - this.hierarchy[manager] = [...(this.hierarchy[manager] || []), target]; - this.render(`${this.roles[target].label} ist jetzt ${this.roles[manager].label} direkt unterstellt.`); - } - - removeEdge(manager, target) { - this.hierarchy[manager] = (this.hierarchy[manager] || []).filter(key => key !== target); - if (!this.hierarchy[manager].length) delete this.hierarchy[manager]; - this.render('Direkte Unterstellung entfernt.'); - } - - reaches(start, goal, visited = new Set()) { - if (start === goal) return true; - if (visited.has(start)) return false; - visited.add(start); - return (this.hierarchy[start] || []).some(target => this.reaches(target, goal, visited)); - } - - startDrag(event) { - const source = event.target instanceof Element ? event.target.closest('[data-drag-role]') : null; - if (!source || !event.dataTransfer) return; - this.draggedRole = source.dataset.dragRole; - event.dataTransfer.effectAllowed = 'link'; event.dataTransfer.setData('text/plain', this.draggedRole); source.classList.add('is-dragging'); - } - - overTarget(event) { - const target = event.target instanceof Element ? event.target.closest('[data-manager-key]') : null; - if (!target || !this.draggedRole || target.dataset.managerKey === this.draggedRole) return; - event.preventDefault(); target.classList.add('is-drag-over'); - if (event.dataTransfer) event.dataTransfer.dropEffect = 'link'; - } - - dropRole(event) { - const target = event.target instanceof Element ? event.target.closest('[data-manager-key]') : null; - if (!target) return; - event.preventDefault(); - const role = event.dataTransfer?.getData('text/plain') || this.draggedRole; - this.clearDrag(); this.addEdge(target.dataset.managerKey, role); - } - - clearDrag() { - this.draggedRole = ''; - this.container.querySelectorAll('.is-dragging,.is-drag-over').forEach(element => element.classList.remove('is-dragging', 'is-drag-over')); - } - } - - window.OrgSuite = window.OrgSuite || {}; - window.OrgSuite.components = window.OrgSuite.components || {}; - window.OrgSuite.components.HierarchyBoard = HierarchyBoard; -})(); diff --git a/js/components/organization-editor.js b/js/components/organization-editor.js deleted file mode 100644 index 52e4ab0..0000000 --- a/js/components/organization-editor.js +++ /dev/null @@ -1,94 +0,0 @@ -(function() { - 'use strict'; - - const esc = window.LocalBase.ui.esc; - const clone = value => JSON.parse(JSON.stringify(value)); - - /** - * Zweck: Bearbeitet allgemeine AD-Organisationsfelder, Rollen, Bereiche und Urlaubsansichten. - * Zusammenspiel: admin.js -> OrganizationEditor -> HierarchyBoard; LocalBase validiert den gesammelten Payload serverseitig. - */ - class OrganizationEditor { - constructor({ container, form, onSave }) { - this.container = container; this.form = form; this.onSave = onSave; this.definition = null; - this.hierarchyBoard = new window.OrgSuite.components.HierarchyBoard({ container }); - form.addEventListener('submit', event => { event.preventDefault(); if (this.definition) this.onSave(this.collect()); }); - container.addEventListener('click', event => this.onClick(event)); - } - - set(definition) { this.definition = clone(definition); this.render(); } - - onClick(event) { - const button = event.target instanceof Element ? event.target.closest('button[data-action]') : null; - if (!button) return; - if (button.dataset.action === 'add-team') this.addTeam(); - if (button.dataset.action === 'remove-team') button.closest('tr')?.remove(); - } - - render() { - const data = this.definition; - const roles = Object.entries(data.roles).sort(([, a], [, b]) => Number(a.sortOrder) - Number(b.sortOrder)); - const areas = Object.entries(data.areas).sort(([, a], [, b]) => Number(a.sortOrder) - Number(b.sortOrder)); - this.container.innerHTML = ` -
Allgemein - - - - -
-
${roles.map(([key, role]) => this.roleRow(key, role)).join('')}
Fachrollen und Nextcloud-Gruppen
RolleGruppen-IDAnzeigenameKalenderBereichLeitung je BereichPeer-fähigLeitungsblockReihenfolge
-
${areas.map(([key, area]) => this.areaRow(key, area)).join('')}
Bürobereiche
BereichGruppen-IDAnzeigenameReihenfolge
-
Direkte Hierarchie -

Ziehe eine unterstellte Rolle auf die Karte ihrer Leitung. Für die Tastatur steht dieselbe Zuordnung über die Auswahlfelder bereit.

-
-

-
-
${(data.organizationTeams || []).map(team => this.teamRow(team)).join('')}
Teamansichten im Urlaubsplaner
IDAnzeigenameRollenBereicheReihenfolgeAktion
- `; - this.hierarchyBoard.set(data.roles, data.hierarchy || {}); - } - - roleRow(key, role) { - const check = (field, label) => ``; - return `${esc(key)}${check('calendarVisible', `${role.label} im Kalender sichtbar`)}${check('areaScoped', `${role.label} ist bereichsgebunden`)}${check('managementAreaScoped', `Leitungsrecht von ${role.label} ist bereichsgebunden`)}${check('peerEnabled', `Peer-Recht für ${role.label} konfigurierbar`)}${check('staffBlock', `${role.label} im Leitungsblock`)}`; - } - - areaRow(key, area) { return `${esc(key)}`; } - roleOptions(roles) { return roles.map(([key, role]) => ``).join(''); } - - collect() { - const data = clone(this.definition); - for (const field of ['teamGroupPrefix', 'teamLabelPrefix', 'staffBlockLabel']) data[field] = this.container.querySelector(`[data-organization-field="${field}"]`).value.trim(); - data.teamCodeMaxLength = Number(this.container.querySelector('[data-organization-field="teamCodeMaxLength"]').value); - this.container.querySelectorAll('[data-role-key]').forEach(row => { - const role = data.roles[row.dataset.roleKey]; - for (const field of ['groupId', 'label']) role[field] = row.querySelector(`[data-field="${field}"]`).value.trim(); - for (const field of ['calendarVisible', 'areaScoped', 'managementAreaScoped', 'peerEnabled', 'staffBlock']) role[field] = row.querySelector(`[data-field="${field}"]`).checked; - role.sortOrder = Number(row.querySelector('[data-field="sortOrder"]').value); - }); - this.container.querySelectorAll('[data-area-key]').forEach(row => { - const area = data.areas[row.dataset.areaKey]; - for (const field of ['groupId', 'label']) area[field] = row.querySelector(`[data-field="${field}"]`).value.trim(); - area.sortOrder = Number(row.querySelector('[data-field="sortOrder"]').value); - }); - data.hierarchy = this.hierarchyBoard.get(); - data.organizationTeams = [...this.container.querySelectorAll('[data-organization-team]')].map(row => ({ id: row.querySelector('[data-field="id"]').value.trim(), label: row.querySelector('[data-field="label"]').value.trim(), roles: this.list(row.querySelector('[data-field="roles"]').value), areas: this.list(row.querySelector('[data-field="areas"]').value), sortOrder: Number(row.querySelector('[data-field="sortOrder"]').value) })); - return data; - } - - addTeam() { - const rows = [...this.container.querySelectorAll('[data-organization-team]')]; - const ids = new Set(rows.map(row => row.querySelector('[data-field="id"]').value)); - let number = rows.length + 1; while (ids.has(`view-${number}`)) number += 1; - const sortOrder = Math.max(0, ...rows.map(row => Number(row.querySelector('[data-field="sortOrder"]').value) || 0)) + 10; - this.container.querySelector('[data-organization-teams]').insertAdjacentHTML('beforeend', this.teamRow({ id: `view-${number}`, label: 'Neue Urlaubsansicht', roles: [], areas: [], sortOrder })); - } - - teamRow(team) { return ``; } - list(value) { return [...new Set(value.split(',').map(item => item.trim()).filter(Boolean))]; } - } - - window.OrgSuite = window.OrgSuite || {}; - window.OrgSuite.components = window.OrgSuite.components || {}; - window.OrgSuite.components.OrganizationEditor = OrganizationEditor; -})(); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e71c7df..95b1007 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -5,15 +5,17 @@ namespace OCA\OrgSuite\AppInfo; use OCA\OrgSuite\Listener\NavigationListener; +use OCA\OrgSuite\Listener\SuiteAssetsListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\Navigation\Events\LoadAdditionalEntriesEvent; /** * Zweck: Registriert die gemeinsamen AD- und BR-Einstiege im Nextcloud-Appmenue. - * Zusammenspiel: Nextcloud -> NavigationListener; die Fachapps laden separat das gemeinsame Suite-Menue. + * Zusammenspiel: Nextcloud -> NavigationListener und SuiteAssetsListener; Fachapps stellen nur optionale Menühosts bereit. */ final class Application extends App implements IBootstrap { public const APP_ID = 'orgsuite'; @@ -24,9 +26,9 @@ public function __construct(array $urlParams = []) { public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadAdditionalEntriesEvent::class, NavigationListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, SuiteAssetsListener::class); } public function boot(IBootContext $context): void { } } - diff --git a/lib/Controller/AdminApiController.php b/lib/Controller/AdminApiController.php deleted file mode 100644 index 168c98d..0000000 --- a/lib/Controller/AdminApiController.php +++ /dev/null @@ -1,75 +0,0 @@ - AdminApiController -> LocalBase-Organisations- und Freigabeservices. - * Vertrag: Keine Methode trägt NoAdminRequired; zusätzlich verweigert der Controller direkte Aufrufe ohne aktive Admin-Sitzung. - */ -final class AdminApiController extends Controller { - public function __construct( - IRequest $request, - private IUserSession $session, - private IGroupManager $groups, - private AdOrganizationSettingsService $organization, - private AdSuiteAdminSettingsService $adminSettings, - private LoggerInterface $logger, - ) { - parent::__construct(Application::APP_ID, $request); - } - - public function settings(): JSONResponse { - if (!$this->isAdmin()) return $this->denied(); - return new JSONResponse([ - 'organization' => $this->organization->definition()->toArray(), - 'calendarPeerEditing' => $this->adminSettings->calendarPeerEditing(), - 'calendarPeerOptions' => $this->adminSettings->calendarPeerOptions(), - 'vacationPeerApproval' => $this->adminSettings->vacationPeerApproval(), - 'vacationPeerOptions' => $this->adminSettings->vacationPeerOptions(), - ]); - } - - public function saveOrganization(array $organization): JSONResponse { - if (!$this->isAdmin()) return $this->denied(); - try { - return new JSONResponse(['organization' => $this->organization->save($organization)->toArray()]); - } catch (InvalidArgumentException $error) { - return new JSONResponse(['error' => $error->getMessage()], Http::STATUS_BAD_REQUEST); - } catch (\Throwable $error) { - $this->logger->error('AD-Organisation konnte nicht gespeichert werden.', ['exception' => $error]); - return new JSONResponse(['error' => 'Die AD-Organisation konnte nicht gespeichert werden.'], Http::STATUS_BAD_REQUEST); - } - } - - public function savePermissions(array $calendarPeerEditing, array $vacationPeerApproval): JSONResponse { - if (!$this->isAdmin()) return $this->denied(); - return new JSONResponse([ - 'calendarPeerEditing' => $this->adminSettings->saveCalendarPeerEditing($calendarPeerEditing), - 'vacationPeerApproval' => $this->adminSettings->saveVacationPeerApproval($vacationPeerApproval), - ]); - } - - private function isAdmin(): bool { - $user = $this->session->getUser(); - return $user !== null && $this->groups->isAdmin($user->getUID()); - } - - private function denied(): JSONResponse { - return new JSONResponse(['error' => 'Keine Berechtigung.'], Http::STATUS_FORBIDDEN); - } -} diff --git a/lib/Listener/SuiteAssetsListener.php b/lib/Listener/SuiteAssetsListener.php new file mode 100644 index 0000000..931acec --- /dev/null +++ b/lib/Listener/SuiteAssetsListener.php @@ -0,0 +1,23 @@ + + */ +final class SuiteAssetsListener implements IEventListener { + public function handle(Event $event): void { + if (!$event instanceof BeforeTemplateRenderedEvent) return; + Util::addScript('orgsuite', 'suite-navigation'); + Util::addStyle('orgsuite', 'suite-navigation'); + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 2c0ca0a..101aa45 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -11,7 +11,7 @@ /** Zweck: Bindet die organisationsweiten Suite-Einstellungen in den Nextcloud-Adminbereich ein. */ final class Admin implements ISettings { public function getForm(): TemplateResponse { - return new TemplateResponse(Application::APP_ID, 'admin'); + return new TemplateResponse('localbase', 'organization-admin', ['standalone' => false]); } public function getSection(): string { diff --git a/templates/admin.php b/templates/admin.php deleted file mode 100644 index f459b27..0000000 --- a/templates/admin.php +++ /dev/null @@ -1,40 +0,0 @@ - -
-

AD-/BR-Suite

-

Organisationsweite Einstellungen sind ausschließlich hier im Nextcloud-Adminbereich änderbar. Einstellungen in den Fachapps gelten nur für das jeweils eingeloggte Konto.

- - -
-

AD-Organisation

-

Diese Konfiguration steuert Gruppen, sichtbare Namen, Bereiche, Hierarchie und Urlaubsansichten in den AD-Fachapps.

-

Wichtig: Änderungen an Gruppen-IDs verschieben keine bestehenden Nextcloud-Mitgliedschaften. Zielgruppen und Mitgliedschaften müssen vor der Umstellung vorbereitet werden.

-
-
- -
-
- -
-

Zusätzliche Rechte innerhalb von Fachgruppen

-
-
- AD Kalender -

Aktivierte Kolleg*innen dürfen innerhalb derselben Fachgruppe Kalenderdaten bearbeiten; Büro- und EB-Rechte bleiben auf gemeinsame Bürobereiche begrenzt.

-
-
-
- AD Urlaub -

Aktivierte Kolleg*innen dürfen innerhalb derselben Fachgruppe geplante Urlaube genehmigen. Eigene Genehmigungen bleiben gesperrt.

-
-
- -
-
-
diff --git a/tests/AdminApiControllerExecutionTest.php b/tests/AdminApiControllerExecutionTest.php deleted file mode 100644 index f59955f..0000000 --- a/tests/AdminApiControllerExecutionTest.php +++ /dev/null @@ -1,81 +0,0 @@ -data; } - public function getStatus(): int { return $this->status; } - } -} -namespace Psr\Log { interface LoggerInterface { public function error(string $message, array $context = []): void; } } -namespace OCA\OrgSuite\AppInfo { final class Application { public const APP_ID = 'orgsuite'; } } -namespace OCA\LocalBase\Organization { - class Definition { public function __construct(private array $data) {} public function toArray(): array { return $this->data; } } - class AdOrganizationSettingsService { - public bool $invalid = false; - public bool $failure = false; - public function definition(): Definition { return new Definition(['roles' => ['buero']]); } - public function save(array $organization): Definition { - if ($this->invalid) throw new \InvalidArgumentException('Ungültige Organisation.'); - if ($this->failure) throw new \RuntimeException('Intern'); - return new Definition($organization); - } - } - class AdSuiteAdminSettingsService { - public function calendarPeerEditing(): array { return ['ad-Buero' => true]; } - public function calendarPeerOptions(): array { return ['ad-Buero']; } - public function vacationPeerApproval(): array { return ['ad-PFK' => false]; } - public function vacationPeerOptions(): array { return ['ad-PFK']; } - public function saveCalendarPeerEditing(array $value): array { return $value; } - public function saveVacationPeerApproval(array $value): array { return $value; } - } -} - -namespace { - require_once __DIR__ . '/../lib/Controller/AdminApiController.php'; - - use OCA\LocalBase\Organization\AdOrganizationSettingsService; - use OCA\LocalBase\Organization\AdSuiteAdminSettingsService; - use OCA\OrgSuite\Controller\AdminApiController; - use OCP\IGroupManager; - use OCP\IRequest; - use OCP\IUser; - use OCP\IUserSession; - use Psr\Log\LoggerInterface; - - $request = new class implements IRequest {}; - $user = new class implements IUser { public function getUID(): string { return 'admin'; } }; - $session = new class($user) implements IUserSession { public function __construct(private ?IUser $user) {} public function getUser(): ?IUser { return $this->user; } }; - $groups = new class implements IGroupManager { public bool $admin = false; public function isAdmin(string $uid): bool { return $this->admin; } }; - $organization = new AdOrganizationSettingsService(); - $settings = new AdSuiteAdminSettingsService(); - $logger = new class implements LoggerInterface { public array $errors = []; public function error(string $message, array $context = []): void { $this->errors[] = [$message, $context]; } }; - $controller = new AdminApiController($request, $session, $groups, $organization, $settings, $logger); - if ($controller->settings()->getStatus() !== 403) throw new RuntimeException('Nicht-Admin kann Einstellungen lesen.'); - if ($controller->saveOrganization([])->getStatus() !== 403 || $controller->savePermissions([], [])->getStatus() !== 403) throw new RuntimeException('Nicht-Admin kann Einstellungen schreiben.'); - $groups->admin = true; - $data = $controller->settings()->getData(); - if (($data['organization']['roles'][0] ?? '') !== 'buero' || !isset($data['calendarPeerOptions'], $data['vacationPeerOptions'])) throw new RuntimeException('Admin-Einstellungen sind unvollständig.'); - if ($controller->saveOrganization(['roles' => ['pfk']])->getData()['organization']['roles'][0] !== 'pfk') throw new RuntimeException('Organisation wird nicht gespeichert.'); - $organization->invalid = true; - if ($controller->saveOrganization([])->getStatus() !== 400) throw new RuntimeException('Validierungsfehler erhält keinen Status 400.'); - $organization->invalid = false; - $organization->failure = true; - if ($controller->saveOrganization([])->getStatus() !== 400 || $logger->errors === []) throw new RuntimeException('Interner Fehler wird nicht sicher behandelt.'); - $permissions = $controller->savePermissions(['ad-Buero' => false], ['ad-PFK' => true])->getData(); - if ($permissions['calendarPeerEditing']['ad-Buero'] !== false || $permissions['vacationPeerApproval']['ad-PFK'] !== true) throw new RuntimeException('Freigaben werden nicht gespeichert.'); - - echo "OrgSuite admin controller execution tests passed\n"; -} diff --git a/tests/AdminSettingsContractTest.php b/tests/AdminSettingsContractTest.php index 8f07c8d..c77d777 100644 --- a/tests/AdminSettingsContractTest.php +++ b/tests/AdminSettingsContractTest.php @@ -5,26 +5,12 @@ $root = dirname(__DIR__); $info = file_get_contents($root . '/appinfo/info.xml'); $routes = file_get_contents($root . '/appinfo/routes.php'); -$controller = file_get_contents($root . '/lib/Controller/AdminApiController.php'); -$template = file_get_contents($root . '/templates/admin.php'); -foreach ([$info, $routes, $controller, $template] as $source) if ($source === false) throw new RuntimeException('Admin-Vertragsdatei konnte nicht gelesen werden.'); +$setting = file_get_contents($root . '/lib/Settings/Admin.php'); +foreach ([$info, $routes, $setting] as $source) if ($source === false) throw new RuntimeException('Admin-Vertragsdatei konnte nicht gelesen werden.'); + +foreach (['OCA\OrgSuite\Settings\Admin', 'OCA\OrgSuite\Settings\AdminSection'] as $contract) if (!str_contains($info, $contract)) throw new RuntimeException("Admin-Registrierung fehlt: {$contract}"); +if (str_contains($info, '')) throw new RuntimeException('Nicht unterstützte App-Abhängigkeit im OrgSuite-Manifest.'); +if (str_contains($routes, '/api/admin/')) throw new RuntimeException('OrgSuite besitzt noch einen parallelen Admin-Datenpfad.'); +foreach (["new TemplateResponse('localbase', 'organization-admin'", "'standalone' => false"] as $contract) if (!str_contains($setting, $contract)) throw new RuntimeException("LocalBase-Adminadapter fehlt: {$contract}"); -foreach (['localbase', 'OCA\OrgSuite\Settings\Admin', 'OCA\OrgSuite\Settings\AdminSection'] as $contract) { - if (!str_contains($info, $contract)) throw new RuntimeException("Admin-Registrierung fehlt: {$contract}"); -} -foreach (['/api/admin/settings', '/api/admin/organization', '/api/admin/permissions'] as $contract) { - if (!str_contains($routes, $contract)) throw new RuntimeException("Admin-Route fehlt: {$contract}"); -} -if (preg_match('/#\[[^\]]*NoAdminRequired/', $controller)) throw new RuntimeException('Admin-Controller ist für normale Nutzer*innen freigegeben.'); -if (preg_match('/#\[[^\]]*NoCSRFRequired[^\]]*\]\s+public function save/', $controller)) throw new RuntimeException('Schreibender Admin-Endpunkt umgeht den CSRF-Schutz.'); -foreach (['private function isAdmin()', '$this->groups->isAdmin(', 'Http::STATUS_FORBIDDEN', 'saveOrganization', 'savePermissions'] as $contract) { - if (!str_contains($controller, $contract)) throw new RuntimeException("Serverseitiger Admin-Vertrag fehlt: {$contract}"); -} -foreach (['id="orgsuite-admin"', 'id="orgs-organization-form"', 'id="orgs-permissions-form"', 'ausschließlich hier im Nextcloud-Adminbereich'] as $contract) { - if (!str_contains($template, $contract)) throw new RuntimeException("Admin-UI-Vertrag fehlt: {$contract}"); -} -foreach (["\\OCP\\Util::addScript('orgsuite', 'components/hierarchy-board')", "\\OCP\\Util::addScript('orgsuite', 'components/organization-editor')", "\\OCP\\Util::addStyle('orgsuite', 'admin')"] as $contract) { - if (!str_contains($template, $contract)) throw new RuntimeException("Admin-Assetvertrag fehlt: {$contract}"); -} -if (preg_match('/^\\s*(?:script|style)\\s*\\(/m', $template) === 1) throw new RuntimeException('Veralteter globaler Templatehelfer gefunden.'); echo "AdminSettingsContractTest: OK\n"; diff --git a/tests/AdminSettingsExecutionTest.php b/tests/AdminSettingsExecutionTest.php new file mode 100644 index 0000000..815df15 --- /dev/null +++ b/tests/AdminSettingsExecutionTest.php @@ -0,0 +1,44 @@ +getForm(); + if ($form->appName !== 'localbase' || $form->templateName !== 'organization-admin' || $form->params !== ['standalone' => false]) { + throw new RuntimeException('OrgSuite bindet nicht das LocalBase-Organisationsformular ein.'); + } + if ((new Admin())->getSection() !== 'orgsuite' || (new Admin())->getPriority() !== 20) { + throw new RuntimeException('OrgSuite-Adminsetting besitzt falsche Metadaten.'); + } + + $url = new class implements IURLGenerator { + public function imagePath(string $appName, string $file): string { return "$appName/$file"; } + }; + $section = new AdminSection($url); + if ($section->getID() !== 'orgsuite' || $section->getName() !== 'AD-/BR-Suite' || $section->getPriority() !== 60 || $section->getIcon() !== 'orgsuite/ad.svg') { + throw new RuntimeException('OrgSuite-Adminabschnitt besitzt falsche Metadaten.'); + } + + echo "OrgSuite admin settings execution test passed\n"; +} diff --git a/tests/ApplicationBootstrapExecutionTest.php b/tests/ApplicationBootstrapExecutionTest.php new file mode 100644 index 0000000..efc1132 --- /dev/null +++ b/tests/ApplicationBootstrapExecutionTest.php @@ -0,0 +1,43 @@ +listeners[] = [$event, $listener]; } + }; + $application = new Application(); + $application->register($registration); + $application->boot(new class implements IBootContext {}); + + if ($registration->listeners !== [ + [LoadAdditionalEntriesEvent::class, NavigationListener::class], + [BeforeTemplateRenderedEvent::class, SuiteAssetsListener::class], + ]) { + throw new RuntimeException('OrgSuite-Bootstrap registriert nicht alle Listener.'); + } + + echo "OrgSuite application bootstrap execution test passed\n"; +} diff --git a/tests/SuiteAssetsListenerExecutionTest.php b/tests/SuiteAssetsListenerExecutionTest.php new file mode 100644 index 0000000..8a35657 --- /dev/null +++ b/tests/SuiteAssetsListenerExecutionTest.php @@ -0,0 +1,33 @@ +handle(new Event()); + if (Util::$scripts !== [] || Util::$styles !== []) throw new RuntimeException('Fremdes Event lädt Suite-Assets.'); + $listener->handle(new BeforeTemplateRenderedEvent()); + if (Util::$scripts !== [['orgsuite', 'suite-navigation']] || Util::$styles !== [['orgsuite', 'suite-navigation']]) { + throw new RuntimeException('Suite-Assets werden nicht zentral registriert.'); + } + + echo "OrgSuite assets listener execution tests passed\n"; +} diff --git a/tests/http-smoke.sh b/tests/http-smoke.sh index 8d43eae..6f4529b 100755 --- a/tests/http-smoke.sh +++ b/tests/http-smoke.sh @@ -51,7 +51,7 @@ if [[ -z "$admin_token" ]]; then fi curl --fail --silent --show-error --insecure --user "$ORGS_ADMIN_USER:$ORGS_ADMIN_PASSWORD" \ --cookie "$admin_cookies" --cookie-jar "$admin_cookies" -H "requesttoken: $admin_token" \ - "$base_url/index.php/apps/orgsuite/api/admin/settings" --output "$admin_settings" + "$base_url/index.php/apps/localbase/api/ad-suite/admin/settings" --output "$admin_settings" for contract in '"organization"' '"calendarPeerEditing"' '"vacationPeerApproval"'; do if ! grep -q "$contract" "$admin_settings"; then echo "Admin-API-Vertrag fehlt: $contract" >&2 @@ -62,7 +62,7 @@ done status="$(curl --silent --show-error --insecure --user "$ORGS_ADMIN_USER:$ORGS_ADMIN_PASSWORD" \ --cookie "$admin_cookies" --cookie-jar "$admin_cookies" -H 'Content-Type: application/json' \ -X PUT --data '{}' --output "$workdir/csrf.json" --write-out '%{http_code}' \ - "$base_url/index.php/apps/orgsuite/api/admin/permissions")" + "$base_url/index.php/apps/localbase/api/ad-suite/admin/permissions")" if [[ "$status" != '412' ]]; then echo "Admin-Schreibzugriff ohne CSRF-Token ergab HTTP $status statt 412." >&2 exit 1 @@ -81,7 +81,7 @@ fi status="$(curl --silent --show-error --insecure --user "$nonadmin:$nonadmin_password" \ --cookie "$nonadmin_cookies" --cookie-jar "$nonadmin_cookies" -H "requesttoken: $nonadmin_token" \ --output "$workdir/denied.json" --write-out '%{http_code}' \ - "$base_url/index.php/apps/orgsuite/api/admin/settings")" + "$base_url/index.php/apps/localbase/api/ad-suite/admin/settings")" if [[ "$status" != '403' ]]; then echo "Standardkonto erhielt beim Admin-Endpunkt HTTP $status statt 403." >&2 exit 1 diff --git a/tests/js/admin-settings-smoke.mjs b/tests/js/admin-settings-smoke.mjs deleted file mode 100644 index e54eba1..0000000 --- a/tests/js/admin-settings-smoke.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { runInNewContext } from 'node:vm'; - -const editorSource = readFileSync(new URL('../../js/components/organization-editor.js', import.meta.url), 'utf8'); -const hierarchySource = readFileSync(new URL('../../js/components/hierarchy-board.js', import.meta.url), 'utf8'); -const adminSource = readFileSync(new URL('../../js/admin.js', import.meta.url), 'utf8'); -const css = readFileSync(new URL('../../css/admin.css', import.meta.url), 'utf8'); - -for (const contract of ['class OrganizationEditor', 'Direkte Hierarchie', 'Fachrollen und Nextcloud-Gruppen', 'data-organization-teams', 'this.hierarchyBoard.get()']) { - if (!editorSource.includes(contract)) throw new Error(`Organisationseditor-Vertrag fehlt: ${contract}`); -} -for (const contract of ['class HierarchyBoard', 'draggable="true"', 'addEdge(manager, target)', 'Diese Verbindung würde einen Hierarchiezyklus erzeugen.', 'levels(roleKeys)']) if (!hierarchySource.includes(contract)) throw new Error(`Organigramm-Vertrag fehlt: ${contract}`); -for (const contract of ['/api/admin/settings', '/api/admin/organization', '/api/admin/permissions', 'calendarPeerEditing', 'vacationPeerApproval']) { - if (!adminSource.includes(contract)) throw new Error(`Admin-Frontendvertrag fehlt: ${contract}`); -} -for (const contract of ['width: 100%', 'max-width: none', 'overflow-x: auto', '.orgs-organigram', '.orgs-card.is-drag-over']) { - if (!css.includes(contract)) throw new Error(`Admin-Layoutvertrag fehlt: ${contract}`); -} - -const context = { window: { LocalBase: { ui: { esc: value => String(value ?? '') } } }, JSON, Set, Math, Object, Element: class {} }; -runInNewContext(hierarchySource, context); -runInNewContext(editorSource, context); -const board = Object.create(context.window.OrgSuite.components.HierarchyBoard.prototype); -board.hierarchy = { gf: ['pdl'], pdl: ['pfk'] }; -if (JSON.stringify(board.levels(['gf', 'pdl', 'pfk'])) !== JSON.stringify([['gf'], ['pdl'], ['pfk']])) throw new Error('Organigramm-Ebenen werden nicht aus der Hierarchie abgeleitet.'); -if (!board.reaches('gf', 'pfk') || board.reaches('pfk', 'gf')) throw new Error('Clientseitige Zyklusprüfung ist fehlerhaft.'); - -console.log('OrgSuite Admin settings smoke passed'); diff --git a/tests/run-js.mjs b/tests/run-js.mjs index c9fe45b..aeaa6d8 100644 --- a/tests/run-js.mjs +++ b/tests/run-js.mjs @@ -1,9 +1,5 @@ import {execFileSync} from 'node:child_process'; execFileSync('node', ['--check', new URL('../js/suite-navigation.js', import.meta.url).pathname], {stdio: 'inherit'}); -execFileSync('node', ['--check', new URL('../js/admin.js', import.meta.url).pathname], {stdio: 'inherit'}); -execFileSync('node', ['--check', new URL('../js/components/hierarchy-board.js', import.meta.url).pathname], {stdio: 'inherit'}); -execFileSync('node', ['--check', new URL('../js/components/organization-editor.js', import.meta.url).pathname], {stdio: 'inherit'}); execFileSync('node', [new URL('./js/suite-navigation-smoke.mjs', import.meta.url).pathname], {stdio: 'inherit'}); -execFileSync('node', [new URL('./js/admin-settings-smoke.mjs', import.meta.url).pathname], {stdio: 'inherit'}); console.log('OrgSuite JavaScript tests passed');