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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
|
|
@ -25,6 +26,7 @@ __all__ = [
|
||||||
"coalesce_effective_layout",
|
"coalesce_effective_layout",
|
||||||
"default_layout_dict",
|
"default_layout_dict",
|
||||||
"lab_default_layout_dict",
|
"lab_default_layout_dict",
|
||||||
|
"merge_missing_catalog_widgets",
|
||||||
"product_default_layout_dict",
|
"product_default_layout_dict",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -52,6 +54,25 @@ def default_layout_dict() -> dict[str, Any]:
|
||||||
return product_default_layout_dict()
|
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):
|
class DashboardWidgetEntry(BaseModel):
|
||||||
id: str = Field(min_length=1, max_length=64)
|
id: str = Field(min_length=1, max_length=64)
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ from fastapi import APIRouter, HTTPException, Depends
|
||||||
from db import get_db, get_cursor, r2d
|
from db import get_db, get_cursor, r2d
|
||||||
from auth import require_admin, hash_pin
|
from auth import require_admin, hash_pin
|
||||||
from models import AdminProfileUpdate
|
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 dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||||
from widget_catalog import WIDGET_CATALOG
|
from widget_catalog import WIDGET_CATALOG
|
||||||
from widget_feature_requirements_db import (
|
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)."""
|
"""Aktueller Produkt-Dashboard-Standard (DB oder Code)."""
|
||||||
_ = session
|
_ = session
|
||||||
with get_db() as conn:
|
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
|
from_database = get_stored_product_default_validated(conn) is not None
|
||||||
code_ref = product_default_layout_dict()
|
code_ref = product_default_layout_dict()
|
||||||
return {
|
return {
|
||||||
|
|
@ -217,7 +222,7 @@ def admin_delete_dashboard_product_default(session: dict = Depends(require_admin
|
||||||
_ = session
|
_ = session
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
delete_product_default_override(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}
|
return {"ok": True, "layout": layout, "from_database": False}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from dashboard_layout_schema import (
|
||||||
DashboardLayoutPayload,
|
DashboardLayoutPayload,
|
||||||
coalesce_effective_layout,
|
coalesce_effective_layout,
|
||||||
lab_default_layout_dict,
|
lab_default_layout_dict,
|
||||||
|
merge_missing_catalog_widgets,
|
||||||
)
|
)
|
||||||
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
||||||
from db import get_cursor, get_db
|
from db import get_cursor, get_db
|
||||||
|
|
@ -51,9 +52,11 @@ def get_dashboard_layout(
|
||||||
raw = row["dashboard_layout"] if row else None
|
raw = row["dashboard_layout"] if row else None
|
||||||
custom, effective = coalesce_effective_layout(raw)
|
custom, effective = coalesce_effective_layout(raw)
|
||||||
with get_db() as conn:
|
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:
|
if not custom:
|
||||||
effective = base_product
|
effective = base_product
|
||||||
|
else:
|
||||||
|
effective = merge_missing_catalog_widgets(effective)
|
||||||
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
||||||
product_adj = apply_entitlements_to_layout_dict(base_product, 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)
|
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from dashboard_layout_schema import (
|
||||||
DashboardLayoutPayload,
|
DashboardLayoutPayload,
|
||||||
coalesce_effective_layout,
|
coalesce_effective_layout,
|
||||||
default_layout_dict,
|
default_layout_dict,
|
||||||
|
merge_missing_catalog_widgets,
|
||||||
)
|
)
|
||||||
from widget_catalog import DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
from widget_catalog import DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
||||||
|
|
||||||
|
|
@ -56,3 +57,19 @@ def test_coalesce_valid_raw():
|
||||||
custom, eff = coalesce_effective_layout(raw)
|
custom, eff = coalesce_effective_layout(raw)
|
||||||
assert custom is True
|
assert custom is True
|
||||||
assert eff == raw
|
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",
|
"photos": "1.0.0",
|
||||||
"insights": "1.3.0",
|
"insights": "1.3.0",
|
||||||
"prompts": "1.1.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",
|
"stats": "1.0.1",
|
||||||
"exportdata": "1.1.0",
|
"exportdata": "1.1.0",
|
||||||
"importdata": "1.0.0",
|
"importdata": "1.0.0",
|
||||||
"membership": "2.1.0",
|
"membership": "2.1.0",
|
||||||
"workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode)
|
"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
|
"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)
|
"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([
|
const CHART_DAYS_WIDGET_IDS = new Set([
|
||||||
'body_overview',
|
'body_overview',
|
||||||
|
'body_history_viz',
|
||||||
'activity_overview',
|
'activity_overview',
|
||||||
'nutrition_detail_charts',
|
'nutrition_detail_charts',
|
||||||
'recovery_charts_panel',
|
'recovery_charts_panel',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user