From 3d09b489b970b8b5c79554eb9b5bc94481cd979d Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 29 Jun 2026 16:22:54 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20helpers=20UI/validation=20des=20vues=20?= =?UTF-8?q?sauvegard=C3=A9es=20#95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/saved_views/ui.py | 69 ++++++++++++++++++++++++++++++++++++ tests/saved_views/test_ui.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/saved_views/ui.py create mode 100644 tests/saved_views/test_ui.py diff --git a/src/saved_views/ui.py b/src/saved_views/ui.py new file mode 100644 index 0000000..1b17f8b --- /dev/null +++ b/src/saved_views/ui.py @@ -0,0 +1,69 @@ +import dash_bootstrap_components as dbc +from dash import html + + +def bar_style(has_subscription: bool) -> dict: + return {} if has_subscription else {"display": "none"} + + +def clean_view_name(name: str | None) -> str: + return (name or "").strip() + + +def prepare_view_to_save( + has_subscription: bool, name: str | None +) -> tuple[str | None, str | None]: + if not has_subscription: + return None, "Réservé aux abonné·es." + clean = clean_view_name(name) + if not clean: + return None, "Veuillez saisir un nom pour la vue." + return clean, None + + +def saved_views_items(views) -> list: + return [ + dbc.DropdownMenuItem(view["name"], href=f"/tableau?{view['query']}") + for view in views + ] + + +def _view_row(view) -> html.Div: + view_id = view["id"] + return html.Div( + className="saved-view-row d-flex align-items-center gap-2 mb-2", + children=[ + html.Span(view["name"], className="flex-grow-1"), + dbc.Button( + "Ouvrir", + href=f"/tableau?{view['query']}", + color="link", + size="sm", + ), + dbc.Button( + "Renommer", + id={"type": "vue-rename-open", "index": view_id}, + color="secondary", + outline=True, + size="sm", + ), + dbc.Button( + "Supprimer", + id={"type": "vue-delete", "index": view_id}, + color="danger", + outline=True, + size="sm", + ), + ], + ) + + +def views_table(views) -> html.Div: + if not views: + return html.Div( + html.P( + "Vous n'avez pas encore de vue enregistrée. " + "Créez-en une depuis le Tableau, bouton « Sauvegarder la vue »." + ) + ) + return html.Div([_view_row(v) for v in views]) diff --git a/tests/saved_views/test_ui.py b/tests/saved_views/test_ui.py new file mode 100644 index 0000000..88a1678 --- /dev/null +++ b/tests/saved_views/test_ui.py @@ -0,0 +1,59 @@ +from src.saved_views import ui + + +class _Row(dict): + """Imite un sqlite3.Row : accès par clé.""" + + +def _view(view_id, name, query): + return _Row(id=view_id, name=name, query=query) + + +def test_bar_style_hidden_for_non_subscriber(): + assert ui.bar_style(False) == {"display": "none"} + assert ui.bar_style(True) == {} + + +def test_clean_view_name_strips_and_empties(): + assert ui.clean_view_name(" Ma vue ") == "Ma vue" + assert ui.clean_view_name(" ") == "" + assert ui.clean_view_name(None) == "" + + +def test_prepare_refuses_non_subscriber(): + name, err = ui.prepare_view_to_save(False, "Ma vue") + assert name is None + assert err + + +def test_prepare_refuses_empty_name(): + name, err = ui.prepare_view_to_save(True, " ") + assert name is None + assert err + + +def test_prepare_accepts_valid(): + name, err = ui.prepare_view_to_save(True, " Ma vue ") + assert name == "Ma vue" + assert err is None + + +def test_saved_views_items_build_links(): + items = ui.saved_views_items( + [_view(1, "Vue A", "filtres=a"), _view(2, "Vue B", "tris=b")] + ) + assert len(items) == 2 + assert items[0].href == "/tableau?filtres=a" + assert items[0].children == "Vue A" + + +def test_views_table_empty_state(): + out = ui.views_table([]) + # un Div non vide (message d'état) sans item de suppression + assert out is not None + + +def test_views_table_lists_views(): + out = ui.views_table([_view(1, "Vue A", "filtres=a")]) + text = str(out) + assert "Vue A" in text