Merge pull request #818 from lethemanh/698-personal-settings
#698 personal settings
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,27 +18,72 @@ export const FC_DAYS = [
|
||||
{ fc: 0, ics: 'SU' }
|
||||
]
|
||||
|
||||
export function WeekDaySelector({
|
||||
const WeekDayStyle = (
|
||||
isSelected: boolean,
|
||||
isMobile: boolean,
|
||||
disabled?: boolean
|
||||
): SxProps<Theme> => {
|
||||
const desktopStyle = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'primary.main' : '#AEAEC0',
|
||||
color: isSelected ? '#fff' : '#8C9CAF',
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: isSelected ? 'primary.main' : 'transparent',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
'@media (hover: hover)': {
|
||||
'&:hover': !disabled
|
||||
? {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.main',
|
||||
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
|
||||
}: WeekDaySelectorProps) {
|
||||
}) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const getDayLabel = (ics: string) => {
|
||||
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.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')
|
||||
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) => {
|
||||
const handleToggle = (fcDay: number): void => {
|
||||
if (disabled) return
|
||||
const updated = selectedDays.includes(fcDay)
|
||||
? selectedDays.filter(d => d !== fcDay)
|
||||
@@ -46,10 +92,10 @@ export function WeekDaySelector({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" gap={1}>
|
||||
<Box display="flex" flexDirection={isMobile ? 'column' : 'row'} gap={1}>
|
||||
{FC_DAYS.map(({ fc, ics }) => {
|
||||
const isSelected = selectedDays.includes(fc)
|
||||
const fullLabel = getDayLabel(ics)
|
||||
const fullLabel = getDayLabel(ics, isMobile)
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -60,30 +106,9 @@ export function WeekDaySelector({
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => handleToggle(fc)}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'primary.main' : '#AEAEC0',
|
||||
color: isSelected ? '#fff' : '#8C9CAF',
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: isSelected ? 'primary.main' : 'transparent',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
'&:hover': !disabled
|
||||
? {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.main',
|
||||
color: '#fff'
|
||||
}
|
||||
: undefined
|
||||
}}
|
||||
sx={WeekDayStyle(isSelected, isMobile, disabled)}
|
||||
>
|
||||
{fullLabel.charAt(0)}
|
||||
{isMobile ? fullLabel : fullLabel.charAt(0)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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,53 +52,81 @@ 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">
|
||||
<div className="left-menu">
|
||||
<IconButton
|
||||
onClick={onOpenSidebar}
|
||||
aria-label={t('menubar.toggleSidebar')}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<div className="menu-items">
|
||||
<SmallNavigationControls onNavigate={handleNavigation} />
|
||||
</div>
|
||||
<div className="menu-items">
|
||||
<Stack direction="row" className="current-date-time">
|
||||
<Typography
|
||||
sx={{ lineHeight: 'unset' }}
|
||||
onClick={onToggleDatePicker}
|
||||
>
|
||||
{dateLabel}
|
||||
</Typography>
|
||||
{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
|
||||
size="small"
|
||||
onClick={onToggleDatePicker}
|
||||
aria-label={
|
||||
openDatePicker
|
||||
? t('menubar.hideDatePicker')
|
||||
: t('menubar.showDatePicker')
|
||||
}
|
||||
title={
|
||||
openDatePicker
|
||||
? t('menubar.hideDatePicker')
|
||||
: t('menubar.showDatePicker')
|
||||
}
|
||||
aria-expanded={openDatePicker}
|
||||
onClick={onOpenSidebar}
|
||||
aria-label={t('menubar.toggleSidebar')}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
{openDatePicker ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />}
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-menu">
|
||||
<div className="search-container">
|
||||
<SearchBar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="menu-items">
|
||||
<SmallNavigationControls onNavigate={handleNavigation} />
|
||||
</div>
|
||||
<div className="menu-items">
|
||||
<Stack direction="row" className="current-date-time">
|
||||
<Typography
|
||||
sx={{ lineHeight: 'unset' }}
|
||||
onClick={onToggleDatePicker}
|
||||
>
|
||||
{dateLabel}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onToggleDatePicker}
|
||||
aria-label={
|
||||
openDatePicker
|
||||
? t('menubar.hideDatePicker')
|
||||
: t('menubar.showDatePicker')
|
||||
}
|
||||
title={
|
||||
openDatePicker
|
||||
? t('menubar.hideDatePicker')
|
||||
: t('menubar.showDatePicker')
|
||||
}
|
||||
aria-expanded={openDatePicker}
|
||||
>
|
||||
{openDatePicker ? (
|
||||
<ArrowDropUpIcon />
|
||||
) : (
|
||||
<ArrowDropDownIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-menu">
|
||||
<div className="search-container">
|
||||
<SearchBar />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{openDatePicker ? (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
)}
|
||||
{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')}
|
||||
{isMobile ? (
|
||||
<MobileSettingsPage
|
||||
activeSettingsSubTab={activeSettingsSubTab}
|
||||
setLanguageErrorOpen={setLanguageErrorOpen}
|
||||
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
|
||||
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
|
||||
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
|
||||
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
|
||||
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
|
||||
handleSettingsSubTabChange={handleSettingsSubTabChange}
|
||||
setActiveSettingsSubTab={setActiveSettingsSubTab}
|
||||
/>
|
||||
) : (
|
||||
<DesktopSettingsPage
|
||||
activeSettingsSubTab={activeSettingsSubTab}
|
||||
setLanguageErrorOpen={setLanguageErrorOpen}
|
||||
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
|
||||
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
|
||||
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
|
||||
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
|
||||
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
|
||||
handleSettingsSubTabChange={handleSettingsSubTabChange}
|
||||
setActiveSettingsSubTab={setActiveSettingsSubTab}
|
||||
/>
|
||||
)}
|
||||
<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 }
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -130,6 +130,15 @@
|
||||
"saturday": "СБ",
|
||||
"sunday": "ВС"
|
||||
},
|
||||
"fullDays": {
|
||||
"monday": "Понедельник",
|
||||
"tuesday": "Вторник",
|
||||
"wednesday": "Среда",
|
||||
"thursday": "Четверг",
|
||||
"friday": "Пятница",
|
||||
"saturday": "Суббота",
|
||||
"sunday": "Воскресенье"
|
||||
},
|
||||
"end": {
|
||||
"label": "Сколько раз повторить",
|
||||
"never": "Всегда",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user