feat(deep-link): bidirectional URL sync for dashboards#131
Merged
rubenvdlinde merged 3 commits intodevelopmentfrom May 5, 2026
Merged
feat(deep-link): bidirectional URL sync for dashboards#131rubenvdlinde merged 3 commits intodevelopmentfrom
rubenvdlinde merged 3 commits intodevelopmentfrom
Conversation
A dashboard now has its own URL — `/apps/mydash/{slug}` (slug-chains
work for nested dashboards: `/apps/mydash/finance/q1`). Visiting a
deep-link lands the workspace on the matching dashboard; switching
dashboards in the sidebar pushes a history entry so back/forward
navigates between dashboards.
Stale or unknown slugs fall back silently to the seven-step resolver
rather than 404 — old bookmarks always land on something.
Backend:
- Catch-all route `page#deepLink` on `/{deepLink}` with a negative-
lookahead requirement that excludes `/api/...`. Registered last so
every literal route wins first.
- `PageController::deepLink` delegates to a refactored `index($deepLink)`
that resolves the slug-chain through `DashboardTreeService`, falls
back to the resolver on failure (logged), and pushes the canonical
path (computed via `computePath`) into initial state as `deepLinkPath`
so the frontend can normalise the URL in-place.
- New `GET /api/dashboards/{uuid}/path` endpoint returns the canonical
slug-chain for a UUID. Empty path is a valid response (NULL slugs are
legal but unaddressable) — frontend treats it as "leave the URL alone".
Frontend:
- `loadInitialState` reads the optional `deepLinkPath` key with empty-
string default for backwards-compat with older deploys.
- Views.vue replaces the URL on mount via `history.replaceState` to
match what the server actually rendered, then watches
`activeDashboard.uuid` and pushes a new history entry on every switch.
- `popstate` listener strips the route prefix and re-resolves via the
existing `getDashboardByPath` API, then `switchDashboard()`.
Tests:
- New `DashboardApiControllerComputePathTest` (4 cases) pinning the
canonical-path endpoint contract.
- Newman: deep-link page render with known + unknown slugs (silent-
fallback contract); regression check that `/api/health` still routes
past the catch-all; canonical-path API envelope shape; empty-path
empty-uuid contract.
Contributor
Quality Report — ConductionNL/mydash @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 501/501 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 90.7% (127/140 statements)
Quality workflow — 2026-05-05 21:06 UTC
Download the full PDF report from the workflow artifacts.
…ard-deeplinking # Conflicts: # tests/integration/mydash.postman_collection.json
Contributor
Quality Report — ConductionNL/mydash @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 501/501 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 90.7% (127/140 statements)
Quality workflow — 2026-05-05 21:36 UTC
Download the full PDF report from the workflow artifacts.
Contributor
Quality Report — ConductionNL/mydash @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 501/501 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 90.7% (127/140 statements)
Quality workflow — 2026-05-05 21:39 UTC
Download the full PDF report from the workflow artifacts.
4 tasks
rubenvdlinde
added a commit
that referenced
this pull request
May 6, 2026
…ixes First end-to-end run of `--project docs-capture` against the local Nextcloud instance. 6/15 tests pass cleanly; the rest hit selector misses on UI markup that drifted since the spec was authored. Failures are non-fatal (each test re-navigates from `/apps/mydash/`) so the run captures everything reachable up to the first miss in each flow. Captured: - user track: 13 PNGs (U1 first launch + U2 add button + create modal, U3 edit-mode + widget picker, U4 edit mode + drag + reflow, U7 default marker, U8 url bar, U9 after switch) - admin track: 3 PNGs (A2 templates list + create modal, A3 group cog) Spec changes: - `mode: 'default'` instead of serial — selector misses no longer cascade to abort the suite. - U2 create-dashboard form: hooked on placeholder text from `DashboardConfigModal.vue` (`"My dashboard"`, `"What is this dashboard for?"`) instead of the non-existent `name=` attributes. - `playwright.config.ts`: per-project `testIgnore` so the chromium regression project skips the capture spec while the docs-capture project picks it up. The previous root-level ignore was shadowing the project's testMatch. Remaining selector misses live in the spec for follow-up: - U3 picker → form → added (the `Text` button selector misses its actual NcActions label) - U4 resize handle (`.ui-resizable-handle.ui-resizable-se` doesn't match the actual GridStack drag-handle class) - U5/U6 context menu (`.widget-context-menu` vs the actual class) - U7 fallback marker (depends on PR #130 deployment) - U8 deep-link landing (depends on PR #131 deployment) - U10 dashboard-config modal selector - A1 / A4 / A5 admin pages — most need an admin nav probe Each remaining failure dropped a `test-results/<test>/test-failed-1.png` that shows the page state at the moment of failure — those are useful debugging starting points.
rubenvdlinde
added a commit
that referenced
this pull request
May 6, 2026
Selector misses on the first capture-spec run came from depending on visible label text (which i18n shifts), CSS classes (refactor magnets), and aria-labels that map to multiple elements. Switched the high-traffic selectors to `data-testid` so the spec survives copy edits, class renames, and Vue refactors. Twelve new test ids: - `DashboardConfigModal.vue` — dashboard-name-input, dashboard-description-input, dashboard-save-button, dashboard-delete-button - `DashboardRowActions.vue` — cog-edit-dashboard, cog-dashboard-config, cog-add-widget, cog-set-default, cog-delete - `WidgetContextMenu.vue` — widget-context-menu (container), ctx-edit, ctx-remove, ctx-cancel - `AddWidgetModal.vue` — add-widget-save The `data-testid` attributes are inert in production renders (no behaviour, no styling) and don't change any user-facing markup. Specs target them via `[data-testid="…"]` selectors. Updated `tests/e2e/docs-screenshots.spec.ts` (U2, U3, U5, U6, U7, U10) to use the new ids. The remaining failure modes are env-specific (PR #130/#131 deployment for the ★ marker and deep-link landing) rather than selector brittleness. Note: the live env serves the previously-built bundle, so reshooting requires `npm run build` (or removing `js/mydash-main.js` so the playwright globalSetup auto-rebuilds) before `npx playwright test --project docs-capture`.
rubenvdlinde
added a commit
that referenced
this pull request
May 6, 2026
…nted bundle Built the worktree bundle (with the 28 new test-ids) and swapped it into the live nextcloud instance to validate the capture spec end-to-end. Bundle restored to the original after the run; live mydash unchanged. 9 of 15 capture tests passed cleanly, producing 26 unique PNGs: User track (passing): U1 first launch (3), U3 add widget (4 — picker + form + added now work via widget-type-select), U9 switch dashboards, U10 rename/delete (config modal + delete confirm now work via dashboard-name-input + dashboard-delete-button). Admin track (passing): A1 toggle (2), A2 templates (3), A3 group cog, A4 roles section + create modal, A5 bulk panel. Failures fall into three buckets: 1. Env-dependent (PR #131 server routes not deployed): - U7 fallback marker / U8 deep-link landed — the JS pushes URL state but the PHP catch-all route doesn't exist server-side, so navigation 404s mid-test. 2. Drag/right-click flakiness (timing, not selectors): - U4 reposition/resize — `mouse.move` + `mouse.down` against the GridStack handle is order-sensitive; the headed retry mode would stabilise, but a single-shot capture run misses on every hover. - U5 style editor / U6 remove — right-click event timing on the widget wrapper varies between runs. 3. Form-validation edge case: - U2 create dashboard — the existing test instance has many newman-fork dashboards already, so the auto-derived slug collides. Either de-collide via timestamp suffix in the spec or accept the rare miss. The PR is ready for review. The remaining selector misses live in `tests/e2e/docs-screenshots.spec.ts` and can be addressed in a follow-up; the docs themselves are complete.
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.
Summary
Each dashboard gets its own URL —
/apps/mydash/{slug-chain}. The sync is bidirectional: visiting a deep-link lands the workspace on the matching dashboard, switching dashboards in the sidebar pushes a new history entry, and back/forward buttons navigate between dashboards.Behaviour
/apps/mydash/finance/q1-roadmap→ loads with that dashboard activepushStateBackend
page#deepLinkroute on/{deepLink}with negative-lookahead(?!api(?:/|$)).+excluding/api/.... Registered last inappinfo/routes.php.PageController::deepLink()delegates to refactoredindex(string $deepLink = '')which resolves throughDashboardTreeService::resolvePath(), falls back on failure (logged), and pushes the canonical path computed viacomputePath()into initial state asdeepLinkPath.GET /api/dashboards/{uuid}/pathendpoint returns the canonical slug-chain. Empty path is a valid response (NULL slugs are unaddressable).Frontend
Views.vuereplaces the URL on mount (replaceState) to match server, then watchesactiveDashboard.uuidand pushes a history entry on every switch.popstatelistener strips the route prefix and re-resolves via the existinggetDashboardByPathAPI.api.getDashboardPath(uuid)— thin client.loadInitialStatereads optionaldeepLinkPath(empty default for backwards-compat).Tests
DashboardApiControllerComputePathTest(4 cases) — auth, missing-uuid, happy path, empty-path-as-valid-response./api/healthstill routes past the catch-all; canonical-path API envelope shape.Risks
/api/healthreachability. Anyone adding API routes BELOW this catch-all inroutes.phpwill quietly shadow them.(Re-open of PR #128 after
feat/*→feature/*branch rename.)