Skip to content

feat(deep-link): bidirectional URL sync for dashboards#131

Merged
rubenvdlinde merged 3 commits intodevelopmentfrom
feature/dashboard-deeplinking
May 5, 2026
Merged

feat(deep-link): bidirectional URL sync for dashboards#131
rubenvdlinde merged 3 commits intodevelopmentfrom
feature/dashboard-deeplinking

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

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 active
  • Sidebar click → URL updates without reload via pushState
  • Browser back → previous dashboard returns
  • Stale / unknown slug → silent fallback to resolver, never 404
  • Dashboard with no slug → no URL update

Backend

  • Catch-all page#deepLink route on /{deepLink} with negative-lookahead (?!api(?:/|$)).+ excluding /api/.... Registered last in appinfo/routes.php.
  • PageController::deepLink() delegates to refactored index(string $deepLink = '') which resolves through DashboardTreeService::resolvePath(), falls back on failure (logged), and pushes the canonical path computed via computePath() into initial state as deepLinkPath.
  • New GET /api/dashboards/{uuid}/path endpoint returns the canonical slug-chain. Empty path is a valid response (NULL slugs are unaddressable).

Frontend

  • Views.vue replaces the URL on mount (replaceState) to match server, then watches activeDashboard.uuid and pushes a history entry on every switch. popstate listener strips the route prefix and re-resolves via the existing getDashboardByPath API.
  • api.getDashboardPath(uuid) — thin client.
  • loadInitialState reads optional deepLinkPath (empty default for backwards-compat).

Tests

  • DashboardApiControllerComputePathTest (4 cases) — auth, missing-uuid, happy path, empty-path-as-valid-response.
  • 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.

Risks

  • The catch-all is one route ordering away from breaking every API endpoint. The negative-lookahead is the contract; a Newman regression pins /api/health reachability. Anyone adding API routes BELOW this catch-all in routes.php will quietly shadow them.

(Re-open of PR #128 after feat/*feature/* branch rename.)

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.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Quality Report — ConductionNL/mydash @ 07062a2

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
@rubenvdlinde rubenvdlinde merged commit f653d10 into development May 5, 2026
49 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/dashboard-deeplinking branch May 5, 2026 21:32
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Quality Report — ConductionNL/mydash @ fde44f6

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Quality Report — ConductionNL/mydash @ fde44f6

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.

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.
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