@@ -22,9 +22,9 @@ import {
2222 TableCellsIcon ,
2323 UsersIcon
2424} from "@heroicons/react/20/solid" ;
25- import { Link , useNavigation } from "@remix-run/react" ;
25+ import { Link , useFetcher , useNavigation } from "@remix-run/react" ;
2626import { LayoutGroup , motion } from "framer-motion" ;
27- import { useEffect , useRef , useState , type ReactNode } from "react" ;
27+ import { useCallback , useEffect , useRef , useState , type ReactNode } from "react" ;
2828import simplur from "simplur" ;
2929import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon" ;
3030import { DropdownIcon } from "~/assets/icons/DropdownIcon" ;
@@ -44,6 +44,7 @@ import { useHasAdminAccess } from "~/hooks/useUser";
4444import { useShortcutKeys } from "~/hooks/useShortcutKeys" ;
4545import { ShortcutKey } from "../primitives/ShortcutKey" ;
4646import { type User } from "~/models/user.server" ;
47+ import { type DashboardPreferences } from "~/services/dashboardPreferences.server" ;
4748import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route" ;
4849import { type FeedbackType } from "~/routes/resources.feedback" ;
4950import { IncidentStatusPanel } from "~/routes/resources.incidents" ;
@@ -104,7 +105,10 @@ import { SideMenuHeader } from "./SideMenuHeader";
104105import { SideMenuItem } from "./SideMenuItem" ;
105106import { SideMenuSection } from "./SideMenuSection" ;
106107
107- type SideMenuUser = Pick < User , "email" | "admin" > & { isImpersonating : boolean } ;
108+ type SideMenuUser = Pick < User , "email" | "admin" > & {
109+ isImpersonating : boolean ;
110+ dashboardPreferences : DashboardPreferences ;
111+ } ;
108112export type SideMenuProject = Pick <
109113 MatchedProject ,
110114 "id" | "name" | "slug" | "version" | "environments" | "engine"
@@ -130,17 +134,82 @@ export function SideMenu({
130134} : SideMenuProps ) {
131135 const borderRef = useRef < HTMLDivElement > ( null ) ;
132136 const [ showHeaderDivider , setShowHeaderDivider ] = useState ( false ) ;
133- const [ isCollapsed , setIsCollapsed ] = useState ( false ) ;
137+ const [ isCollapsed , setIsCollapsed ] = useState (
138+ user . dashboardPreferences . sideMenu ?. isCollapsed ?? false
139+ ) ;
140+ const preferencesFetcher = useFetcher ( ) ;
141+ const pendingPreferencesRef = useRef < {
142+ isCollapsed ?: boolean ;
143+ manageSectionCollapsed ?: boolean ;
144+ } > ( { } ) ;
145+ const debounceTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
134146 const currentPlan = useCurrentPlan ( ) ;
135147 const { isConnected } = useDevPresence ( ) ;
136148 const isFreeUser = currentPlan ?. v3Subscription ?. isPaying === false ;
137149 const isAdmin = useHasAdminAccess ( ) ;
138150 const { isManagedCloud } = useFeatures ( ) ;
139151 const featureFlags = useFeatureFlags ( ) ;
140152
153+ const persistSideMenuPreferences = useCallback (
154+ ( data : { isCollapsed ?: boolean ; manageSectionCollapsed ?: boolean } ) => {
155+ if ( user . isImpersonating ) return ;
156+
157+ // Merge with any pending changes
158+ pendingPreferencesRef . current = {
159+ ...pendingPreferencesRef . current ,
160+ ...data ,
161+ } ;
162+
163+ // Clear existing timeout
164+ if ( debounceTimeoutRef . current ) {
165+ clearTimeout ( debounceTimeoutRef . current ) ;
166+ }
167+
168+ // Debounce the actual submission by 500ms
169+ debounceTimeoutRef . current = setTimeout ( ( ) => {
170+ const pending = pendingPreferencesRef . current ;
171+ const formData = new FormData ( ) ;
172+ if ( pending . isCollapsed !== undefined ) {
173+ formData . append ( "isCollapsed" , String ( pending . isCollapsed ) ) ;
174+ }
175+ if ( pending . manageSectionCollapsed !== undefined ) {
176+ formData . append ( "manageSectionCollapsed" , String ( pending . manageSectionCollapsed ) ) ;
177+ }
178+ preferencesFetcher . submit ( formData , {
179+ method : "POST" ,
180+ action : "/resources/preferences/sidemenu" ,
181+ } ) ;
182+ pendingPreferencesRef . current = { } ;
183+ } , 500 ) ;
184+ } ,
185+ [ user . isImpersonating , preferencesFetcher ]
186+ ) ;
187+
188+ // Cleanup timeout on unmount
189+ useEffect ( ( ) => {
190+ return ( ) => {
191+ if ( debounceTimeoutRef . current ) {
192+ clearTimeout ( debounceTimeoutRef . current ) ;
193+ }
194+ } ;
195+ } , [ ] ) ;
196+
197+ const handleToggleCollapsed = ( ) => {
198+ const newIsCollapsed = ! isCollapsed ;
199+ setIsCollapsed ( newIsCollapsed ) ;
200+ persistSideMenuPreferences ( { isCollapsed : newIsCollapsed } ) ;
201+ } ;
202+
203+ const handleManageSectionToggle = useCallback (
204+ ( collapsed : boolean ) => {
205+ persistSideMenuPreferences ( { manageSectionCollapsed : collapsed } ) ;
206+ } ,
207+ [ persistSideMenuPreferences ]
208+ ) ;
209+
141210 useShortcutKeys ( {
142211 shortcut : { modifiers : [ "mod" ] , key : "b" , enabledOnInputElements : true } ,
143- action : ( ) => setIsCollapsed ( ( prev ) => ! prev ) ,
212+ action : handleToggleCollapsed ,
144213 } ) ;
145214
146215 useEffect ( ( ) => {
@@ -164,7 +233,7 @@ export function SideMenu({
164233 isCollapsed ? "w-[2.75rem]" : "w-56"
165234 ) }
166235 >
167- < CollapseToggle isCollapsed = { isCollapsed } onToggle = { ( ) => setIsCollapsed ( ! isCollapsed ) } />
236+ < CollapseToggle isCollapsed = { isCollapsed } onToggle = { handleToggleCollapsed } />
168237 < div className = "absolute inset-0 grid grid-cols-[100%] grid-rows-[2.5rem_1fr_auto] overflow-hidden" >
169238 < div
170239 className = { cn (
@@ -353,6 +422,8 @@ export function SideMenu({
353422 title = "Manage"
354423 isSideMenuCollapsed = { isCollapsed }
355424 itemSpacingClassName = "space-y-0"
425+ initialCollapsed = { user . dashboardPreferences . sideMenu ?. manageSectionCollapsed ?? false }
426+ onCollapseToggle = { handleManageSectionToggle }
356427 >
357428 < SideMenuItem
358429 name = "Bulk actions"
0 commit comments