Merge vues sauvegardées #95

This commit is contained in:
Colin Maudry
2026-06-29 16:54:11 +02:00
16 changed files with 1848 additions and 13 deletions
+6
View File
@@ -29,6 +29,12 @@ SECTIONS = [
"href": "/compte/filtres",
"require_subscription": True,
},
{
"key": "vues",
"label": "Mes vues",
"href": "/compte/vues",
"require_subscription": True,
},
{
"key": "siret",
"label": "Mon SIRET",
+106
View File
@@ -0,0 +1,106 @@
import dash_bootstrap_components as dbc
from dash import (
ALL,
Input,
Output,
State,
callback,
ctx,
html,
no_update,
register_page,
)
from flask_login import current_user
from src.pages._compte_shell import account_guard, account_shell
from src.saved_views import db as saved_views_db
from src.saved_views import ui as saved_views_ui
register_page(
__name__,
path="/compte/vues",
title="Mes vues | decp.info",
name="Mes vues",
description="Gérez vos vues enregistrées du tableau des marchés.",
)
def _content():
views = saved_views_db.list_views(current_user.id, "tableau")
return html.Div(
[
html.H2("Mes vues"),
html.P(
"Les vues que vous enregistrez depuis le Tableau apparaissent ici. "
"Cliquez sur « Ouvrir » pour appliquer une vue."
),
html.Div(saved_views_ui.views_table(views), id="vues-list"),
dbc.Modal(
id="vue-rename-modal",
is_open=False,
children=[
dbc.ModalHeader(dbc.ModalTitle("Renommer la vue")),
dbc.ModalBody(
dbc.Input(id="vue-rename-input", type="text"),
),
dbc.ModalFooter(
dbc.Button("Renommer", id="vue-rename-confirm", color="primary")
),
],
),
dbc.Input(id="vue-rename-id", type="hidden"),
]
)
def layout(**_):
guard = account_guard("/compte/vues", require_subscription=True)
if guard is not None:
return guard
return account_shell("vues", _content())
@callback(
Output("vues-list", "children"),
Input({"type": "vue-delete", "index": ALL}, "n_clicks"),
prevent_initial_call=True,
)
def delete_view(n_clicks):
if not ctx.triggered_id or not any(n_clicks):
return no_update
saved_views_db.delete(ctx.triggered_id["index"], current_user.id)
views = saved_views_db.list_views(current_user.id, "tableau")
return saved_views_ui.views_table(views)
@callback(
Output("vue-rename-modal", "is_open"),
Output("vue-rename-id", "value"),
Input({"type": "vue-rename-open", "index": ALL}, "n_clicks"),
Input("vue-rename-confirm", "n_clicks"),
State("vue-rename-modal", "is_open"),
prevent_initial_call=True,
)
def toggle_rename_modal(_open, _confirm, is_open):
if isinstance(ctx.triggered_id, dict) and any(_open):
return True, str(ctx.triggered_id["index"])
return False, no_update
@callback(
Output("vues-list", "children", allow_duplicate=True),
Input("vue-rename-confirm", "n_clicks"),
State("vue-rename-id", "value"),
State("vue-rename-input", "value"),
prevent_initial_call=True,
)
def rename_view(_n, view_id, new_name):
clean = saved_views_ui.clean_view_name(new_name)
if not view_id or not clean:
return no_update
try:
saved_views_db.rename(int(view_id), current_user.id, clean)
except Exception:
return no_update
views = saved_views_db.list_views(current_user.id, "tableau")
return saved_views_ui.views_table(views)
+104 -13
View File
@@ -18,13 +18,18 @@ from dash import (
no_update,
register_page,
)
from flask_login import current_user
from src.db import query_marches, schema
from src.figures import DataTable, make_column_picker
from src.pages._compte_shell import current_user_has_subscription
from src.saved_views import db as saved_views_db
from src.saved_views import ui as saved_views_ui
from src.utils import get_data_update_timestamp, logger
from src.utils.seo import META_CONTENT
from src.utils.table import (
COLUMNS,
build_view_query,
filter_table_data,
get_default_hidden_columns,
invert_columns,
@@ -245,6 +250,50 @@ layout = [
className="column_list",
title="Choisir les colonnes à afficher et masquer",
),
html.Div(
id="saved-views-bar",
style={"display": "none"},
className="d-inline-flex align-items-center gap-2",
children=[
dbc.Button(
"Sauvegarder la vue",
id="btn-save-view",
title="Enregistrer les filtres, tris et colonnes actuels sous un nom",
),
dbc.DropdownMenu(
id="saved-views-menu",
label="Mes vues",
children=[],
className="d-inline-block",
),
],
),
dcc.Store(id="saved-views-refresh"),
dbc.Modal(
id="save-view-modal",
is_open=False,
children=[
dbc.ModalHeader(dbc.ModalTitle("Sauvegarder la vue")),
dbc.ModalBody(
[
dbc.Label("Nom de la vue"),
dcc.Input(
id="save-view-name",
type="text",
className="form-control",
),
html.Div(id="save-view-feedback", className="mt-2"),
]
),
dbc.ModalFooter(
dbc.Button(
"Enregistrer",
id="btn-save-view-confirm",
color="primary",
)
),
],
),
html.P("lignes", id="nb_rows"),
html.Div(id="copy-container"),
dcc.Input(id="share-url", readOnly=True, style={"display": "none"}),
@@ -425,19 +474,7 @@ def sync_url_and_reset_button(filter_query, sort_by, hidden_columns, href):
# Extract base URL (remove existing query params)
base_url = href.split("?")[0]
params = {}
if filter_query:
params["filtres"] = filter_query
if sort_by:
params["tris"] = json.dumps(sort_by)
if hidden_columns:
table_columns = invert_columns(hidden_columns)
table_columns = ",".join(table_columns)
params["colonnes"] = table_columns
query_string = urllib.parse.urlencode(params)
query_string = build_view_query(filter_query, sort_by, hidden_columns)
full_url = f"{base_url}?{query_string}" if query_string else base_url
copy_button = dcc.Clipboard(
@@ -548,3 +585,57 @@ def toggle_tableau_columns(click_open, click_close, is_open):
)
def reset_view(n_clicks):
return "", []
@callback(
Output("saved-views-bar", "style"),
Input("tableau_url", "pathname"),
)
def toggle_saved_views_bar(_pathname):
return saved_views_ui.bar_style(current_user_has_subscription())
@callback(
Output("save-view-modal", "is_open"),
Input("btn-save-view", "n_clicks"),
prevent_initial_call=True,
)
def toggle_save_view_modal(_open):
return True
@callback(
Output("save-view-modal", "is_open", allow_duplicate=True),
Output("save-view-feedback", "children"),
Output("saved-views-refresh", "data"),
Input("btn-save-view-confirm", "n_clicks"),
State("save-view-name", "value"),
State("tableau_datatable", "filter_query"),
State("tableau_datatable", "sort_by"),
State("tableau_datatable", "hidden_columns"),
prevent_initial_call=True,
)
def save_view(_n, name, filter_query, sort_by, hidden_columns):
has_sub = current_user_has_subscription()
clean_name, error = saved_views_ui.prepare_view_to_save(has_sub, name)
if error:
return True, html.Span(error, style={"color": "red"}), no_update
query = build_view_query(filter_query, sort_by, hidden_columns)
saved_views_db.upsert(current_user.id, "tableau", clean_name, query)
return (
False,
html.Span(f"Vue « {clean_name} » enregistrée.", style={"color": "green"}),
clean_name,
)
@callback(
Output("saved-views-menu", "children"),
Input("tableau_url", "pathname"),
Input("saved-views-refresh", "data"),
)
def populate_saved_views_menu(_pathname, _refresh):
if not current_user_has_subscription():
return []
views = saved_views_db.list_views(current_user.id, "tableau")
return saved_views_ui.saved_views_items(views)