feat: implement merge_missing_catalog_widgets function to enhance dashboard layout
- Added the `merge_missing_catalog_widgets` function to append missing widget IDs from the catalog to the dashboard layout while preserving the existing order. - Updated the admin and app dashboard routes to utilize the new function, ensuring that new catalog entries are visible without requiring users to reset their layouts. - Enhanced tests to validate the functionality of the new merging logic, ensuring proper integration with existing layouts. - Bumped application version to reflect these changes.
This commit is contained in:
parent
2453da0da1
commit
01c0d1745f
|
|
@ -5,6 +5,7 @@ Erlaubte Widget-IDs und Reihenfolge: widget_catalog.WIDGET_CATALOG.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
|
@ -25,6 +26,7 @@ __all__ = [
|
|||
"coalesce_effective_layout",
|
||||
"default_layout_dict",
|
||||
"lab_default_layout_dict",
|
||||
"merge_missing_catalog_widgets",
|
||||
"product_default_layout_dict",
|
||||
]
|
||||
|
||||
|
|
@ -52,6 +54,25 @@ def default_layout_dict() -> dict[str, Any]:
|
|||
return product_default_layout_dict()
|
||||
|
||||
|
||||
def merge_missing_catalog_widgets(layout: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Hängt fehlende Widget-IDs aus WIDGET_CATALOG an (enabled=False, leere config).
|
||||
Bestehende Reihenfolge bleibt erhalten — nötig, damit neue Katalog-Einträge in
|
||||
„Übersicht anpassen“ / Lab erscheinen, ohne dass Nutzer:innen das Layout resetten müssen.
|
||||
"""
|
||||
out = copy.deepcopy(layout)
|
||||
widgets: list[dict[str, Any]] = list(out.get("widgets") or [])
|
||||
seen: set[str] = {str(w["id"]) for w in widgets if w.get("id")}
|
||||
for e in WIDGET_CATALOG:
|
||||
wid = e["id"]
|
||||
if wid not in seen:
|
||||
widgets.append({"id": wid, "enabled": False, "config": {}})
|
||||
seen.add(wid)
|
||||
out["version"] = out.get("version", 1)
|
||||
out["widgets"] = widgets
|
||||
return out
|
||||
|
||||
|
||||
class DashboardWidgetEntry(BaseModel):
|
||||
id: str = Field(min_length=1, max_length=64)
|
||||
enabled: bool = True
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ from fastapi import APIRouter, HTTPException, Depends
|
|||
from db import get_db, get_cursor, r2d
|
||||
from auth import require_admin, hash_pin
|
||||
from models import AdminProfileUpdate
|
||||
from dashboard_layout_schema import ALLOWED_WIDGET_IDS, DashboardLayoutPayload, product_default_layout_dict
|
||||
from dashboard_layout_schema import (
|
||||
ALLOWED_WIDGET_IDS,
|
||||
DashboardLayoutPayload,
|
||||
merge_missing_catalog_widgets,
|
||||
product_default_layout_dict,
|
||||
)
|
||||
from dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||
from widget_catalog import WIDGET_CATALOG
|
||||
from widget_feature_requirements_db import (
|
||||
|
|
@ -184,7 +189,7 @@ def admin_get_dashboard_product_default(session: dict = Depends(require_admin)):
|
|||
"""Aktueller Produkt-Dashboard-Standard (DB oder Code)."""
|
||||
_ = session
|
||||
with get_db() as conn:
|
||||
layout = get_product_default_base_dict(conn)
|
||||
layout = merge_missing_catalog_widgets(get_product_default_base_dict(conn))
|
||||
from_database = get_stored_product_default_validated(conn) is not None
|
||||
code_ref = product_default_layout_dict()
|
||||
return {
|
||||
|
|
@ -217,7 +222,7 @@ def admin_delete_dashboard_product_default(session: dict = Depends(require_admin
|
|||
_ = session
|
||||
with get_db() as conn:
|
||||
delete_product_default_override(conn)
|
||||
layout = get_product_default_base_dict(conn)
|
||||
layout = merge_missing_catalog_widgets(get_product_default_base_dict(conn))
|
||||
return {"ok": True, "layout": layout, "from_database": False}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from dashboard_layout_schema import (
|
|||
DashboardLayoutPayload,
|
||||
coalesce_effective_layout,
|
||||
lab_default_layout_dict,
|
||||
merge_missing_catalog_widgets,
|
||||
)
|
||||
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
||||
from db import get_cursor, get_db
|
||||
|
|
@ -51,9 +52,11 @@ def get_dashboard_layout(
|
|||
raw = row["dashboard_layout"] if row else None
|
||||
custom, effective = coalesce_effective_layout(raw)
|
||||
with get_db() as conn:
|
||||
base_product = get_product_default_base_dict(conn)
|
||||
base_product = merge_missing_catalog_widgets(get_product_default_base_dict(conn))
|
||||
if not custom:
|
||||
effective = base_product
|
||||
else:
|
||||
effective = merge_missing_catalog_widgets(effective)
|
||||
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
||||
product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn)
|
||||
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from dashboard_layout_schema import (
|
|||
DashboardLayoutPayload,
|
||||
coalesce_effective_layout,
|
||||
default_layout_dict,
|
||||
merge_missing_catalog_widgets,
|
||||
)
|
||||
from widget_catalog import DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
||||
|
||||
|
|
@ -56,3 +57,19 @@ def test_coalesce_valid_raw():
|
|||
custom, eff = coalesce_effective_layout(raw)
|
||||
assert custom is True
|
||||
assert eff == raw
|
||||
|
||||
|
||||
def test_merge_missing_catalog_widgets_keeps_order_and_fills_ids():
|
||||
raw = {
|
||||
"version": 1,
|
||||
"widgets": [
|
||||
{"id": "kpi_board", "enabled": True, "config": {}},
|
||||
{"id": "welcome", "enabled": False, "config": {}},
|
||||
],
|
||||
}
|
||||
merged = merge_missing_catalog_widgets(raw)
|
||||
assert [w["id"] for w in merged["widgets"][:2]] == ["kpi_board", "welcome"]
|
||||
assert {w["id"] for w in merged["widgets"]} == ALLOWED_WIDGET_IDS
|
||||
extra = [w for w in merged["widgets"] if w["id"] not in ("kpi_board", "welcome")]
|
||||
assert all(w["enabled"] is False for w in extra)
|
||||
DashboardLayoutPayload.model_validate(merged)
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ MODULE_VERSIONS = {
|
|||
"photos": "1.0.0",
|
||||
"insights": "1.3.0",
|
||||
"prompts": "1.1.0",
|
||||
"admin": "1.4.0", # Widget × Feature-Zuordnung (Migration 041)
|
||||
"admin": "1.4.1", # Produkt-Dashboard-Standard GET/DELETE: merge_missing_catalog_widgets
|
||||
"stats": "1.0.1",
|
||||
"exportdata": "1.1.0",
|
||||
"importdata": "1.0.0",
|
||||
"membership": "2.1.0",
|
||||
"workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode)
|
||||
"app_dashboard": "1.12.0", # Widget body_history_viz (Verlauf body-history-viz Bundle)
|
||||
"app_dashboard": "1.12.1", # GET layout: merge_missing_catalog_widgets (neue Katalog-IDs sichtbar)
|
||||
"csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise
|
||||
"admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
|
||||
const CHART_DAYS_WIDGET_IDS = new Set([
|
||||
'body_overview',
|
||||
'body_history_viz',
|
||||
'activity_overview',
|
||||
'nutrition_detail_charts',
|
||||
'recovery_charts_panel',
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user