Skip to content

Dashboard layout customization: reorder + hide/show sections#68

Open
ndandan wants to merge 13 commits into
Shoshuo:mainfrom
ndandan:pr/dashboard-layout-customization
Open

Dashboard layout customization: reorder + hide/show sections#68
ndandan wants to merge 13 commits into
Shoshuo:mainfrom
ndandan:pr/dashboard-layout-customization

Conversation

@ndandan

@ndandan ndandan commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What

Lets an admin reorder the dashboard's content sections and hide/show them, with two surfaces:

  • On the dashboard — an admin-only Edit layout mode: drag sections to reorder, click Hide to drop one, then Save.
  • In Settings → Display — a Dashboard layout card (drag-to-reorder list + visibility toggles) that mirrors the existing Sidebar visibility card. This is also where a hidden section is brought back.

The Hero stays pinned at the top; the seven content sections (upcoming, requests, health, plex, watchlist, trending, recent) are the reorderable/hideable set.

Why

Today the dashboard renders a fixed order and section visibility is driven only by which services are configured. Users with different priorities (or unconfigured services) want to tailor what shows and in what order.

How

  • DashboardSections — a static registry that is the single source of truth for section keys + default order.
  • DashboardLayoutService — resolves the stored order + per-section hidden flags into an ordered {key, visible} list. Modeled on ThemeService: final, implements ResetInterface, caches per request. Unknown/duplicate keys are dropped and any missing section is appended in default order, so a section added in a future release auto-appears instead of vanishing.
  • Storage is global, in the flat setting table, reusing the sidebar-visibility pattern: dashboard_section_order (comma-joined keys) + dashboard_hide_<key> ('1' = hidden). No migration; a fresh install with no stored layout falls back to the default order, all visible.
  • The dashboard's inline sections were extracted into templates/dashboard/sections/_*.html.twig partials and rendered in a data-driven loop; each partial keeps its existing services_configured.* gate, so a section shows only when visible AND configured.
  • Two write paths, kept in sync on the same keys: the existing settings form (saveSubmitted) and a small admin-gated POST /admin/settings/dashboard-layout endpoint for the on-dashboard editor.
  • Admin-only throughout (class-level #[IsGranted('ROLE_ADMIN')]; the dashboard toolbar is gated by is_granted('ROLE_ADMIN')). Reordering uses native HTML5 drag-and-drop — no new dependencies.

Testing

  • 28 unit tests (113 assertions), pristine output:
    • DashboardSectionsTest — registry shape/order/validity.
    • DashboardLayoutServiceTest — resolution: default order, custom order, unknown-key drop, dedupe, hidden flags, caching/reset.
    • AdminSettingsControllerTest — settings save persists order + hidden flags; the new endpoint persists and returns JSON.
  • lint:twig clean across the new partials + templates.
  • Live-tested on a self-hosted instance: reorder + hide/show through both surfaces, persistence across reloads, and cross-surface consistency (a change on the dashboard reflects in the settings card and vice-versa).

Notes

  • Empty/unconfigured sections render a zero-height wrapper; edit mode correctly skips those (no orphan drag handle or Hide button).
  • Scoped strictly to this feature — no unrelated changes.

ndandan and others added 13 commits June 26, 2026 14:40
…variant

The testResolutionIsCachedUntilReset test was a tautology that always
passed because the ConfigService mock returned the same values every time.
The test called resolve(), reset(), resolve() and compared the result arrays
with assertSame, which would be equal regardless of whether caching worked.

Rewrote the test to verify the caching invariant by counting ConfigService::get()
calls: 8 on first resolve, 0 additional on second (cache hit), 8 more after
reset. This ensures the test fails if caching is removed or broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the 7 inline dashboard sections (upcoming, requests, health,
plex, watchlist, trending, recent) into per-section Twig partials under
templates/dashboard/sections/. Each partial wraps its markup in a
data-section-key="<key>" div and keeps its existing service-config gate
inside. Requests and Health, previously sharing one .row, become two
independent full-width blocks (col-12 in their own row each).

DashboardController now injects DashboardLayoutService and passes
dashboard_layout (resolved order + visibility) to the view; index.html.twig
replaces all inline section markup with a loop over dashboard_layout,
including only visible sections via their partial.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tance in test

- Restore `final` keyword on DashboardLayoutService class declaration to
  match the design pattern established by ThemeService
- In AdminSettingsControllerTest helper, replace createMock() with real
  instance construction using the mocked ConfigService already available
  as a parameter
- Remove orphaned docblock before loadDashboardLayout() that was left
  behind during method extraction

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Add two assertions to testDashboardLayoutEndpointPersistsOrderAndHidden:
1. Assert JSON response body equals ['ok' => true]
2. Assert config->invalidate() is called exactly once

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d removeOverlays

Added idempotency guard (toolbar.dataset.layoutBound) to prevent double-init
of drag/button listeners on turbo:load + immediate invocation. Removed unused
removeOverlays() function (both Save/Cancel paths call window.location.reload).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A gated-out section partial (empty watchlist, unconfigured service)
still emits a zero-height [data-section-key] wrapper. Edit mode was
giving it a drag handle and a Hide button that floated onto the next
section's header. Skip offsetHeight===0 wrappers in addOverlays() and
as drag targets so only sections with rendered content are editable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the suite convention (AdminSettingsControllerTest) so the new
DashboardLayoutServiceTest's ConfigService mocks don't emit PHPUnit
"mock without expectations" notices — keeps test output pristine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DashboardController constructor gained a DashboardLayoutService
parameter; pass it (a real instance over a mocked ConfigService, since
the service is final) at all six construction sites so the full suite
matches the current arity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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