From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 783F81FF0A5 for ; Fri, 04 Sep 2026 12:26:49 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 627A62159F; Fri, 04 Sep 2026 12:26:48 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Subject: [PATCH yew-widget-toolkit v2 2/3] widget: navigation drawer: allow persisting the expanded state Date: Fri, 4 Sep 2026 12:26:35 +0200 Message-ID: <20260904102643.3563579-3-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260904102643.3563579-1-d.csapak@proxmox.com> References: <20260904102643.3563579-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.547 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: TY32ODNTIYIMTQ53YD7RYCGE6E3HCHSI X-Message-ID-Hash: TY32ODNTIYIMTQ53YD7RYCGE6E3HCHSI X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: By exposing a `stateful_id` property that is used to save the `NavigationDrawerState` in the browser local storage. This is loaded on component creation and when the stateful id changes. Signed-off-by: Dominik Csapak --- src/widget/nav/navigation_drawer.rs | 60 ++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/widget/nav/navigation_drawer.rs b/src/widget/nav/navigation_drawer.rs index eae2ca4..890eb04 100644 --- a/src/widget/nav/navigation_drawer.rs +++ b/src/widget/nav/navigation_drawer.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::rc::Rc; +use serde::{Deserialize, Serialize}; use yew::html::{IntoEventCallback, IntoPropValue}; use yew::prelude::*; use yew::virtual_dom::{Key, VComp, VNode}; @@ -10,9 +11,9 @@ use pwt_macros::builder; use crate::css::{OverflowX, OverflowY}; use crate::props::{ AsClassesMut, AsCssStylesMut, ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, CssStyles, - EventSubscriber, IntoOptionalKey, IntoVTag, WidgetBuilder, WidgetStyleBuilder, + EventSubscriber, IntoOptionalKey, IntoVTag, StorageLocation, WidgetBuilder, WidgetStyleBuilder, }; -use crate::state::{NavigationContext, NavigationContextExt, Selection}; +use crate::state::{NavigationContext, NavigationContextExt, PersistentState, Selection}; use crate::{impl_class_prop_builder, impl_yew_std_props_builder}; use crate::dom::focus::roving_tabindex_next_recursive; @@ -82,6 +83,14 @@ pub struct NavigationDrawer { #[builder] #[prop_or(true)] animated: bool, + + /// If set, saves the open/collapsed state in the browser local storage + /// + /// The value is used as the storage key, so it has to be unique within the application. Only + /// the menu items that deviate from their default state are stored. + #[builder(IntoPropValue, into_prop_value)] + #[prop_or_default] + stateful_id: Option, } impl AsClassesMut for NavigationDrawer { @@ -147,8 +156,10 @@ impl NavigationDrawer { } /// A struct that holds the current state that can get saved to local storage -#[derive(Default, Clone, PartialEq)] +#[derive(Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] struct NavigationDrawerState { + #[serde(default, skip_serializing_if = "HashSet::is_empty")] changed_expanded: HashSet, // contains all keys that deviate from their default } @@ -185,6 +196,7 @@ pub struct PwtNavigationDrawer { selection: Selection, default_collapsed: HashMap, state: NavigationDrawerState, + stateful_id: Option, // just a local copy so we don't have pass ctx around _nav_ctx_handle: Option>, } @@ -474,10 +486,39 @@ impl PwtNavigationDrawer { let key_string = key.to_string(); let default_open = !self.default_collapsed.get(key).unwrap_or(&false); // only record the keys that deviate from their default, so the set stays minimal - if expanded == default_open { - self.state.changed_expanded.remove(&key_string); + let modified = if expanded == default_open { + self.state.changed_expanded.remove(&key_string) } else { - self.state.changed_expanded.insert(key_string); + self.state.changed_expanded.insert(key_string) + }; + if modified { + self.save_state(); + } + } + + /// Loads the deviations from the default state from the local storage, if an `id` is set. + fn load_state(&mut self) { + if let Some(id) = self.stateful_id.clone() { + let state = + PersistentState::>::new(StorageLocation::Local(id)); + if let Some(state) = state.into_inner() { + self.state = state; + } + } + } + + /// Saves the deviations from the default state to the local storage, if an `id` is set. + fn save_state(&self) { + if let Some(id) = self.stateful_id.clone() { + let mut state = + PersistentState::>::new(StorageLocation::Local(id)); + + let drawer_state = if self.state == NavigationDrawerState::default() { + None + } else { + Some(self.state.clone()) + }; + state.update(drawer_state); } } } @@ -533,9 +574,12 @@ impl Component for PwtNavigationDrawer { selection, default_collapsed: collect_default_collapse_states(&props.menu), state: NavigationDrawerState::default(), + stateful_id: props.stateful_id.clone(), _nav_ctx_handle, }; + this.load_state(); + // expand the path to the initially active item, so a deep-linked entry is visible if let Some(active) = &active { this.open_ancestors(props, active); @@ -667,6 +711,10 @@ impl Component for PwtNavigationDrawer { } self.default_collapsed = new_default_collapsed; } + if props.stateful_id != old_props.stateful_id { + self.stateful_id = props.stateful_id.clone(); + self.load_state(); + } true } -- 2.47.3