From 3a7337fadc4d960910e8a3f7b7b0ce6950dcae52 Mon Sep 17 00:00:00 2001 From: Sarah Obasi Date: Fri, 29 May 2026 04:22:53 +0100 Subject: [PATCH 1/2] Enhance userPreferences with dashboard layout functions Added visibility toggle to UserPreferences interface and implemented functions to save and retrieve dashboard layout. --- src/lib/userPreferences.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index 99583ec..a59aa28 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -1,5 +1,5 @@ /** - * userPreferences.ts — Issue #142, #188 + * userPreferences.ts — Issue #142, #188, #198 * User preferences schema, defaults, and persistence helpers. * Custom network profiles support for multiple Horizon/RPC presets. */ @@ -20,6 +20,7 @@ export interface WidgetLayout { type: string span: number order: number + visible?: boolean // Added for Issue #198 visibility toggles } export interface NetworkProfile { @@ -92,6 +93,23 @@ export async function updatePreference( return savePreferences({ [key]: value } as Partial) } +// ─── Dashboard Layout Helpers (Issue #198) ──────────────────────────────────── + +/** + * Persists the modified dashboard layout state array. + */ +export async function saveDashboardLayout(layout: WidgetLayout[]): Promise { + return updatePreference('dashboardLayout', layout); +} + +/** + * Retrieves the current dashboard layout array. + */ +export async function getDashboardLayout(): Promise { + const prefs = await loadPreferences(); + return prefs.dashboardLayout || []; +} + // ─── Address book helpers ───────────────────────────────────────────────────── export async function addSavedAddress(entry: Omit): Promise { From 75b5fbb4c09d6b1c4f91159d2caab347be8c4af0 Mon Sep 17 00:00:00 2001 From: Sarah Obasi Date: Fri, 29 May 2026 04:26:51 +0100 Subject: [PATCH 2/2] Refactor dashboard layout management and widget handling --- src/components/dashboard/Overview.jsx | 157 +++++++++++++++++++------- 1 file changed, 114 insertions(+), 43 deletions(-) diff --git a/src/components/dashboard/Overview.jsx b/src/components/dashboard/Overview.jsx index 94bb5af..be96a71 100644 --- a/src/components/dashboard/Overview.jsx +++ b/src/components/dashboard/Overview.jsx @@ -5,9 +5,11 @@ import CopyableValue from './CopyableValue'; import DashboardGrid from '../layout/DashboardGrid'; import WidgetSelector from '../layout/WidgetSelector'; import { useResponsive } from '../../hooks/useResponsive'; -import { usePersistedState } from '../../hooks/usePersistedState'; import { addBreadcrumb } from '../../lib/errorReporting'; +// Import async layout management hooks from userPreferences +import { getDashboardLayout, saveDashboardLayout } from '../../lib/userPreferences'; + // Import widget components import BalanceWidget from '../layout/widgets/BalanceWidget'; import AssetsWidget from '../layout/widgets/AssetsWidget'; @@ -17,12 +19,25 @@ import AccountStatsWidget from '../layout/widgets/AccountStatsWidget'; import QuickActionsWidget from '../layout/widgets/QuickActionsWidget'; import PriceTickerWidget from '../layout/widgets/PriceTickerWidget'; -// Default widget layout +// Get widget component class/function by string identifier +const getWidgetComponent = (type) => { + const components = { + balance: BalanceWidget, + assets: AssetsWidget, + transactions: TransactionsWidget, + networkStats: NetworkStatsWidget, + accountStats: AccountStatsWidget, + quickActions: QuickActionsWidget, + priceTicker: PriceTickerWidget + }; + return components[type] || BalanceWidget; +}; + +// Default widget configuration layout fallbacks const DEFAULT_WIDGETS = [ { id: 'balance-default', type: 'balance', - component: React.createElement(BalanceWidget, { key: 'balance-default' }), width: 300, height: 250, span: 1 @@ -30,7 +45,6 @@ const DEFAULT_WIDGETS = [ { id: 'assets-default', type: 'assets', - component: React.createElement(AssetsWidget, { key: 'assets-default' }), width: 350, height: 300, span: 1 @@ -38,7 +52,6 @@ const DEFAULT_WIDGETS = [ { id: 'transactions-default', type: 'transactions', - component: React.createElement(TransactionsWidget, { key: 'transactions-default' }), width: 400, height: 350, span: 2 @@ -46,7 +59,6 @@ const DEFAULT_WIDGETS = [ { id: 'networkStats-default', type: 'networkStats', - component: React.createElement(NetworkStatsWidget, { key: 'networkStats-default' }), width: 300, height: 280, span: 1 @@ -57,98 +69,157 @@ export default function Overview() { const { connectedAddress, network } = useStore(); const { isMobile, isTablet } = useResponsive(); - // Persisted widget layout - const [widgets, setWidgets] = usePersistedState('dashboard-widgets', DEFAULT_WIDGETS); + const [widgets, setWidgets] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); const [showWidgetSelector, setShowWidgetSelector] = useState(false); - // Refresh widget components when data changes - const refreshWidgets = () => { - const refreshedWidgets = widgets.map(widget => ({ - ...widget, - component: React.createElement(getWidgetComponent(widget.type), { - key: `${widget.type}-${Date.now()}`, - onRefresh: () => refreshWidgets() - }) + // 1. Load layout preferences asynchronously on component mount + useEffect(() => { + async function hydrateDashboardLayout() { + try { + const savedLayout = await getDashboardLayout(); + const activeLayoutRules = (savedLayout && savedLayout.length > 0) ? savedLayout : DEFAULT_WIDGETS; + + // Dynamically append non-serializable React elements using type descriptors + const hydratedWidgets = activeLayoutRules.map(widget => ({ + ...widget, + component: React.createElement(getWidgetComponent(widget.type), { + key: `${widget.id}-${Date.now()}`, + onRefresh: () => refreshWidgets() + }) + })); + + setWidgets(hydratedWidgets); + } catch (error) { + console.error("Failed to restore overview widget layout:", error); + // Fallback to default layout state if an error is thrown + const fallbackWidgets = DEFAULT_WIDGETS.map(widget => ({ + ...widget, + component: React.createElement(getWidgetComponent(widget.type), { + key: `${widget.id}-fallback`, + onRefresh: () => refreshWidgets() + }) + })); + setWidgets(fallbackWidgets); + } finally { + setIsLoading(false); + } + } + hydrateDashboardLayout(); + }, []); + + // Helper utility to clean non-serializable component properties before writing to store + const persistAndSyncLayout = async (updatedWidgets) => { + setWidgets(updatedWidgets); + + const serializedLayout = updatedWidgets.map((w, index) => ({ + id: w.id, + type: w.type, + width: w.width, + height: w.height, + span: w.span, + order: index })); - setWidgets(refreshedWidgets); - addBreadcrumb('Dashboard widgets refreshed', 'user_action'); + + await saveDashboardLayout(serializedLayout); }; - // Get widget component by type - const getWidgetComponent = (type) => { - const components = { - balance: BalanceWidget, - assets: AssetsWidget, - transactions: TransactionsWidget, - networkStats: NetworkStatsWidget, - accountStats: AccountStatsWidget, - quickActions: QuickActionsWidget, - priceTicker: PriceTickerWidget - }; - return components[type] || BalanceWidget; + // Refresh active widget components in-place when layout or data states update + const refreshWidgets = () => { + setWidgets(prevWidgets => + prevWidgets.map(widget => ({ + ...widget, + component: React.createElement(getWidgetComponent(widget.type), { + key: `${widget.id}-${Date.now()}`, + onRefresh: () => refreshWidgets() + }) + })) + ); + addBreadcrumb('Dashboard widgets refreshed', 'user_action'); }; - // Handle layout changes + // Handle arrangement layout sequence shifts const handleLayoutChange = (newLayout) => { - setWidgets(newLayout); + persistAndSyncLayout(newLayout); addBreadcrumb('Dashboard layout changed', 'user_action', { widgetCount: newLayout.length }); }; - // Handle widget resize + // Handle widget resizing dimensions modification const handleWidgetResize = (widget, newSize) => { const updatedWidgets = widgets.map(w => w.id === widget.id ? { ...w, ...newSize } : w ); - setWidgets(updatedWidgets); + persistAndSyncLayout(updatedWidgets); addBreadcrumb('Widget resized', 'user_action', { widgetId: widget.id, newSize }); }; - // Handle widget removal + // Handle structural widget node deletions const handleWidgetRemove = (widget) => { const updatedWidgets = widgets.filter(w => w.id !== widget.id); - setWidgets(updatedWidgets); + persistAndSyncLayout(updatedWidgets); addBreadcrumb('Widget removed', 'user_action', { widgetId: widget.id, widgetType: widget.type }); }; - // Handle adding new widget + // Handle adding a new element container node const handleAddWidget = (newWidget) => { - const updatedWidgets = [...widgets, newWidget]; - setWidgets(updatedWidgets); + const freshWidgetWithElement = { + ...newWidget, + component: React.createElement(getWidgetComponent(newWidget.type), { + key: `${newWidget.id}-${Date.now()}`, + onRefresh: () => refreshWidgets() + }) + }; + const updatedWidgets = [...widgets, freshWidgetWithElement]; + persistAndSyncLayout(updatedWidgets); addBreadcrumb('Widget added', 'user_action', { widgetId: newWidget.id, widgetType: newWidget.type }); }; - // Reset to default layout + // Reset to static fallback architecture layout const handleResetLayout = () => { - setWidgets(DEFAULT_WIDGETS); + const factoryResetWidgets = DEFAULT_WIDGETS.map(widget => ({ + ...widget, + component: React.createElement(getWidgetComponent(widget.type), { + key: `${widget.id}-${Date.now()}`, + onRefresh: () => refreshWidgets() + }) + })); + persistAndSyncLayout(factoryResetWidgets); setIsEditing(false); addBreadcrumb('Dashboard layout reset to default', 'user_action'); }; - // Toggle edit mode + // Toggle layout modification context views const toggleEditMode = () => { setIsEditing(!isEditing); addBreadcrumb(`Dashboard edit mode ${!isEditing ? 'enabled' : 'disabled'}`, 'user_action'); }; - // Responsive column configuration const getColumns = () => { if (isMobile) return { mobile: 1, tablet: 1, desktop: 1 }; if (isTablet) return { mobile: 1, tablet: 2, desktop: 2 }; return { mobile: 1, tablet: 2, desktop: 3 }; }; + if (isLoading) { + return ( +
+ Loading layout choices from user profile... +
+ ); + } + return (
{/* Header */}