Skip to content

feat(router): move pipelinq off hash routing to clean path URLs - #1684

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/pipelinq-history-routing
Sep 1, 2026
Merged

feat(router): move pipelinq off hash routing to clean path URLs#1684
rubenvdlinde merged 1 commit into
developmentfrom
feat/pipelinq-history-routing

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Sixth app off # routing, after stackiq (#899), zaakafhandelapp (#609), opencatalogi (#1341), keepiq (#582) and larpinq (#651).

Source: two lines and a helper

  1. createWebHashHistorycreateWebHistory.
  2. routerBase(), derived from the URL actually being served. Nextcloud serves the app under both /apps/pipelinq/... and /index.php/apps/pipelinq/...; generateUrl() returns only one. Arriving on the other leaves the path outside the router base and the catch-all redirects to / — no error, deep link silently swallowed. This suite pins the /index.php form in several specs, so without it every deep link the suite makes would break.

The test surface was the bulk, and not only URLs

what change
30 spec files ${APP}/#/x and /apps/pipelinq/#/x → real paths
navigation.spec.ts REQUIRED_ENTRIES mapped page ids to hash hrefs and asserted equality → route paths compared as a suffix
dashboard.spec.ts default-route URL regex; KPI tile selector a[href^="#/"] and its /^#\// assertion
xwiki + declarative-view-system 9 toHaveURL(/#\/…/) assertions
helpers/pipelinq.ts revealNavEntry() selected leaves by href*="#/" — which matches nothing once the shell leaves hash routing

The suffix comparison is deliberate: the base legitimately differs between the two URL forms, so pinning either one would fail on the other for a reason that is not a routing defect. Group captions keep their bare href="#", so the app segment remains a clean discriminator between leaves and captions.

Stale prose was corrected rather than deleted in three files — all told the reader that a path deep-link resets the SPA to the Dashboard, which was true and is now the opposite.

Verification

URL result
/apps/pipelinq/ Sales overview
/apps/pipelinq/leads Leads
/index.php/apps/pipelinq/leads Leads
/index.php/apps/pipelinq/clients Clients

No hash anywhere, and nav hrefs are real paths carrying whichever base the page was loaded under — exactly why the suffix assertion replaced the equality one.

e2e: navigation.spec.ts + dashboard.spec.ts 9/9, event-wiring.spec.ts 3/3. eslint 0, prettier --check on the full glob clean, webpack compiles.

Sixth app off `#` routing, after stackiq (softwarecatalog#899),
zaakafhandelapp (#609), opencatalogi (#1341), keepiq (#582) and larpinq
(#651).

Two source parts:

1. createWebHashHistory -> createWebHistory.

2. routerBase(), derived from the URL actually being served. Nextcloud
   serves the app under BOTH /apps/pipelinq/... and
   /index.php/apps/pipelinq/..., but generateUrl() returns only the form
   the instance is configured for. Arriving on the other leaves the path
   outside the router base, vue-router cannot resolve it, and the
   catch-all redirects to '/' -- no error, deep link silently swallowed.
   This suite pins the /index.php form in several specs, so without it
   every deep link the suite makes would break.

The test surface was the bulk of the work, and it was not only URLs:

  30 spec files       `${APP}/#/x`, `/apps/pipelinq/#/x` -> real paths
  navigation.spec.ts  REQUIRED_ENTRIES mapped page ids to HASH hrefs and
                      asserted equality; now route paths compared as a
                      SUFFIX, because the base legitimately differs
                      between the two URL forms and pinning either one
                      would fail on the other for a non-defect
  dashboard.spec.ts   default-route URL regex, and the KPI tile selector
                      `a[href^="#/"]` + its `/^#\\//` assertion
  xwiki + declarative-view-system   9 toHaveURL(/#\\/…/) assertions
  helpers/pipelinq.ts revealNavEntry() selected leaves by `href*="#/"`,
                      which matches NOTHING once the shell leaves hash
                      routing. Leaves are now selected by the app segment;
                      group captions keep their bare `href="#"`, so that
                      remains the discriminator between the two.

Stale prose corrected rather than deleted in helpers/pipelinq.ts,
navigation.spec.ts and the event-wiring spec: all three told the reader a
path deep-link resets the SPA to the Dashboard, which was true and is now
the opposite.

Verified against the published @conduction/nextcloud-vue
(USE_LOCAL_LIB=false):

  /apps/pipelinq/                    -> Sales overview
  /apps/pipelinq/leads               -> Leads
  /index.php/apps/pipelinq/leads     -> Leads
  /index.php/apps/pipelinq/clients   -> Clients

No hash anywhere, and nav hrefs are real paths carrying whichever base
the page was loaded under -- which is exactly why the suffix assertion
replaced the equality one.

e2e: navigation.spec.ts + dashboard.spec.ts 9/9, event-wiring.spec.ts
3/3. eslint exits 0 and prettier --check on the FULL glob is clean.
@rubenvdlinde
rubenvdlinde merged commit e880584 into development Sep 1, 2026
1 check passed
@rubenvdlinde
rubenvdlinde deleted the feat/pipelinq-history-routing branch September 1, 2026 08:51
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/pipelinq @ 6b7a684

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 637/637
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-01 08:55 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Sep 1, 2026
…lf-done (#1694)

development went from 2 failing specs to 54 after #1684 moved pipelinq off
hash routing. The migration updated 34 files but left two things behind.

## visual-coverage-spa-pages.spec.ts: 40+ of the 54

openSpaRoute changed its goto and not its assertion:

  -  page.goto(`/apps/pipelinq/#${hash}`)
  +  page.goto(`/apps/pipelinq/${hash}`)

with `hash` values like "/my-work". That is TWO bugs in one line change:

  - the URL became /apps/pipelinq//my-work, a double slash, because the base
    already ends in "/" and the argument already starts with one;
  - the assertion three lines below still built `new RegExp("#" + hash + "$")`,
    so it waited 10s for a "#" that path routing never produces.

The observed failure was "Received: http://localhost:8080/apps/pipelinq/",
the catch-all redirect to the Dashboard, which is exactly what the comment
above it predicts an unmatched route does.

Renamed the parameter `hash` -> `route` so the next reader is not told it is
a hash, and rewrote that comment: the surviving PATH is now the proof the
route matched.

## Eight assertions in four other specs

pipeline, pipelinq-pos-grouping, pos-transaction-core and request-management
still carried /#\\/…/ patterns for navClick targets and one href check.

⚠️ Third time today this exact trap has cost a red development: a
pull_request into development SKIPS the E2E job, so a routing or naming
change merges green and only breaks afterwards, in specs the PR never
touched. #1672 did it with a nav rename, #1684 with the router, and both
were green when merged.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Sep 1, 2026
… URLs (#1703)

The last hash-routed surface in the fleet. The seven app shells went first
(softwarecatalog#899, zaakafhandelapp#609, opencatalogi#1341, keepiq#582,
larpinq#651, pipelinq#1684, openregister#3270); pipelinq's customer portal
is a SECOND entry point (src/portal.js) and was left behind.

The server already serves it: `portalPage#subpath` answers /portal/{path}
with requirement `^(?!api/).*`, so every portal deep link reaches the SPA
shell while /portal/api/* stays with the real controllers. That api
exclusion is precisely what makes history mode safe here — and it is not
hypothetical, because the booking client WAS calling /portal/services and
getting the shell back at HTTP 200 until #1697.

Two source parts:

1. createWebHashHistory -> createWebHistory.

2. routerBase(), derived from the URL actually being served. Nextcloud
   serves the portal under BOTH /apps/pipelinq/portal/... and
   /index.php/apps/pipelinq/portal/..., but generateUrl() returns only the
   form the instance is configured for. Arriving on the other leaves the
   path outside the router base, vue-router resolves nothing, and the
   portal lands on its default route — which for a customer-facing booking
   link means the booking is silently lost with no error shown.

Test changes, and one of them is not a URL:

  appointment-booking.spec.ts    BOOKING_ROUTE dropped its `#`
  visual-coverage-booking-portal openPortalRoute() built `${BASE}#${hash}`.
                                 ⚠️ Its access-proof assertion ALSO read the
                                 hash — `toHaveURL(new RegExp('#' + hash))`
                                 — and renaming the parameter alone left
                                 that behind as `ReferenceError: hash is not
                                 defined`. The assertion now matches the
                                 PATH suffix and keeps its original job:
                                 installPortalGuard() rewrites to /login for
                                 anything without meta.public, so a URL that
                                 still ends in the requested route proves
                                 both that the route matched and that it was
                                 classified public (ADR-005).

Also corrected three stale comments that told the reader pipelinq is
hash-routed — ci-seed.sh, visual-coverage-export-pages and
visual-coverage-spa-pages. All three were left over from #1684 and now
describe the opposite of what the code does. ci-seed.sh's mattered most:
it justified NOT worrying about overwrite.cli.url by saying hash history
makes the path prefix irrelevant. That immunity is real but it now comes
from routerBase(), not from the hash, and the comment said otherwise.

Verified against the dev instance, with a build of this branch served to
the browser:

  /apps/pipelinq/portal/book/haircut-simple             resolves
  /index.php/apps/pipelinq/portal/book/haircut-simple   resolves
  RELOAD on both                                        resolves

No hash in either URL. The reload is the case hash mode existed to avoid.
Both were run twice to separate flaky from broken — the shared dev
container drops into maintenance mode intermittently, which fails
global-setup at `input[name="user"]` and reads exactly like a routing
break.

e2e: visual-coverage-booking-portal + portal-accessibility, 8/8 pass.
eslint 0 errors, prettier --check on the FULL glob clean, webpack
compiles, and `createWebHashHistory` appears zero times in the portal
bundle.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Sep 1, 2026
* fix(e2e): follow the shell off hash routing onto path deep links

#1684 moved pipelinq's shell to createWebHistory and #1703 did the same for
the customer portal. The e2e suite kept navigating by hash, so every
'#/<route>' goto mounted the app at its default route: the shell was present
and the assertions read a Dashboard that was never the page under test. That
is the whole of the 48-failure run — pages.spec alone lost all eleven index
routes to it.

Two shapes were wrong:

  - The route constants and the portal gotos still carried a '#'.
  - Six shared helpers joined a leading-slash route onto a trailing-slash
    base ('/apps/pipelinq/' + '/bookings'), which had been harmless while the
    '#' separated them and became '/apps/pipelinq//bookings' without it. A
    doubled slash falls outside the router base, so it resolved nowhere.

The portal specs passed on the previous run only because #1703 had not landed
yet; they carry the same defect and are fixed here too.

* style(e2e): wrap the operational nav locator to prettier's width

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Sep 2, 2026
…a load (#1724)

Both failed with a bare 'Test timeout of 60000ms exceeded' and no failed
assertion, which reads as a hang rather than as work that no longer fits. Same
shape as billing-categories in #1713 and pipelinq-pos-grouping in #1716.

commercial-dashboard ran openApp() then goto('/operational') then reload().
Under hash routing only the first was a document load. Since the shell moved
to createWebHistory all three boot the app, and openApp() lands on the
Commercial dashboard, which is the one page this test does not want. Its
comment claimed the deep link had to be a hash because a path-form goto boots
the shell at the default dashboard — true before #1684, and now the opposite.

appointment-booking called openApp() and then gotoHash() twice. gotoHash()
already waits for #content-vue and clears both overlays, so openApp() was
purely a third load onto a Dashboard the test never looks at.

Neither raises a timeout: both stop paying for loads they do not need.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde pushed a commit that referenced this pull request Sep 2, 2026
…eeds

Follow-up in the same PR. The nc-vue 2.30.0 bump fixed the defect these four
tests were reporting, and it changed their failure MODE rather than clearing
them:

  before the bump   Error: toHaveCount failed — cn-report-card, expected 4,
                    received 0
  after the bump    Test timeout of 60000ms exceeded, with NO assertion error
                    at all

That second shape is the tell. No assertion failed, so the cards render now;
the tests simply ran out of budget. Each one calls openApp() — which boots
the shell and dismisses the walkthrough and support dialogs — then does a
full navigation, and `type:"reports"` is mapped through
defineAsyncComponent, so the first of them also pays for fetching that chunk.
A bare timeout reads as "the page is broken" when it means "this test is too
slow", which is exactly how the openregister route sweep misreported itself
earlier today.

180s, with the reasoning written where the next reader will hit it.

Also corrected two comments that survived #1684 and now say the opposite of
what the code does: both told the reader that a path-form goto boots the
shell at the Dashboard and the route must travel in the hash. The shell has
been on createWebHistory(routerBase()) since #1684, and the code beneath
those comments already uses paths.

⚠️ Verified in CI rather than locally, deliberately. Every `page.goto` in
this spec exceeds even the old 30s budget on this machine — a page load
against the shared dev container takes ~13s — and a full local run of the
file was OOM-killed. The targeted check that mattered did pass here:
/apps/pipelinq/reports renders 4 cn-report-card elements with 2.30.0 built
from this branch.
rubenvdlinde added a commit that referenced this pull request Sep 2, 2026
… page (#1731)

* chore(deps): bump @conduction/nextcloud-vue to 2.30.0 for the Reports page

The four `tests/e2e/rapportage.spec.ts` failures on development are the last
of that suite's red, and they are not a test problem: the Reports page has
rendered EMPTY since #1691 shipped it.

Root cause, in the library rather than here. `CnReportsPage` declared two
props — `page` and `translate` — and read its cards from
`this.page.config.cards`. `CnPageRenderer.resolvedProps()` returns
`{ ...topLevel, ...normalizedConfig, ...params }`: it spreads the page's
CONFIG KEYS as top-level props and never passes a `page` object. So `page`
was always `{}`, `cards` was always `[]`, and the component rendered its
empty state for every consumer. The contrast makes the contract obvious —
CnIndexPage declares 96 props precisely because it reads the spread.

Confirmed at runtime rather than inferred: the page MOUNTS (its
`cn-reports-empty` marker is present) with zero `cn-report-card` elements,
which is exactly the 0 CI reported against an expected 4.

nextcloud-vue#920 fixed it by declaring `cards`, `categories`,
`description`, `title` and the label props, with 82 lines of new unit tests,
and 2.30.0 published it. This bump is all that is needed here.

Verified against a build of this branch: /apps/pipelinq/reports renders 4
`cn-report-card` elements and the "Channel analytics" card is visible.

⚠️ The rest of rapportage.spec.ts cannot be judged on this machine — every
`page.goto` in it exceeds the spec's 30s budget because a page load takes
~13s against the shared dev container. In CI those same navigations complete
and the failure was the card assertion, which is what this fixes. CI is the
instrument for the remainder.

eslint 0 errors, prettier --check on the FULL glob clean, 59 vitest tests
pass.

⚠️ Two corrections to earlier reports of mine, both from measuring the wrong
world. The export-pages family was already fixed by #1706 and #1711 while I
was working, so development's E2E is 4 failed / 328 passed rather than the
44 I quoted from a stale branch. And my first reading said the `reports`
page type was unimplemented — that was a stale local node_modules (2.27.2
against a lockfile pinning 2.29.0) plus a bundle-override that did not match
`?v=` query strings. Both instruments were wrong; the CI log settled it.

* fix(e2e): give the rapportage specs the budget the Reports page now needs

Follow-up in the same PR. The nc-vue 2.30.0 bump fixed the defect these four
tests were reporting, and it changed their failure MODE rather than clearing
them:

  before the bump   Error: toHaveCount failed — cn-report-card, expected 4,
                    received 0
  after the bump    Test timeout of 60000ms exceeded, with NO assertion error
                    at all

That second shape is the tell. No assertion failed, so the cards render now;
the tests simply ran out of budget. Each one calls openApp() — which boots
the shell and dismisses the walkthrough and support dialogs — then does a
full navigation, and `type:"reports"` is mapped through
defineAsyncComponent, so the first of them also pays for fetching that chunk.
A bare timeout reads as "the page is broken" when it means "this test is too
slow", which is exactly how the openregister route sweep misreported itself
earlier today.

180s, with the reasoning written where the next reader will hit it.

Also corrected two comments that survived #1684 and now say the opposite of
what the code does: both told the reader that a path-form goto boots the
shell at the Dashboard and the route must travel in the hash. The shell has
been on createWebHistory(routerBase()) since #1684, and the code beneath
those comments already uses paths.

⚠️ Verified in CI rather than locally, deliberately. Every `page.goto` in
this spec exceeds even the old 30s budget on this machine — a page load
against the shared dev container takes ~13s — and a full local run of the
file was OOM-killed. The targeted check that mattered did pass here:
/apps/pipelinq/reports renders 4 cn-report-card elements with 2.30.0 built
from this branch.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant