fix(fe): gate-13 — extract four inline modals, and fix the slot typo that made four settings info panels render empty - #479
Merged
Conversation
…made four settings info panels render empty
gate-13 (modal-isolation) reported three files with inline NcModal/NcDialog
markup. Fixing them surfaced a second, unrelated defect that no gate and no
test could see, because its failure mode is silence.
THE SLOT BUG
AlwaysVisibleSection declared `<slot name="info" />`. Four callers pass
`<template #info-content>`:
UserGroupsConfiguration, EmailConfiguration,
ArchiMateImportExport, OrganizationSynchronization
All four also set `:has-info-content="true"`, so the (i) button rendered and
opened a modal with nothing in it. Vue drops slot content addressed to a slot
the child does not declare — no warning, no error, no failing test. The name
came from CollapsibleSection, which does use `info-content`; only
VersionInformation used the working `info` name.
Both sections now render `<slot name="info-content"><slot name="info" /></slot>`
so `info-content` wins and `info` remains a fallback. All five callers render.
THE EXTRACTION
src/dialogs/ChangePasswordDialog.vue <- ContactpersonenList
src/dialogs/ManageUserGroupsDialog.vue <- ContactpersonenList
src/modals/AlwaysVisibleSectionInfoModal.vue
src/modals/CollapsibleSectionInfoModal.vue
ContactpersonenList drops 1563 -> 954 lines. Password validation, the HIBP
pwned-check, the debounce watcher and the group selection all move into the
dialog that owns them; the parent now only opens them and reacts to events.
`updateContactpersoonGroups` stays in the parent because it mutates the
parent's own organisationData — the dialog reports groups up rather than
reaching into it. Since both dialogs mount fresh per open, `data()` IS the
state reset the parent used to spell out by hand and `beforeUnmount` IS the
timeout cleanup. Every t('softwarecatalog', ...) string is preserved verbatim.
Two info-modal files rather than one shared component: the two sections render
materially different DOM (NcModal's own title chrome and a bare body, versus a
hand-painted h2 + Close footer + ~90 lines of :deep() typography). Sharing them
would need a variant flag switching between two disjoint templates and two
disjoint stylesheets, and converging them would have changed one section's
rendered output.
BOTH DIRECTIONS
A new vitest spec mounts each section with #info-content supplied. Against the
pre-fix wiring:
FAIL tests/vitest/sectionInfoSlot.spec.js > renders #info-content inside the info modal
AssertionError: expected false to be true
FAIL > prefers #info-content over #info when both are supplied
Tests 2 failed | 5 passed (7)
The #info case still PASSED there, which is what shows the test isolates the
bug rather than the harness. After the fix: 227 passed (21 files).
[gate-13] modal-isolation: FAIL - 3 file(s) -> PASS
No other gate moved: 19=291, 25=41, 26=3. (An earlier baseline appeared to
flip six gates; that baseline was captured while `npm ci` was still running.
Re-measured with node_modules present in both arms, gate-13 is the only
verdict that changes.)
TOOLCHAIN
Vitest could not mount an SFC: no Vue plugin, and environment 'node'. Added
@vitejs/plugin-vue + jsdom as devDependencies, a @nextcloud/vue stub alongside
the existing router/dialogs/l10n stubs, and made vitest.config.js an async
factory so the ESM-only plugin can be dynamic-imported from a CommonJS config.
The default environment stays 'node'; the new spec opts into jsdom per-file, so
no existing spec changes behaviour.
lint 0 errors; build compiles; 227/227 unit tests pass.
Contributor
Quality Report — ConductionNL/softwarecatalog @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 128/128 | |||
| npm | ✅ | ✅ 718/718 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-10 20:52 UTC
Download the full PDF report from the workflow artifacts.
…osed
The mis-named `info-content` slot had been hiding a second bug. Because
AlwaysVisibleSection only declared `<slot name="info" />`, Vue silently
dropped the four callers that passed `#info-content` — so their panels
were never rendered, and nothing could fail on them.
EmailConfiguration's panel documents the e-mail template placeholders:
Use placeholders like {{ organization.name }} and {{ user.email }}
Those braces are meant literally, but Vue compiles them as interpolation
against the component, which has no `organization` and no `user`. The
moment the slot name was fixed and the panel rendered for the first time
it threw `Cannot read properties of undefined (reading 'name')`, which
tripped the shared "no console errors" assertion in every Playwright
settings test.
`v-pre` keeps the braces as documentation. The other four info panels
were checked and render clean.
Also adds tests/vitest/settingsInfoPanels.spec.js, which renders the REAL
markup of every info panel under src/views/settings/sections/. The
existing sectionInfoSlot.spec.js proves the slot MECHANISM forwards
content, but it does so with synthetic probe markup — which is precisely
why it could not see this. Verified both ways: without `v-pre` the new
spec reproduces the exact TypeError from CI.
Contributor
Quality Report — ConductionNL/softwarecatalog @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 128/128 | |||
| npm | ✅ | ✅ 718/718 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-10 21:32 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gate-13(modal-isolation) reported three files with inlineNcModal/NcDialogmarkup. Fixing them surfaced a second, unrelated defect that no gate and no test could see, because its failure mode is silence — and fixing that one unmasked a third that had been latent since the day it was written.The slot bug
AlwaysVisibleSection.vuedeclared<slot name="info" />. Four callers pass<template #info-content>:src/views/settings/sections/UserGroupsConfiguration.vuesrc/views/settings/sections/EmailConfiguration.vuesrc/views/settings/sections/ArchiMateImportExport.vuesrc/views/settings/sections/OrganizationSynchronization.vueAll four also set
:has-info-content="true", so the ⓘ button rendered and opened a modal with nothing in it. Vue silently drops slot content addressed to a slot the child does not declare — no warning, no error, no failing test. The name came fromCollapsibleSection.vue, which really does useinfo-content; onlyVersionInformation.vueused the workinginfoname.Both sections now render
<slot name="info-content"><slot name="info" /></slot>, soinfo-contentwins andinfostays a fallback. All five callers render.The bug the slot fix unmasked
The first push turned Playwright E2E red: 6 failed / 70 passed, every failure a settings test asserting "no app console errors", every one reporting the same runtime error thrown while the settings page rendered:
Mapping offset
2:761407in the built bundle gives:which is
EmailConfiguration.vue:385:Those braces are literal documentation of the e-mail template placeholders an admin can use. Vue compiles them as interpolation against the component, which has no
organizationand nouser.The slot rename did not introduce this — it unmasked it. Because
AlwaysVisibleSectiononly ever declaredinfo, Vue had been silently dropping this panel's markup, so the broken interpolation was never evaluated. Fixing the slot made the panel render for the first time in its life; it threw immediately, and it poisoned the shared "no console errors" assertion across every settings test — which is why an unrelatedEolSyncSettingstest went red too.v-prekeeps the braces as documentation.Negative result, with the search space stated: the other three newly-rendering panels (
UserGroupsConfiguration,ArchiMateImportExport,OrganizationSynchronization) andVersionInformation's#infopanel were all rendered and checked. Exactly one offending line exists in the whole app.It was not the test double
The obvious, plausible, wrong conclusion was "the
@nextcloud/vuestub hid it". It did not. The real, unstubbedAlwaysVisibleSectionwas mounted under jsdom across six usage shapes — no info content,#infoonly,#info-contentonly, both, neither, and header-actions — and all six render clean with the realNcModal. The slot-forwarding mechanism was never the problem.The actual gap was different:
sectionInfoSlot.spec.jsexercised the mechanism with synthetic probe markup and never rendered any real caller's content. A mechanism test cannot see a defect in the content it does not render.So
tests/vitest/settingsInfoPanels.spec.jsrenders the real markup of every#info/#info-contentblock undersrc/views/settings/sections/. It carries a positive control asserting it found all five files, so a broken extractor cannot vacuously pass, and it asserts the literal braces appear in the rendered output — so "fixing" the crash by deleting the offending line fails too.The extraction
src/dialogs/ChangePasswordDialog.vueContactpersonenList.vuesrc/dialogs/ManageUserGroupsDialog.vueContactpersonenList.vuesrc/modals/AlwaysVisibleSectionInfoModal.vueAlwaysVisibleSection.vuesrc/modals/CollapsibleSectionInfoModal.vueCollapsibleSection.vueContactpersonenList.vuedrops 1563 → 954 lines. Password validation, the HIBP pwned-check, the debounce watcher and group selection move into the dialog that owns them; the parent only opens them and reacts to events.updateContactpersoonGroupsstays in the parent because it mutates the parent's ownorganisationData— the dialog reports groups up rather than reaching into it. Because both dialogs mount fresh per open,data()is the state reset the parent used to spell out by hand andbeforeUnmountis the timeout cleanup. Everyt('softwarecatalog', …)string is preserved verbatim.Two info-modal files rather than one shared component, deliberately: the two sections render materially different DOM (NcModal's own title chrome + a bare body, versus a hand-painted
<h2>+ Close footer + ~90 lines of:deep()typography). Sharing them would need a variant flag switching between two disjoint templates and two disjoint stylesheets, and converging them would have changed one section's rendered output.Both directions
Slot wiring. A vitest spec mounts each section with
#info-contentsupplied. Against the pre-fix wiring:The
#infocase still passed there — which is what shows the test isolates the bug rather than the harness.Info-panel rendering. With
v-prereverted,settingsInfoPanels.spec.jsreproduces the exact CI error string:After both fixes: 234 passed (22 files).
No gate got worse
19 = 291, 25 = 41, 26 = 3 — unchanged. 46 now passes (fixed by
274790f, included on this branch). 5 → 3 failing gates over 65 gate lines.Worth recording, because it nearly cost an afternoon: an earlier baseline appeared to flip six gates. That baseline had been captured while
npm ciwas still running — the world changed under the measurement, not the code. Re-measured withnode_modulespresent in both arms, gate-13 is the only verdict that changes. A baseline taken against a half-built environment measures the environment.Toolchain notes
Vitest could not mount an SFC: no Vue plugin, and
environment: 'node'. This adds@vitejs/plugin-vue+jsdomas devDependencies, a@nextcloud/vuestub alongside the existing router/dialogs/l10n stubs, and makesvitest.config.jsan async factory so the ESM-only plugin can be dynamic-imported from a CommonJS config. The default environment staysnode; the new specs opt into jsdom per-file, so no existing spec changes behaviour.@vue/compiler-domis added as a devDependency (@vue/compiler-sfcwas already one) forsettingsInfoPanels.spec.js. That spec compiles and renders each info-panel template fragment rather than mounting the whole section component — deliberately. Mounting a section drags in the Pinia stores, axios and the Nextcloud runtime that this OFFLINE suite exists to do without; it was attempted and fails on an unrelated CJS resolution issue (@nextcloud/vue's exports map declares norequirecondition). Rendering the fragment reproduces the real failure exactly, with none of that weight.npm run lint0 errors (475 pre-existing warnings) ·npm run buildcompiles · 234/234 unit tests pass.