11import dayjs from 'dayjs' ;
2- import { useCallback , useMemo , useState , useEffect } from 'react' ;
2+ import { debounce } from 'lodash-es' ;
3+ import { useCallback , useMemo , useState , useEffect , useRef } from 'react' ;
34import { useTranslation } from 'react-i18next' ;
45
56import { DateFormat , TimeFormat } from '@/application/types' ;
67import { MetadataKey } from '@/application/user-metadata' ;
78import { ReactComponent as ChevronDownIcon } from '@/assets/icons/alt_arrow_down.svg' ;
89import { useCurrentUser , useService } from '@/components/main/app.hooks' ;
9- import { Dialog , DialogContent , DialogTitle , DialogTrigger } from '@/components/ui/dialog' ;
10+ import { Dialog , DialogContent , DialogDescription , DialogTitle , DialogTrigger } from '@/components/ui/dialog' ;
1011import {
1112 DropdownMenu ,
1213 DropdownMenuContent ,
@@ -20,6 +21,7 @@ export function AccountSettings({ children }: { children?: React.ReactNode }) {
2021 const { t } = useTranslation ( ) ;
2122 const currentUser = useCurrentUser ( ) ;
2223 const service = useService ( ) ;
24+ const [ open , setIsOpen ] = useState ( false ) ;
2325
2426 const [ dateFormat , setDateFormat ] = useState (
2527 ( ) => Number ( currentUser ?. metadata ?. [ MetadataKey . DateFormat ] as DateFormat ) || DateFormat . Local
@@ -29,54 +31,86 @@ export function AccountSettings({ children }: { children?: React.ReactNode }) {
2931 ) ;
3032 const [ startWeekOn , setStartWeekOn ] = useState ( ( ) => Number ( currentUser ?. metadata ?. [ MetadataKey . StartWeekOn ] ) || 0 ) ;
3133
34+ const metadataUpdateRef = useRef < Record < string , unknown > | null > ( null ) ;
35+
36+ const debounceUpdateProfile = useMemo ( ( ) => {
37+ return debounce ( async ( ) => {
38+ if ( ! service || ! currentUser ?. metadata || ! metadataUpdateRef . current ) return ;
39+
40+ await service ?. updateUserProfile ( metadataUpdateRef . current ) ;
41+ } , 300 ) ;
42+ } , [ service , currentUser ] ) ;
43+
3244 useEffect ( ( ) => {
33- setDateFormat ( Number ( currentUser ?. metadata ?. [ MetadataKey . DateFormat ] as DateFormat ) || DateFormat . Local ) ;
34- setTimeFormat ( Number ( currentUser ?. metadata ?. [ MetadataKey . TimeFormat ] as TimeFormat ) || TimeFormat . TwelveHour ) ;
35- setStartWeekOn ( Number ( currentUser ?. metadata ?. [ MetadataKey . StartWeekOn ] ) || 0 ) ;
36- } , [ currentUser ] ) ;
45+ return ( ) => {
46+ debounceUpdateProfile . cancel ( ) ;
47+ } ;
48+ } , [ debounceUpdateProfile ] ) ;
3749
3850 const handleSelectDateFormat = useCallback (
3951 async ( dateFormat : number ) => {
4052 setDateFormat ( dateFormat ) ;
41- if ( ! service || ! currentUser ?. metadata ) return ;
53+ if ( ! currentUser ?. metadata ) return ;
4254
43- await service ?. updateUserProfile ( { ...currentUser . metadata , [ MetadataKey . DateFormat ] : dateFormat } ) ;
44- await service ?. getCurrentUser ( ) ;
55+ metadataUpdateRef . current = { ...currentUser . metadata , [ MetadataKey . DateFormat ] : dateFormat } ;
56+ await debounceUpdateProfile ( ) ;
4557 } ,
46- [ currentUser , service ]
58+ [ currentUser , debounceUpdateProfile ]
4759 ) ;
4860
4961 const handleSelectTimeFormat = useCallback (
5062 async ( timeFormat : number ) => {
5163 setTimeFormat ( timeFormat ) ;
52- if ( ! service || ! currentUser ?. metadata ) return ;
64+ if ( ! currentUser ?. metadata ) return ;
5365
54- await service ?. updateUserProfile ( { ...currentUser . metadata , [ MetadataKey . TimeFormat ] : timeFormat } ) ;
55- await service ?. getCurrentUser ( ) ;
66+ metadataUpdateRef . current = { ...currentUser . metadata , [ MetadataKey . TimeFormat ] : timeFormat } ;
67+ await debounceUpdateProfile ( ) ;
5668 } ,
57- [ currentUser , service ]
69+ [ currentUser , debounceUpdateProfile ]
5870 ) ;
5971
6072 const handleSelectStartWeekOn = useCallback (
6173 async ( startWeekOn : number ) => {
6274 setStartWeekOn ( startWeekOn ) ;
63- if ( ! service || ! currentUser ?. metadata ) return ;
6475
65- await service ?. updateUserProfile ( { ...currentUser . metadata , [ MetadataKey . StartWeekOn ] : startWeekOn } ) ;
66- await service ?. getCurrentUser ( ) ;
76+ if ( ! currentUser ?. metadata ) return ;
77+
78+ metadataUpdateRef . current = { ...currentUser . metadata , [ MetadataKey . StartWeekOn ] : startWeekOn } ;
79+ await debounceUpdateProfile ( ) ;
80+ } ,
81+ [ currentUser , debounceUpdateProfile ]
82+ ) ;
83+
84+ const onOpenChange = useCallback (
85+ async ( isOpen : boolean ) => {
86+ if ( isOpen ) {
87+ const user = await service ?. getCurrentUser ( ) ;
88+
89+ setDateFormat ( Number ( user ?. metadata ?. [ MetadataKey . DateFormat ] as DateFormat ) || DateFormat . Local ) ;
90+ setTimeFormat ( Number ( user ?. metadata ?. [ MetadataKey . TimeFormat ] as TimeFormat ) || TimeFormat . TwelveHour ) ;
91+ setStartWeekOn ( Number ( user ?. metadata ?. [ MetadataKey . StartWeekOn ] ) || 0 ) ;
92+ }
93+
94+ setIsOpen ( isOpen ) ;
6795 } ,
68- [ currentUser , service ]
96+ [ service ]
6997 ) ;
7098
7199 if ( ! currentUser || ! service ) {
72100 return < > </ > ;
73101 }
74102
75103 return (
76- < Dialog >
104+ < Dialog open = { open } onOpenChange = { onOpenChange } >
77105 < DialogTrigger asChild > { children } </ DialogTrigger >
78- < DialogContent className = 'flex h-[300px] min-h-0 w-[400px] flex-col gap-3 sm:max-w-[calc(100%-2rem)]' >
106+ < DialogContent
107+ data-testid = 'account-settings-dialog'
108+ className = 'flex h-[300px] min-h-0 w-[400px] flex-col gap-3 sm:max-w-[calc(100%-2rem)]'
109+ >
79110 < DialogTitle className = 'text-md font-bold text-text-primary' > { t ( 'web.accountSettings' ) } </ DialogTitle >
111+ < DialogDescription className = 'sr-only' >
112+ Configure your account preferences including date format, time format, and week start day
113+ </ DialogDescription >
80114 < div className = 'flex min-h-0 w-full flex-1 flex-col items-start gap-3 py-4' >
81115 < DateFormatDropdown dateFormat = { dateFormat } onSelect = { handleSelectDateFormat } />
82116 < TimeFormatDropdown timeFormat = { timeFormat } onSelect = { handleSelectTimeFormat } />
@@ -126,6 +160,7 @@ function DateFormatDropdown({ dateFormat, onSelect }: { dateFormat: number; onSe
126160 < div className = 'relative' >
127161 < DropdownMenu open = { isOpen } onOpenChange = { setIsOpen } modal = { false } >
128162 < DropdownMenuTrigger
163+ data-testid = 'date-format-dropdown'
129164 asChild
130165 onPointerDown = { ( e ) => {
131166 e . preventDefault ( ) ;
@@ -148,7 +183,11 @@ function DateFormatDropdown({ dateFormat, onSelect }: { dateFormat: number; onSe
148183 < DropdownMenuContent align = 'start' >
149184 < DropdownMenuRadioGroup value = { dateFormat . toString ( ) } onValueChange = { ( value ) => onSelect ( Number ( value ) ) } >
150185 { dateFormats . map ( ( item ) => (
151- < DropdownMenuRadioItem key = { item . value } value = { item . value . toString ( ) } >
186+ < DropdownMenuRadioItem
187+ data-testid = { `date-format-${ item . value } ` }
188+ key = { item . value }
189+ value = { item . value . toString ( ) }
190+ >
152191 { item . label }
153192 </ DropdownMenuRadioItem >
154193 ) ) }
@@ -187,6 +226,7 @@ function TimeFormatDropdown({ timeFormat, onSelect }: { timeFormat: number; onSe
187226 < div className = 'relative' >
188227 < DropdownMenu open = { isOpen } onOpenChange = { setIsOpen } modal = { false } >
189228 < DropdownMenuTrigger
229+ data-testid = 'time-format-dropdown'
190230 asChild
191231 onPointerDown = { ( e ) => {
192232 e . preventDefault ( ) ;
@@ -209,7 +249,11 @@ function TimeFormatDropdown({ timeFormat, onSelect }: { timeFormat: number; onSe
209249 < DropdownMenuContent align = 'start' >
210250 < DropdownMenuRadioGroup value = { timeFormat . toString ( ) } onValueChange = { ( value ) => onSelect ( Number ( value ) ) } >
211251 { timeFormats . map ( ( item ) => (
212- < DropdownMenuRadioItem key = { item . value } value = { item . value . toString ( ) } >
252+ < DropdownMenuRadioItem
253+ data-testid = { `time-format-${ item . value } ` }
254+ key = { item . value }
255+ value = { item . value . toString ( ) }
256+ >
213257 { item . label }
214258 </ DropdownMenuRadioItem >
215259 ) ) }
@@ -251,6 +295,7 @@ function StartWeekOnDropdown({
251295 < div className = 'relative' >
252296 < DropdownMenu open = { isOpen } onOpenChange = { setIsOpen } modal = { false } >
253297 < DropdownMenuTrigger
298+ data-testid = 'start-week-on-dropdown'
254299 asChild
255300 onPointerDown = { ( e ) => {
256301 e . preventDefault ( ) ;
@@ -273,7 +318,11 @@ function StartWeekOnDropdown({
273318 < DropdownMenuContent align = 'start' >
274319 < DropdownMenuRadioGroup value = { startWeekOn . toString ( ) } onValueChange = { ( value ) => onSelect ( Number ( value ) ) } >
275320 { daysOfWeek . map ( ( item ) => (
276- < DropdownMenuRadioItem key = { item . value } value = { item . value . toString ( ) } >
321+ < DropdownMenuRadioItem
322+ data-testid = { `start-week-${ item . value } ` }
323+ key = { item . value }
324+ value = { item . value . toString ( ) }
325+ >
277326 { item . label }
278327 </ DropdownMenuRadioItem >
279328 ) ) }
0 commit comments