#698 personal settings

This commit is contained in:
lethemanh
2026-04-22 16:15:34 +07:00
parent 7f922cbea1
commit 07efb8ce51
24 changed files with 1127 additions and 453 deletions
@@ -1,15 +1,21 @@
import { Drawer, IconButton, Box } from '@linagora/twake-mui'
import {
Drawer,
Box,
useTheme,
ListItemIcon,
ListItemText,
ListItemButton
} from '@linagora/twake-mui'
import { CalendarSidebarProps } from './SideBar'
import { SidebarCommonContent } from './SidebarCommonContent'
import { ViewSwitcher } from './ViewSwitcher'
import { MainTitle } from '@/components/Menubar/MainTitle'
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'
import { useI18n } from 'twake-i18n'
import { AppListMenu } from '@/components/Menubar/AppListMenu'
import { UserMenu } from '@/components/Menubar/UserMenu'
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { useAppDispatch } from '@/app/hooks'
import { useUtilMenus } from '../hooks/useUtilMenus'
import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice'
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
import { UtilButtons } from './UtilButtons'
export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
open,
@@ -26,25 +32,20 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
onDateChange
}) => {
const { t } = useI18n()
const user = useAppSelector(state => state.user.userData)
const theme = useTheme()
const dispatch = useAppDispatch()
const {
anchorEl,
supportLink,
userMenuAnchorEl,
handleAppMenuOpen,
handleAppMenuClose,
handleUserMenuOpen,
handleUserMenuClose,
handleSettingsClick,
handleLogoutClick
} = useUtilMenus()
const { handleSettingsClick } = useUtilMenus()
const openSearch = (): void => {
dispatch(setIsMobileSearchOpen(true))
}
const openSettings = (): void => {
handleSettingsClick()
onClose()
}
return (
<Drawer
variant="temporary"
@@ -77,39 +78,7 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
onDateChange={onDateChange}
/>
<Box display="flex" alignItems="center">
{supportLink && (
<IconButton
component="a"
href={supportLink}
target="_blank"
rel="noopener noreferrer"
style={{ marginRight: 4 }}
aria-label={t('menubar.help')}
title={t('menubar.help')}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
)}
<AppListMenu
anchorEl={anchorEl}
onAppMenuOpen={handleAppMenuOpen}
onAppMenuClose={handleAppMenuClose}
iconSize="small"
/>
<UserMenu
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
onSettingsClick={handleSettingsClick}
onLogoutClick={() => void handleLogoutClick()}
onUserMenuOpen={handleUserMenuOpen}
isIframe={isIframe}
user={user}
size="s"
/>
</Box>
<UtilButtons isIframe={isIframe} />
</Box>
)}
@@ -127,6 +96,32 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
setSelectedCalendars={setSelectedCalendars}
openSearchOnMobile={openSearch}
/>
<Box sx={{ marginTop: 1 }}>
<ListItemButton sx={{ padding: 0 }} onClick={openSettings}>
<ListItemIcon sx={{ minWidth: 'unset' }}>
<SettingsOutlinedIcon
sx={{
marginRight: 1,
color: theme.palette.grey.A700,
fontSize: 20
}}
/>
</ListItemIcon>
<ListItemText
primary={t('menubar.settings')}
slotProps={{
primary: {
sx: {
fontSize: '14px',
fontWeight: 500,
lineHeight: '20px'
}
}
}}
/>
</ListItemButton>
</Box>
</Drawer>
)
}
@@ -0,0 +1,60 @@
import { IconButton, Box } from '@linagora/twake-mui'
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'
import { useI18n } from 'twake-i18n'
import { AppListMenu } from '@/components/Menubar/AppListMenu'
import { UserMenu } from '@/components/Menubar/UserMenu'
import { useAppSelector } from '@/app/hooks'
import { useUtilMenus } from '../hooks/useUtilMenus'
export const UtilButtons: React.FC<{ isIframe?: boolean }> = ({ isIframe }) => {
const { t } = useI18n()
const user = useAppSelector(state => state.user.userData)
const {
anchorEl,
supportLink,
userMenuAnchorEl,
handleAppMenuOpen,
handleAppMenuClose,
handleUserMenuOpen,
handleUserMenuClose,
handleSettingsClick,
handleLogoutClick
} = useUtilMenus()
return (
<Box display="flex" alignItems="center">
{supportLink && (
<IconButton
component="a"
href={supportLink}
target="_blank"
rel="noopener noreferrer"
style={{ marginRight: 4 }}
aria-label={t('menubar.help')}
title={t('menubar.help')}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
)}
<AppListMenu
anchorEl={anchorEl}
onAppMenuOpen={handleAppMenuOpen}
onAppMenuClose={handleAppMenuClose}
iconSize="small"
/>
<UserMenu
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
onSettingsClick={handleSettingsClick}
onLogoutClick={() => void handleLogoutClick()}
onUserMenuOpen={handleUserMenuOpen}
isIframe={isIframe}
user={user}
size="s"
/>
</Box>
)
}
+72 -47
View File
@@ -1,4 +1,5 @@
import { Box } from '@linagora/twake-mui'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { Box, SxProps, Theme } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
interface WeekDaySelectorProps {
@@ -17,50 +18,12 @@ export const FC_DAYS = [
{ fc: 0, ics: 'SU' }
]
export function WeekDaySelector({
selectedDays,
onChange,
disabled
}: WeekDaySelectorProps) {
const { t } = useI18n()
const getDayLabel = (ics: string) => {
const dayMap: Record<string, string> = {
MO: t('event.repeat.days.monday'),
TU: t('event.repeat.days.tuesday'),
WE: t('event.repeat.days.wednesday'),
TH: t('event.repeat.days.thursday'),
FR: t('event.repeat.days.friday'),
SA: t('event.repeat.days.saturday'),
SU: t('event.repeat.days.sunday')
}
return dayMap[ics] || ics
}
const handleToggle = (fcDay: number) => {
if (disabled) return
const updated = selectedDays.includes(fcDay)
? selectedDays.filter(d => d !== fcDay)
: [...selectedDays, fcDay]
onChange(updated)
}
return (
<Box display="flex" gap={1}>
{FC_DAYS.map(({ fc, ics }) => {
const isSelected = selectedDays.includes(fc)
const fullLabel = getDayLabel(ics)
return (
<Box
component="button"
type="button"
key={ics}
aria-label={fullLabel}
aria-pressed={isSelected}
onClick={() => handleToggle(fc)}
disabled={disabled}
sx={{
const WeekDayStyle = (
isSelected: boolean,
isMobile: boolean,
disabled?: boolean
): SxProps<Theme> => {
const desktopStyle = {
width: 40,
height: 40,
borderRadius: '4px',
@@ -74,6 +37,7 @@ export function WeekDaySelector({
justifyContent: 'center',
bgcolor: isSelected ? 'primary.main' : 'transparent',
cursor: disabled ? 'default' : 'pointer',
'@media (hover: hover)': {
'&:hover': !disabled
? {
borderColor: 'primary.main',
@@ -81,9 +45,70 @@ export function WeekDaySelector({
color: '#fff'
}
: undefined
}}
}
} as SxProps<Theme>
if (isMobile) {
return {
...desktopStyle,
width: 200,
justifyContent: 'start',
padding: 1
}
}
return desktopStyle
}
export const WeekDaySelector: React.FC<WeekDaySelectorProps> = ({
selectedDays,
onChange,
disabled
}) => {
const { t } = useI18n()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const getDayLabel = (ics: string, fullLabel: boolean): string => {
const weekdayKey = fullLabel ? 'fullDays' : 'days'
const dayMap: Record<string, string> = {
MO: t(`event.repeat.${weekdayKey}.monday`),
TU: t(`event.repeat.${weekdayKey}.tuesday`),
WE: t(`event.repeat.${weekdayKey}.wednesday`),
TH: t(`event.repeat.${weekdayKey}.thursday`),
FR: t(`event.repeat.${weekdayKey}.friday`),
SA: t(`event.repeat.${weekdayKey}.saturday`),
SU: t(`event.repeat.${weekdayKey}.sunday`)
}
return dayMap[ics] || ics
}
const handleToggle = (fcDay: number): void => {
if (disabled) return
const updated = selectedDays.includes(fcDay)
? selectedDays.filter(d => d !== fcDay)
: [...selectedDays, fcDay]
onChange(updated)
}
return (
<Box display="flex" flexDirection={isMobile ? 'column' : 'row'} gap={1}>
{FC_DAYS.map(({ fc, ics }) => {
const isSelected = selectedDays.includes(fc)
const fullLabel = getDayLabel(ics, isMobile)
return (
<Box
component="button"
type="button"
key={ics}
aria-label={fullLabel}
aria-pressed={isSelected}
onClick={() => handleToggle(fc)}
disabled={disabled}
sx={WeekDayStyle(isSelected, isMobile, disabled)}
>
{fullLabel.charAt(0)}
{isMobile ? fullLabel : fullLabel.charAt(0)}
</Box>
)
})}
+35 -1
View File
@@ -10,6 +10,9 @@ import './Menubar.styl'
import { DatePickerMobile } from './components/DatePickerMobile'
import { Typography } from '@mui/material'
import { SmallNavigationControls } from './components/SmallNavigationControls'
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { setView } from '@/features/Settings/SettingsSlice'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
export type MobileMenubarProps = {
calendarRef: React.RefObject<CalendarApi | null>
@@ -27,6 +30,9 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
onOpenSidebar
}) => {
const { t } = useI18n()
const dispatch = useAppDispatch()
const view = useAppSelector(state => state.settings.view)
const [openDatePicker, setOpenDatePicker] = useState(false)
@@ -46,9 +52,31 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
})
}
const handleBackClick = (event: React.MouseEvent): void => {
event.stopPropagation()
event.preventDefault()
dispatch(setView('calendar'))
}
return (
<>
<header className="menubar menubar--mobile">
{view === 'settings' ? (
<>
<IconButton
onClick={handleBackClick}
aria-label={t('settings.back')}
className="back-button"
>
<ArrowBackIcon />
</IconButton>
<Typography variant="h3" sx={{ width: '100%' }}>
{t('menubar.settings')}
</Typography>
</>
) : (
<>
<div className="left-menu">
<IconButton
onClick={onOpenSidebar}
@@ -83,7 +111,11 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
}
aria-expanded={openDatePicker}
>
{openDatePicker ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />}
{openDatePicker ? (
<ArrowDropUpIcon />
) : (
<ArrowDropDownIcon />
)}
</IconButton>
</Stack>
</div>
@@ -93,6 +125,8 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
<SearchBar />
</div>
</div>
</>
)}
</header>
{openDatePicker ? (
+107
View File
@@ -0,0 +1,107 @@
import {
Box,
Typography,
styled,
ButtonBase,
SwipeableDrawer
} from '@linagora/twake-mui'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import React, { forwardRef, useImperativeHandle, useState } from 'react'
export interface MobileSelectorHandle {
open: boolean
onClose: () => void
}
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
zIndex: theme.zIndex.modal + 100
}))
const SelectorButton = styled(ButtonBase)(({ theme }) => ({
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(1.5, 2),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
textAlign: 'left',
cursor: 'pointer',
transition: 'border-color 0.2s',
'&:hover': {
borderColor: theme.palette.text.primary
}
}))
interface MobileSelectorProps {
displayText: string
children?: React.ReactNode
bottomSheetRef?: React.RefObject<HTMLDivElement>
paperRef?: React.RefObject<HTMLDivElement>
bottomSheetChildren?: (props: {
open: boolean
onClose: () => void
}) => React.ReactNode
}
export const MobileSelector = forwardRef<
MobileSelectorHandle,
MobileSelectorProps
>(
(
{ displayText, children, bottomSheetRef, paperRef, bottomSheetChildren },
ref
) => {
const [open, setOpen] = useState(false)
const onClose = (): void => {
setOpen(false)
}
useImperativeHandle(ref, () => ({
open,
onClose
}))
return (
<>
<SelectorButton onClick={() => setOpen(true)}>
<Typography variant="body1">{displayText}</Typography>
<Box
component={ArrowDropDownIcon}
sx={{
fontSize: 20,
transition: 'transform 0.2s',
transform: open ? 'rotate(180deg)' : 'rotate(0deg)'
}}
/>
</SelectorButton>
{children ? (
<StyledSwipeableDrawer
ref={bottomSheetRef}
anchor="bottom"
open={open}
onClose={onClose}
onOpen={(): void => {}}
disableAutoFocus
slotProps={{
paper: {
ref: paperRef,
sx: { maxHeight: '90dvh' }
}
}}
>
{children}
</StyledSwipeableDrawer>
) : (
bottomSheetChildren?.({ open, onClose })
)}
</>
)
}
)
MobileSelector.displayName = 'MobileSelector'
@@ -0,0 +1,108 @@
import {
Box,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Typography
} from '@linagora/twake-mui'
import SettingsIcon from '@mui/icons-material/Settings'
import { useState } from 'react'
import { useI18n } from 'twake-i18n'
import { GeneralSettings } from './GeneralSettings'
import { NotificationsSettings } from './NotificationSettings'
import './SettingsPage.styl'
import { SettingsHeader } from './SettingsHeader'
import type { SidebarNavItem, SettingsSubTab } from './SettingsPage'
export const DesktopSettingsPage: React.FC<{
activeSettingsSubTab: SettingsSubTab
setLanguageErrorOpen: (open: boolean) => void
setTimeZoneErrorOpen: (open: boolean) => void
setAlarmEmailsErrorOpen: (open: boolean) => void
setHideDeclinedEventsErrorOpen: (open: boolean) => void
setDisplayWeekNumbersErrorOpen: (open: boolean) => void
setWorkingDaysErrorOpen: (open: boolean) => void
setActiveSettingsSubTab: (subTab: SettingsSubTab) => void
handleSettingsSubTabChange: (
_event: React.SyntheticEvent,
newValue: SettingsSubTab
) => void
}> = ({
activeSettingsSubTab,
setLanguageErrorOpen,
setTimeZoneErrorOpen,
setAlarmEmailsErrorOpen,
setHideDeclinedEventsErrorOpen,
setDisplayWeekNumbersErrorOpen,
setWorkingDaysErrorOpen,
handleSettingsSubTabChange,
setActiveSettingsSubTab
}) => {
const { t } = useI18n()
const [activeNavItem, setActiveNavItem] = useState<SidebarNavItem>('settings')
const handleNavItemClick = (item: SidebarNavItem): void => {
setActiveNavItem(item)
if (item === 'settings') {
setActiveSettingsSubTab('settings')
}
}
return (
<>
<Box className="settings-sidebar">
<List>
<ListItemButton
className={`settings-nav-item ${activeNavItem === 'settings' ? 'active' : ''}`}
onClick={() => handleNavItemClick('settings')}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary={t('settings.title') || 'Settings'} />
</ListItemButton>
</List>
</Box>
<Box className="settings-content">
<SettingsHeader
activeNavItem={activeNavItem}
activeSettingsSubTab={activeSettingsSubTab}
handleSettingsSubTabChange={handleSettingsSubTabChange}
/>
<Box className="settings-content-body">
{activeNavItem === 'settings' && (
<>
{activeSettingsSubTab === 'settings' && (
<GeneralSettings
onLanguageError={() => setLanguageErrorOpen(true)}
onTimeZoneError={() => setTimeZoneErrorOpen(true)}
onHideDeclinedEventsError={() =>
setHideDeclinedEventsErrorOpen(true)
}
onDisplayWeekNumbersError={() =>
setDisplayWeekNumbersErrorOpen(true)
}
onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
/>
)}
{activeSettingsSubTab === 'notifications' && (
<NotificationsSettings
onAlarmEmailsError={() => setAlarmEmailsErrorOpen(true)}
/>
)}
</>
)}
</Box>
{activeNavItem === 'sync' && (
<Box className="settings-tab-content">
<Typography variant="body1" color="text.secondary">
{t('settings.sync.empty') || 'Sync settings coming soon'}
</Typography>
</Box>
)}
</Box>
</>
)
}
+14 -159
View File
@@ -1,35 +1,25 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { useTimeZoneList } from '@/components/Timezone/hooks/useTimeZoneList'
import { WeekDaySelector } from '@/components/Event/WeekDaySelector'
import { TimezoneAutocomplete } from '@/components/Timezone/TimezoneAutocomplete'
import { browserDefaultTimeZone, getTimezoneOffset } from '@/utils/timezone'
import {
Box,
FormControl,
FormControlLabel,
MenuItem,
Select,
Switch,
Typography
} from '@linagora/twake-mui'
import { useRef, useCallback, useEffect } from 'react'
import { useI18n } from 'twake-i18n'
import {
setLanguage as setUserLanguage,
setTimezone as setUserTimeZone,
updateUserConfigurationsAsync
} from '../User/userSlice'
import { AVAILABLE_LANGUAGES } from './constants'
import { updateUserConfigurationsAsync } from '../User/userSlice'
import {
BusinessHour,
setBusinessHours,
setDisplayWeekNumbers,
setHideDeclinedEvents,
setIsBrowserDefaultTimeZone,
setLanguage as setSettingsLanguage,
setTimeZone as setSettingsTimeZone,
setWorkingDays
} from './SettingsSlice'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { LanguageSelector } from './LanguageSelector'
import { TimezoneSelector } from './TimezoneSelector'
interface GeneralSettingsProps {
onLanguageError: () => void
@@ -48,22 +38,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
}) => {
const dispatch = useAppDispatch()
const { t } = useI18n()
const previousConfig = useAppSelector(state => state.user.coreConfig)
const userLanguage = useAppSelector(state => state.user?.coreConfig.language)
const settingsLanguage = useAppSelector(state => state.settings?.language)
const currentLanguage = userLanguage || settingsLanguage || 'en'
const timezoneList = useTimeZoneList()
const userTimeZone = useAppSelector(
state => state.user?.coreConfig?.datetime?.timeZone
)
const settingTimeZone = useAppSelector(state => state.settings?.timeZone)
const currentTimeZone =
userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone
const isBrowserDefault = useAppSelector(
state => state.settings.isBrowserDefaultTimeZone
)
const { isTooSmall: isMobile } = useScreenSizeDetection()
const hideDeclinedEvents = useAppSelector(
state => state.settings?.hideDeclinedEvents
@@ -78,67 +53,6 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
null
)
const handleLanguageChange = (
event:
| React.ChangeEvent<
Omit<HTMLInputElement, 'value'> & {
value: string
}
>
| (Event & {
target: {
value: string
name: string
}
})
): void => {
const newLanguage = event.target.value
const previousLanguage = currentLanguage
dispatch(setUserLanguage(newLanguage))
dispatch(setSettingsLanguage(newLanguage))
dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
.unwrap()
.catch(() => {
dispatch(setUserLanguage(previousLanguage))
dispatch(setSettingsLanguage(previousLanguage))
onLanguageError()
})
}
const handleTimeZoneChange = (newTimeZone: string): void => {
const previousTimeZone = currentTimeZone
dispatch(setUserTimeZone(newTimeZone))
dispatch(setSettingsTimeZone(newTimeZone))
dispatch(
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
)
.unwrap()
.catch(() => {
dispatch(setUserTimeZone(previousTimeZone))
dispatch(setSettingsTimeZone(previousTimeZone))
onTimeZoneError()
})
}
const handleTimeZoneDefaultChange = (isDefault: boolean): void => {
const previousTimeZone = currentTimeZone
dispatch(setIsBrowserDefaultTimeZone(isDefault))
if (isDefault) {
dispatch(setUserTimeZone(null))
dispatch(setSettingsTimeZone(browserDefaultTimeZone))
dispatch(
updateUserConfigurationsAsync({ timezone: null, previousConfig })
)
.unwrap()
.catch(() => {
dispatch(setUserTimeZone(previousTimeZone))
dispatch(setSettingsTimeZone(previousTimeZone))
dispatch(setIsBrowserDefaultTimeZone(!isDefault))
onTimeZoneError()
})
}
}
const handleHideDeclinedEvents = (value: boolean): void => {
dispatch(setHideDeclinedEvents(value))
dispatch(updateUserConfigurationsAsync({ hideDeclinedEvents: value }))
@@ -207,74 +121,15 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
})
}
const inputMinWidth = isMobile ? '100%' : 500
return (
<Box className="settings-tab-content">
{/* Language */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{t('settings.language') || 'Language'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('settings.languageDescription') ||
'This will be the language used in your Twake Calendar'}
</Typography>
<FormControl size="small" sx={{ minWidth: 500 }}>
<Select
value={currentLanguage}
onChange={handleLanguageChange}
variant="outlined"
aria-label={t('settings.languageSelector') || 'Language selector'}
>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<LanguageSelector onLanguageError={onLanguageError} />
{/* Timezone */}
<Box
sx={{
mb: 4
}}
>
<Typography variant="h6" sx={{ mb: 1 }}>
{t('settings.timeZone')}
</Typography>
<Box>
<FormControl size="small" sx={{ minWidth: 500 }}>
<FormControlLabel
control={
<Switch
checked={isBrowserDefault}
onChange={() =>
handleTimeZoneDefaultChange(!isBrowserDefault)
}
aria-label={t('settings.timeZoneBrowserDefault')}
/>
}
label={t('settings.timeZoneBrowserDefault')}
labelPlacement="start"
sx={{
minWidth: 400,
justifyContent: 'space-between',
marginLeft: 0,
mb: 2
}}
/>
{!isBrowserDefault && (
<TimezoneAutocomplete
value={currentTimeZone}
zones={timezoneList.zones}
getTimezoneOffset={getTimezoneOffset}
onChange={handleTimeZoneChange}
/>
)}
</FormControl>
</Box>
</Box>
<TimezoneSelector onTimeZoneError={onTimeZoneError} />
{/* Working */}
<Box sx={{ mb: 4 }}>
@@ -285,7 +140,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
selectedDays={businessHours?.daysOfWeek ?? []}
onChange={days => handleBusinessHour({ days })}
/>
<FormControl size="small" sx={{ minWidth: 500, mt: 2 }}>
<FormControl size="small" sx={{ minWidth: inputMinWidth, mt: 2 }}>
<FormControlLabel
control={
<Switch
@@ -296,7 +151,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
label={t('settings.showOnlyWorkingDays')}
labelPlacement="start"
sx={{
minWidth: 400,
minWidth: isMobile ? '100%' : 400,
justifyContent: 'space-between',
marginLeft: 0
}}
@@ -309,7 +164,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
<Typography variant="h6" sx={{ mb: 1 }}>
{t('settings.calAndEvent')}
</Typography>
<FormControl size="small" sx={{ minWidth: 500 }}>
<FormControl size="small" sx={{ minWidth: inputMinWidth }}>
<FormControlLabel
control={
<Switch
@@ -321,7 +176,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
label={t('settings.showDeclinedEvent')}
labelPlacement="start"
sx={{
minWidth: 400,
minWidth: isMobile ? '100%' : 400,
justifyContent: 'space-between',
marginLeft: 0
}}
@@ -337,7 +192,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
label={t('settings.displayWeekNumbers')}
labelPlacement="start"
sx={{
minWidth: 400,
minWidth: isMobile ? '100%' : 400,
justifyContent: 'space-between',
marginLeft: 0
}}
@@ -0,0 +1,28 @@
import { FormControl, MenuItem, Select } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import { AVAILABLE_LANGUAGES } from '../constants'
import { LanguageSelectorInputProps } from './index.types'
export const DesktopSelectLanguage: React.FC<LanguageSelectorInputProps> = ({
currentLanguage,
onChange
}) => {
const { t } = useI18n()
return (
<FormControl size="small" sx={{ minWidth: 500 }}>
<Select
value={currentLanguage}
onChange={onChange}
variant="outlined"
aria-label={t('settings.languageSelector')}
>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
)
}
@@ -0,0 +1,66 @@
import React, { useRef } from 'react'
import { AVAILABLE_LANGUAGES } from '../constants'
import { EventChange, LanguageSelectorInputProps } from './index.types'
import {
MobileSelector,
MobileSelectorHandle
} from '@/components/MobileSelector'
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
Typography,
useTheme
} from '@linagora/twake-mui'
import { Check as CheckIcon } from '@mui/icons-material'
export const MobileLanguageSelector: React.FC<LanguageSelectorInputProps> = ({
currentLanguage,
onChange
}) => {
const theme = useTheme()
const selectorRef = useRef<MobileSelectorHandle>(null)
const currentLabel =
AVAILABLE_LANGUAGES.find(({ code }) => code === currentLanguage)?.label ??
currentLanguage
const handleChange = (event: EventChange): void => {
onChange(event)
selectorRef.current?.onClose()
}
return (
<MobileSelector ref={selectorRef} displayText={currentLabel}>
<List sx={{ overflow: 'auto', flex: 1, pt: 0 }}>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<ListItem key={code} value={code} disablePadding>
<ListItemButton
selected={currentLanguage === code}
aria-selected={currentLanguage === code}
onClick={() =>
handleChange({ target: { value: code } } as EventChange)
}
sx={{ py: 1 }}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography>{label}</Typography>
</Box>
}
/>
{currentLanguage === code && (
<CheckIcon
sx={{ color: theme.palette.primary.main, fontSize: '20px' }}
/>
)}
</ListItemButton>
</ListItem>
))}
</List>
</MobileSelector>
)
}
@@ -0,0 +1,39 @@
import { Box, Typography } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { DesktopSelectLanguage } from './DesktopLanguageSelector'
import { MobileLanguageSelector } from './MobileLanguageSelector'
import { useLanguageChange } from '../hooks/useLanguageChange'
import type { LanguageSelectorProps } from './index.types'
export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
onLanguageError
}) => {
const { t } = useI18n()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const { currentLanguage, handleLanguageChange } = useLanguageChange({
onLanguageError
})
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{t('settings.language')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('settings.languageDescription')}
</Typography>
{isMobile ? (
<MobileLanguageSelector
currentLanguage={currentLanguage}
onChange={handleLanguageChange}
/>
) : (
<DesktopSelectLanguage
currentLanguage={currentLanguage}
onChange={handleLanguageChange}
/>
)}
</Box>
)
}
@@ -0,0 +1,26 @@
export type EventChange =
| React.ChangeEvent<
Omit<HTMLInputElement, 'value'> & {
value: string
}
>
| (Event & {
target: {
value: string
name: string
}
})
export interface LanguageSelectorProps {
onLanguageError: () => void
}
export interface LanguageSelectorInputProps {
currentLanguage: string
onChange: (event: EventChange) => void
}
export interface LanguageListBottomSheetProps extends LanguageSelectorInputProps {
open: boolean
onClose: () => void
}
@@ -0,0 +1,60 @@
import { Box } from '@linagora/twake-mui'
import { GeneralSettings } from './GeneralSettings'
import { NotificationsSettings } from './NotificationSettings'
import './SettingsPage.styl'
import { SettingsHeader } from './SettingsHeader'
import { SettingsSubTab } from './SettingsPage'
export const MobileSettingsPage: React.FC<{
activeSettingsSubTab: SettingsSubTab
setLanguageErrorOpen: (open: boolean) => void
setTimeZoneErrorOpen: (open: boolean) => void
setAlarmEmailsErrorOpen: (open: boolean) => void
setHideDeclinedEventsErrorOpen: (open: boolean) => void
setDisplayWeekNumbersErrorOpen: (open: boolean) => void
setWorkingDaysErrorOpen: (open: boolean) => void
setActiveSettingsSubTab: (subTab: SettingsSubTab) => void
handleSettingsSubTabChange: (
_event: React.SyntheticEvent,
newValue: SettingsSubTab
) => void
}> = ({
activeSettingsSubTab,
setLanguageErrorOpen,
setTimeZoneErrorOpen,
setAlarmEmailsErrorOpen,
setHideDeclinedEventsErrorOpen,
setDisplayWeekNumbersErrorOpen,
setWorkingDaysErrorOpen,
handleSettingsSubTabChange
}) => {
return (
<Box className="settings-content" sx={{ borderRadius: 0 }}>
<SettingsHeader
activeNavItem="settings"
activeSettingsSubTab={activeSettingsSubTab}
handleSettingsSubTabChange={handleSettingsSubTabChange}
/>
<Box className="settings-content-body">
{activeSettingsSubTab === 'settings' && (
<GeneralSettings
onLanguageError={() => setLanguageErrorOpen(true)}
onTimeZoneError={() => setTimeZoneErrorOpen(true)}
onHideDeclinedEventsError={() =>
setHideDeclinedEventsErrorOpen(true)
}
onDisplayWeekNumbersError={() =>
setDisplayWeekNumbersErrorOpen(true)
}
onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
/>
)}
{activeSettingsSubTab === 'notifications' && (
<NotificationsSettings
onAlarmEmailsError={() => setAlarmEmailsErrorOpen(true)}
/>
)}
</Box>
</Box>
)
}
+12 -4
View File
@@ -6,23 +6,27 @@ import {
setAlarmEmails,
updateUserConfigurationsAsync
} from '../User/userSlice'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
interface NotificationsSettingsProps {
onAlarmEmailsError: () => void
}
export function NotificationsSettings({
export const NotificationsSettings: React.FC<NotificationsSettingsProps> = ({
onAlarmEmailsError
}: NotificationsSettingsProps) {
}) => {
const dispatch = useAppDispatch()
const { t } = useI18n()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const alarmEmailsEnabled = useAppSelector(
state => state.user?.alarmEmailsEnabled ?? true
)
const handleAlarmEmailsToggle = (
event: React.ChangeEvent<HTMLInputElement>
) => {
): void => {
const newValue = event.target.checked
const previousValue = alarmEmailsEnabled
dispatch(setAlarmEmails(newValue))
@@ -48,7 +52,11 @@ export function NotificationsSettings({
}
label={t('settings.notifications.email')}
labelPlacement="start"
sx={{ minWidth: 400, justifyContent: 'space-between', marginLeft: 0 }}
sx={{
minWidth: isMobile ? '100%' : 400,
justifyContent: 'space-between',
marginLeft: 0
}}
/>
</Box>
)
@@ -0,0 +1,79 @@
import { Snackbar } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import './SettingsPage.styl'
export const SettingErrorSnackbars: React.FC<{
languageErrorOpen: boolean
timeZoneErrorOpen: boolean
alarmEmailsErrorOpen: boolean
hideDeclinedEventsErrorOpen: boolean
displayWeekNumbersErrorOpen: boolean
workingDaysErrorOpen: boolean
handleLanguageErrorClose: () => void
handleTimeZoneErrorClose: () => void
handleAlarmEmailsErrorClose: () => void
handleHideDeclinedEventsErrorClose: () => void
handleDisplayWeekNumbersErrorClose: () => void
setWorkingDaysErrorOpen: (open: boolean) => void
}> = ({
languageErrorOpen,
timeZoneErrorOpen,
alarmEmailsErrorOpen,
hideDeclinedEventsErrorOpen,
displayWeekNumbersErrorOpen,
workingDaysErrorOpen,
handleLanguageErrorClose,
handleTimeZoneErrorClose,
handleAlarmEmailsErrorClose,
handleHideDeclinedEventsErrorClose,
handleDisplayWeekNumbersErrorClose,
setWorkingDaysErrorOpen
}) => {
const { t } = useI18n()
return (
<>
<Snackbar
open={languageErrorOpen}
autoHideDuration={4000}
onClose={handleLanguageErrorClose}
message={
t('settings.languageUpdateError') || 'Failed to update language'
}
/>
<Snackbar
open={timeZoneErrorOpen}
autoHideDuration={4000}
onClose={handleTimeZoneErrorClose}
message={t('settings.timeZoneUpdateError')}
/>
<Snackbar
open={alarmEmailsErrorOpen}
autoHideDuration={4000}
onClose={handleAlarmEmailsErrorClose}
message={
t('settings.alarmEmailsUpdateError') ||
'Failed to update email notifications setting'
}
/>
<Snackbar
open={hideDeclinedEventsErrorOpen}
autoHideDuration={4000}
onClose={handleHideDeclinedEventsErrorClose}
message={t('settings.hideDeclinedEventsUpdateError')}
/>
<Snackbar
open={displayWeekNumbersErrorOpen}
autoHideDuration={4000}
onClose={handleDisplayWeekNumbersErrorClose}
message={t('settings.displayWeekNumbersUpdateError')}
/>
<Snackbar
open={workingDaysErrorOpen}
autoHideDuration={4000}
onClose={() => setWorkingDaysErrorOpen(false)}
message={t('settings.workingDaysUpdateError')}
/>
</>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { useAppDispatch } from '@/app/hooks'
import { Box, IconButton, Tab, Tabs } from '@linagora/twake-mui'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import { useI18n } from 'twake-i18n'
import './SettingsPage.styl'
import { setView } from './SettingsSlice'
import { SettingsSubTab, SidebarNavItem } from './SettingsPage'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
export const SettingsHeader: React.FC<{
activeNavItem?: SidebarNavItem
activeSettingsSubTab: SettingsSubTab
handleSettingsSubTabChange: (
_event: React.SyntheticEvent,
newValue: SettingsSubTab
) => void
}> = ({ activeNavItem, activeSettingsSubTab, handleSettingsSubTabChange }) => {
const dispatch = useAppDispatch()
const { t } = useI18n()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const handleBackClick = (): void => {
dispatch(setView('calendar'))
}
return (
<Box className="settings-content-header">
{!isMobile && (
<IconButton
onClick={handleBackClick}
aria-label={t('settings.back') || 'Back to calendar'}
className="back-button"
>
<ArrowBackIcon sx={{ color: '#605D62', fontSize: 30 }} />
</IconButton>
)}
{activeNavItem === 'settings' && (
<Tabs
value={activeSettingsSubTab}
onChange={handleSettingsSubTabChange}
className={`settings-content-tabs${isMobile ? '--mobile' : ''}`}
>
<Tab value="settings" label={t('settings.title')} />
<Tab value="notifications" label={t('settings.notifications')} />
</Tabs>
)}
</Box>
)
}
+7
View File
@@ -4,6 +4,9 @@
height calc(100vh - 70px)
padding 0 10px 10px 0;
&--mobile
padding 0
.main-layout.settings-layout.isInIframe
height 100vh
@@ -106,3 +109,7 @@
.MuiTabs-indicator
background-color #F67E35
&--mobile
.MuiTab-root
font-size 17px
+52 -159
View File
@@ -1,33 +1,16 @@
import { useAppDispatch } from '@/app/hooks'
import {
Box,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Snackbar,
Tab,
Tabs,
Typography
} from '@linagora/twake-mui'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import SettingsIcon from '@mui/icons-material/Settings'
import { useState } from 'react'
import { useI18n } from 'twake-i18n'
import { GeneralSettings } from './GeneralSettings'
import { NotificationsSettings } from './NotificationSettings'
import './SettingsPage.styl'
import { setView } from './SettingsSlice'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { SettingErrorSnackbars } from './SettingErrorSnackbars'
import { DesktopSettingsPage } from './DesktopSettingsPage'
import { MobileSettingsPage } from './MobileSettingsPage'
type SidebarNavItem = 'settings' | 'sync'
type SettingsSubTab = 'settings' | 'notifications'
export type SidebarNavItem = 'settings' | 'sync'
export type SettingsSubTab = 'settings' | 'notifications'
export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
const dispatch = useAppDispatch()
const { t } = useI18n()
const SettingsPage: React.FC<{ isInIframe?: boolean }> = ({ isInIframe }) => {
const { isTooSmall: isMobile } = useScreenSizeDetection()
const [activeNavItem, setActiveNavItem] = useState<SidebarNavItem>('settings')
const [activeSettingsSubTab, setActiveSettingsSubTab] =
useState<SettingsSubTab>('settings')
@@ -40,163 +23,73 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
useState(false)
const [workingDaysErrorOpen, setWorkingDaysErrorOpen] = useState(false)
const handleBackClick = () => {
dispatch(setView('calendar'))
}
const handleNavItemClick = (item: SidebarNavItem) => {
setActiveNavItem(item)
if (item === 'settings') {
setActiveSettingsSubTab('settings')
}
}
const handleSettingsSubTabChange = (
_event: React.SyntheticEvent,
newValue: SettingsSubTab
) => {
): void => {
setActiveSettingsSubTab(newValue)
}
const handleLanguageErrorClose = () => {
const handleLanguageErrorClose = (): void => {
setLanguageErrorOpen(false)
}
const handleTimeZoneErrorClose = () => {
const handleTimeZoneErrorClose = (): void => {
setTimeZoneErrorOpen(false)
}
const handleHideDeclinedEventsErrorClose = () => {
const handleHideDeclinedEventsErrorClose = (): void => {
setHideDeclinedEventsErrorOpen(false)
}
const handleAlarmEmailsErrorClose = () => {
const handleAlarmEmailsErrorClose = (): void => {
setAlarmEmailsErrorOpen(false)
}
const handleDisplayWeekNumbersErrorClose = () => {
const handleDisplayWeekNumbersErrorClose = (): void => {
setDisplayWeekNumbersErrorOpen(false)
}
return (
<main
className={`main-layout settings-layout${isInIframe ? ' isInIframe' : ''}`}
className={`main-layout settings-layout${isInIframe ? ' isInIframe' : ''} ${isMobile ? 'settings-layout--mobile' : ''}`}
>
<Box className="settings-sidebar">
<List>
<ListItem
className={`settings-nav-item ${activeNavItem === 'settings' ? 'active' : ''}`}
onClick={() => handleNavItemClick('settings')}
sx={{ cursor: 'pointer' }}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary={t('settings.title') || 'Settings'} />
</ListItem>
{/* <ListItem
className={`settings-nav-item ${activeNavItem === "sync" ? "active" : ""}`}
onClick={() => handleNavItemClick("sync")}
sx={{ cursor: "pointer" }}
>
<ListItemIcon>
<SyncIcon />
</ListItemIcon>
<ListItemText primary={t("settings.sync") || "Sync"} />
</ListItem> */}
</List>
</Box>
<Box className="settings-content">
<Box className="settings-content-header">
<IconButton
onClick={handleBackClick}
aria-label={t('settings.back') || 'Back to calendar'}
className="back-button"
>
<ArrowBackIcon sx={{ color: '#605D62', fontSize: 30 }} />
</IconButton>
{activeNavItem === 'settings' && (
<Tabs
value={activeSettingsSubTab}
onChange={handleSettingsSubTabChange}
className="settings-content-tabs"
>
<Tab value="settings" label={t('settings.title') || 'Settings'} />
<Tab
value="notifications"
label={t('settings.notifications') || 'Notifications'}
{isMobile ? (
<MobileSettingsPage
activeSettingsSubTab={activeSettingsSubTab}
setLanguageErrorOpen={setLanguageErrorOpen}
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
handleSettingsSubTabChange={handleSettingsSubTabChange}
setActiveSettingsSubTab={setActiveSettingsSubTab}
/>
</Tabs>
)}
</Box>
<Box className="settings-content-body">
{activeNavItem === 'settings' && (
<>
{activeSettingsSubTab === 'settings' && (
<GeneralSettings
onLanguageError={() => setLanguageErrorOpen(true)}
onTimeZoneError={() => setTimeZoneErrorOpen(true)}
onHideDeclinedEventsError={() =>
setHideDeclinedEventsErrorOpen(true)
}
onDisplayWeekNumbersError={() =>
setDisplayWeekNumbersErrorOpen(true)
}
onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
) : (
<DesktopSettingsPage
activeSettingsSubTab={activeSettingsSubTab}
setLanguageErrorOpen={setLanguageErrorOpen}
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
handleSettingsSubTabChange={handleSettingsSubTabChange}
setActiveSettingsSubTab={setActiveSettingsSubTab}
/>
)}
{activeSettingsSubTab === 'notifications' && (
<NotificationsSettings
onAlarmEmailsError={() => setAlarmEmailsErrorOpen(true)}
/>
)}
</>
)}
</Box>
{activeNavItem === 'sync' && (
<Box className="settings-tab-content">
<Typography variant="body1" color="text.secondary">
{t('settings.sync.empty') || 'Sync settings coming soon'}
</Typography>
</Box>
)}
</Box>
<Snackbar
open={languageErrorOpen}
autoHideDuration={4000}
onClose={handleLanguageErrorClose}
message={
t('settings.languageUpdateError') || 'Failed to update language'
}
/>
<Snackbar
open={timeZoneErrorOpen}
autoHideDuration={4000}
onClose={handleTimeZoneErrorClose}
message={t('settings.timeZoneUpdateError')}
/>
<Snackbar
open={alarmEmailsErrorOpen}
autoHideDuration={4000}
onClose={handleAlarmEmailsErrorClose}
message={
t('settings.alarmEmailsUpdateError') ||
'Failed to update email notifications setting'
}
/>
<Snackbar
open={hideDeclinedEventsErrorOpen}
autoHideDuration={4000}
onClose={handleHideDeclinedEventsErrorClose}
message={t('settings.hideDeclinedEventsUpdateError')}
/>
<Snackbar
open={displayWeekNumbersErrorOpen}
autoHideDuration={4000}
onClose={handleDisplayWeekNumbersErrorClose}
message={t('settings.displayWeekNumbersUpdateError')}
/>
<Snackbar
open={workingDaysErrorOpen}
autoHideDuration={4000}
onClose={() => setWorkingDaysErrorOpen(false)}
message={t('settings.workingDaysUpdateError')}
<SettingErrorSnackbars
languageErrorOpen={languageErrorOpen}
timeZoneErrorOpen={timeZoneErrorOpen}
alarmEmailsErrorOpen={alarmEmailsErrorOpen}
hideDeclinedEventsErrorOpen={hideDeclinedEventsErrorOpen}
displayWeekNumbersErrorOpen={displayWeekNumbersErrorOpen}
workingDaysErrorOpen={workingDaysErrorOpen}
handleLanguageErrorClose={handleLanguageErrorClose}
handleTimeZoneErrorClose={handleTimeZoneErrorClose}
handleAlarmEmailsErrorClose={handleAlarmEmailsErrorClose}
handleHideDeclinedEventsErrorClose={handleHideDeclinedEventsErrorClose}
handleDisplayWeekNumbersErrorClose={handleDisplayWeekNumbersErrorClose}
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
/>
</main>
)
}
export default SettingsPage
@@ -0,0 +1,26 @@
import React from 'react'
import { MobileSelector } from '@/components/MobileSelector'
import { SmallTimezoneSelector } from '@/components/Timezone/SmallTimeZoneSelector'
export const MobileTimezoneSelector: React.FC<{
currentTimezone: string
onChange: (tz: string) => void
}> = ({ currentTimezone, onChange }) => {
return (
<MobileSelector
displayText={currentTimezone}
bottomSheetChildren={({ open, onClose }) => (
<SmallTimezoneSelector
open={open}
onClose={onClose}
value={currentTimezone}
onChange={(tz: string) => {
onChange(tz)
onClose()
}}
referenceDate={new Date()}
/>
)}
/>
)
}
@@ -0,0 +1,130 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { useTimeZoneList } from '@/components/Timezone/hooks/useTimeZoneList'
import { TimezoneAutocomplete } from '@/components/Timezone/TimezoneAutocomplete'
import { browserDefaultTimeZone, getTimezoneOffset } from '@/utils/timezone'
import {
Box,
FormControl,
FormControlLabel,
Switch,
Typography
} from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import {
setTimezone as setUserTimeZone,
updateUserConfigurationsAsync
} from '../../User/userSlice'
import {
setIsBrowserDefaultTimeZone,
setTimeZone as setSettingsTimeZone
} from '../SettingsSlice'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { MobileTimezoneSelector } from './MobileTimezoneSelector'
interface TimezoneSelectorProps {
onTimeZoneError: () => void
}
export const TimezoneSelector: React.FC<TimezoneSelectorProps> = ({
onTimeZoneError
}) => {
const dispatch = useAppDispatch()
const { t } = useI18n()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const previousConfig = useAppSelector(state => state.user.coreConfig)
const timezoneList = useTimeZoneList()
const userTimeZone = useAppSelector(
state => state.user?.coreConfig?.datetime?.timeZone
)
const settingTimeZone = useAppSelector(state => state.settings?.timeZone)
const currentTimeZone =
userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone
const isBrowserDefault = useAppSelector(
state => state.settings.isBrowserDefaultTimeZone
)
const handleTimeZoneChange = (newTimeZone: string): void => {
const previousTimeZone = currentTimeZone
dispatch(setUserTimeZone(newTimeZone))
dispatch(setSettingsTimeZone(newTimeZone))
dispatch(
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
)
.unwrap()
.catch(() => {
dispatch(setUserTimeZone(previousTimeZone))
dispatch(setSettingsTimeZone(previousTimeZone))
onTimeZoneError()
})
}
const handleTimeZoneDefaultChange = (isDefault: boolean): void => {
const previousTimeZone = currentTimeZone
dispatch(setIsBrowserDefaultTimeZone(isDefault))
if (isDefault) {
dispatch(setUserTimeZone(null))
dispatch(setSettingsTimeZone(browserDefaultTimeZone))
dispatch(
updateUserConfigurationsAsync({ timezone: null, previousConfig })
)
.unwrap()
.catch(() => {
dispatch(setUserTimeZone(previousTimeZone))
dispatch(setSettingsTimeZone(previousTimeZone))
dispatch(setIsBrowserDefaultTimeZone(!isDefault))
onTimeZoneError()
})
}
}
const inputMinWidth = isMobile ? '100%' : 500
return (
<Box
sx={{
mb: 4
}}
>
<Typography variant="h6" sx={{ mb: 1 }}>
{t('settings.timeZone')}
</Typography>
<Box>
<FormControl size="small" sx={{ minWidth: inputMinWidth }}>
<FormControlLabel
control={
<Switch
checked={isBrowserDefault}
onChange={() => handleTimeZoneDefaultChange(!isBrowserDefault)}
aria-label={t('settings.timeZoneBrowserDefault')}
/>
}
label={t('settings.timeZoneBrowserDefault')}
labelPlacement="start"
sx={{
minWidth: isMobile ? '100%' : 400,
justifyContent: 'space-between',
marginLeft: 0,
mb: 2
}}
/>
{!isMobile && !isBrowserDefault && (
<TimezoneAutocomplete
value={currentTimeZone}
zones={timezoneList.zones}
getTimezoneOffset={getTimezoneOffset}
onChange={handleTimeZoneChange}
/>
)}
</FormControl>
</Box>
{isMobile && !isBrowserDefault && (
<MobileTimezoneSelector
currentTimezone={currentTimeZone}
onChange={handleTimeZoneChange}
/>
)}
</Box>
)
}
@@ -0,0 +1,43 @@
import { useRef } from 'react'
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import {
setLanguage as setUserLanguage,
updateUserConfigurationsAsync
} from '../../User/userSlice'
import { setLanguage as setSettingsLanguage } from '../SettingsSlice'
import { EventChange } from '../LanguageSelector/index.types'
export const useLanguageChange = ({
onLanguageError
}: {
onLanguageError: () => void
}): {
handleLanguageChange: (event: EventChange) => void
currentLanguage: string
} => {
const dispatch = useAppDispatch()
const userLanguage = useAppSelector(state => state.user?.coreConfig.language)
const settingsLanguage = useAppSelector(state => state.settings?.language)
const currentLanguage = userLanguage || settingsLanguage || 'en'
const languageRequestSeq = useRef(0)
const handleLanguageChange = (event: EventChange): void => {
const newLanguage = event.target.value
const previousLanguage = currentLanguage
const requestSeq = ++languageRequestSeq.current
dispatch(setUserLanguage(newLanguage))
dispatch(setSettingsLanguage(newLanguage))
dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
.unwrap()
.catch(() => {
if (requestSeq !== languageRequestSeq.current) {
return
}
dispatch(setUserLanguage(previousLanguage))
dispatch(setSettingsLanguage(previousLanguage))
onLanguageError()
})
}
return { currentLanguage, handleLanguageChange }
}
+9
View File
@@ -129,6 +129,15 @@
"saturday": "SA",
"sunday": "SU"
},
"fullDays": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"end": {
"label": "How many times to repeat",
"never": "Always",
+9
View File
@@ -130,6 +130,15 @@
"saturday": "SA",
"sunday": "DI"
},
"fullDays": {
"monday": "Lundi",
"tuesday": "Mardi",
"wednesday": "Mercredi",
"thursday": "Jeudi",
"friday": "Vendredi",
"saturday": "Samedi",
"sunday": "Dimanche"
},
"end": {
"label": "Fin :",
"never": "Jamais",
+9
View File
@@ -130,6 +130,15 @@
"saturday": "СБ",
"sunday": "ВС"
},
"fullDays": {
"monday": "Понедельник",
"tuesday": "Вторник",
"wednesday": "Среда",
"thursday": "Четверг",
"friday": "Пятница",
"saturday": "Суббота",
"sunday": "Воскресенье"
},
"end": {
"label": "Сколько раз повторить",
"never": "Всегда",
+9
View File
@@ -128,6 +128,15 @@
"saturday": "Bảy",
"sunday": "CN"
},
"fullDays": {
"monday": "Thứ hai",
"tuesday": "Thứ ba",
"wednesday": "Thứ tư",
"thursday": "Thứ năm",
"friday": "Thứ sáu",
"saturday": "Thứ bảy",
"sunday": "Chủ nhật"
},
"end": {
"label": "Lặp lại bao nhiêu lần",
"never": "Luôn luôn",