#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 { CalendarSidebarProps } from './SideBar'
|
||||||
import { SidebarCommonContent } from './SidebarCommonContent'
|
import { SidebarCommonContent } from './SidebarCommonContent'
|
||||||
import { ViewSwitcher } from './ViewSwitcher'
|
import { ViewSwitcher } from './ViewSwitcher'
|
||||||
import { MainTitle } from '@/components/Menubar/MainTitle'
|
import { MainTitle } from '@/components/Menubar/MainTitle'
|
||||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'
|
|
||||||
import { useI18n } from 'twake-i18n'
|
import { useI18n } from 'twake-i18n'
|
||||||
import { AppListMenu } from '@/components/Menubar/AppListMenu'
|
import { useAppDispatch } from '@/app/hooks'
|
||||||
import { UserMenu } from '@/components/Menubar/UserMenu'
|
|
||||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
|
||||||
import { useUtilMenus } from '../hooks/useUtilMenus'
|
import { useUtilMenus } from '../hooks/useUtilMenus'
|
||||||
import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice'
|
import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice'
|
||||||
|
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
|
||||||
|
import { UtilButtons } from './UtilButtons'
|
||||||
|
|
||||||
export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
|
export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
|
||||||
open,
|
open,
|
||||||
@@ -26,25 +32,20 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
|
|||||||
onDateChange
|
onDateChange
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const user = useAppSelector(state => state.user.userData)
|
const theme = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const {
|
const { handleSettingsClick } = useUtilMenus()
|
||||||
anchorEl,
|
|
||||||
supportLink,
|
|
||||||
userMenuAnchorEl,
|
|
||||||
handleAppMenuOpen,
|
|
||||||
handleAppMenuClose,
|
|
||||||
handleUserMenuOpen,
|
|
||||||
handleUserMenuClose,
|
|
||||||
handleSettingsClick,
|
|
||||||
handleLogoutClick
|
|
||||||
} = useUtilMenus()
|
|
||||||
|
|
||||||
const openSearch = (): void => {
|
const openSearch = (): void => {
|
||||||
dispatch(setIsMobileSearchOpen(true))
|
dispatch(setIsMobileSearchOpen(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSettings = (): void => {
|
||||||
|
handleSettingsClick()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
@@ -77,39 +78,7 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
|
|||||||
onDateChange={onDateChange}
|
onDateChange={onDateChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box display="flex" alignItems="center">
|
<UtilButtons isIframe={isIframe} />
|
||||||
{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>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -127,6 +96,32 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
|
|||||||
setSelectedCalendars={setSelectedCalendars}
|
setSelectedCalendars={setSelectedCalendars}
|
||||||
openSearchOnMobile={openSearch}
|
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>
|
</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'
|
import { useI18n } from 'twake-i18n'
|
||||||
|
|
||||||
interface WeekDaySelectorProps {
|
interface WeekDaySelectorProps {
|
||||||
@@ -17,50 +18,12 @@ export const FC_DAYS = [
|
|||||||
{ fc: 0, ics: 'SU' }
|
{ fc: 0, ics: 'SU' }
|
||||||
]
|
]
|
||||||
|
|
||||||
export function WeekDaySelector({
|
const WeekDayStyle = (
|
||||||
selectedDays,
|
isSelected: boolean,
|
||||||
onChange,
|
isMobile: boolean,
|
||||||
disabled
|
disabled?: boolean
|
||||||
}: WeekDaySelectorProps) {
|
): SxProps<Theme> => {
|
||||||
const { t } = useI18n()
|
const desktopStyle = {
|
||||||
|
|
||||||
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={{
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@@ -74,6 +37,7 @@ export function WeekDaySelector({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
bgcolor: isSelected ? 'primary.main' : 'transparent',
|
bgcolor: isSelected ? 'primary.main' : 'transparent',
|
||||||
cursor: disabled ? 'default' : 'pointer',
|
cursor: disabled ? 'default' : 'pointer',
|
||||||
|
'@media (hover: hover)': {
|
||||||
'&:hover': !disabled
|
'&:hover': !disabled
|
||||||
? {
|
? {
|
||||||
borderColor: 'primary.main',
|
borderColor: 'primary.main',
|
||||||
@@ -81,9 +45,70 @@ export function WeekDaySelector({
|
|||||||
color: '#fff'
|
color: '#fff'
|
||||||
}
|
}
|
||||||
: undefined
|
: 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>
|
</Box>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import './Menubar.styl'
|
|||||||
import { DatePickerMobile } from './components/DatePickerMobile'
|
import { DatePickerMobile } from './components/DatePickerMobile'
|
||||||
import { Typography } from '@mui/material'
|
import { Typography } from '@mui/material'
|
||||||
import { SmallNavigationControls } from './components/SmallNavigationControls'
|
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 = {
|
export type MobileMenubarProps = {
|
||||||
calendarRef: React.RefObject<CalendarApi | null>
|
calendarRef: React.RefObject<CalendarApi | null>
|
||||||
@@ -27,6 +30,9 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
|
|||||||
onOpenSidebar
|
onOpenSidebar
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const view = useAppSelector(state => state.settings.view)
|
||||||
|
|
||||||
const [openDatePicker, setOpenDatePicker] = useState(false)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="menubar menubar--mobile">
|
<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">
|
<div className="left-menu">
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={onOpenSidebar}
|
onClick={onOpenSidebar}
|
||||||
@@ -83,7 +111,11 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
|
|||||||
}
|
}
|
||||||
aria-expanded={openDatePicker}
|
aria-expanded={openDatePicker}
|
||||||
>
|
>
|
||||||
{openDatePicker ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />}
|
{openDatePicker ? (
|
||||||
|
<ArrowDropUpIcon />
|
||||||
|
) : (
|
||||||
|
<ArrowDropDownIcon />
|
||||||
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,6 +125,8 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{openDatePicker ? (
|
{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 { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||||
import { useTimeZoneList } from '@/components/Timezone/hooks/useTimeZoneList'
|
|
||||||
import { WeekDaySelector } from '@/components/Event/WeekDaySelector'
|
import { WeekDaySelector } from '@/components/Event/WeekDaySelector'
|
||||||
import { TimezoneAutocomplete } from '@/components/Timezone/TimezoneAutocomplete'
|
|
||||||
import { browserDefaultTimeZone, getTimezoneOffset } from '@/utils/timezone'
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
Switch,
|
Switch,
|
||||||
Typography
|
Typography
|
||||||
} from '@linagora/twake-mui'
|
} from '@linagora/twake-mui'
|
||||||
import { useRef, useCallback, useEffect } from 'react'
|
import { useRef, useCallback, useEffect } from 'react'
|
||||||
import { useI18n } from 'twake-i18n'
|
import { useI18n } from 'twake-i18n'
|
||||||
import {
|
import { updateUserConfigurationsAsync } from '../User/userSlice'
|
||||||
setLanguage as setUserLanguage,
|
|
||||||
setTimezone as setUserTimeZone,
|
|
||||||
updateUserConfigurationsAsync
|
|
||||||
} from '../User/userSlice'
|
|
||||||
import { AVAILABLE_LANGUAGES } from './constants'
|
|
||||||
import {
|
import {
|
||||||
BusinessHour,
|
BusinessHour,
|
||||||
setBusinessHours,
|
setBusinessHours,
|
||||||
setDisplayWeekNumbers,
|
setDisplayWeekNumbers,
|
||||||
setHideDeclinedEvents,
|
setHideDeclinedEvents,
|
||||||
setIsBrowserDefaultTimeZone,
|
|
||||||
setLanguage as setSettingsLanguage,
|
|
||||||
setTimeZone as setSettingsTimeZone,
|
|
||||||
setWorkingDays
|
setWorkingDays
|
||||||
} from './SettingsSlice'
|
} from './SettingsSlice'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { LanguageSelector } from './LanguageSelector'
|
||||||
|
import { TimezoneSelector } from './TimezoneSelector'
|
||||||
|
|
||||||
interface GeneralSettingsProps {
|
interface GeneralSettingsProps {
|
||||||
onLanguageError: () => void
|
onLanguageError: () => void
|
||||||
@@ -48,22 +38,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
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 hideDeclinedEvents = useAppSelector(
|
const hideDeclinedEvents = useAppSelector(
|
||||||
state => state.settings?.hideDeclinedEvents
|
state => state.settings?.hideDeclinedEvents
|
||||||
@@ -78,67 +53,6 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
null
|
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 => {
|
const handleHideDeclinedEvents = (value: boolean): void => {
|
||||||
dispatch(setHideDeclinedEvents(value))
|
dispatch(setHideDeclinedEvents(value))
|
||||||
dispatch(updateUserConfigurationsAsync({ hideDeclinedEvents: value }))
|
dispatch(updateUserConfigurationsAsync({ hideDeclinedEvents: value }))
|
||||||
@@ -207,74 +121,15 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputMinWidth = isMobile ? '100%' : 500
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="settings-tab-content">
|
<Box className="settings-tab-content">
|
||||||
{/* Language */}
|
{/* Language */}
|
||||||
<Box sx={{ mb: 4 }}>
|
<LanguageSelector onLanguageError={onLanguageError} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Timezone */}
|
{/* Timezone */}
|
||||||
<Box
|
<TimezoneSelector onTimeZoneError={onTimeZoneError} />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Working */}
|
{/* Working */}
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
@@ -285,7 +140,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
selectedDays={businessHours?.daysOfWeek ?? []}
|
selectedDays={businessHours?.daysOfWeek ?? []}
|
||||||
onChange={days => handleBusinessHour({ days })}
|
onChange={days => handleBusinessHour({ days })}
|
||||||
/>
|
/>
|
||||||
<FormControl size="small" sx={{ minWidth: 500, mt: 2 }}>
|
<FormControl size="small" sx={{ minWidth: inputMinWidth, mt: 2 }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
@@ -296,7 +151,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
label={t('settings.showOnlyWorkingDays')}
|
label={t('settings.showOnlyWorkingDays')}
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 400,
|
minWidth: isMobile ? '100%' : 400,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginLeft: 0
|
marginLeft: 0
|
||||||
}}
|
}}
|
||||||
@@ -309,7 +164,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||||
{t('settings.calAndEvent')}
|
{t('settings.calAndEvent')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<FormControl size="small" sx={{ minWidth: 500 }}>
|
<FormControl size="small" sx={{ minWidth: inputMinWidth }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
@@ -321,7 +176,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
label={t('settings.showDeclinedEvent')}
|
label={t('settings.showDeclinedEvent')}
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 400,
|
minWidth: isMobile ? '100%' : 400,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginLeft: 0
|
marginLeft: 0
|
||||||
}}
|
}}
|
||||||
@@ -337,7 +192,7 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
|
|||||||
label={t('settings.displayWeekNumbers')}
|
label={t('settings.displayWeekNumbers')}
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 400,
|
minWidth: isMobile ? '100%' : 400,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginLeft: 0
|
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,
|
setAlarmEmails,
|
||||||
updateUserConfigurationsAsync
|
updateUserConfigurationsAsync
|
||||||
} from '../User/userSlice'
|
} from '../User/userSlice'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
|
||||||
interface NotificationsSettingsProps {
|
interface NotificationsSettingsProps {
|
||||||
onAlarmEmailsError: () => void
|
onAlarmEmailsError: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationsSettings({
|
export const NotificationsSettings: React.FC<NotificationsSettingsProps> = ({
|
||||||
onAlarmEmailsError
|
onAlarmEmailsError
|
||||||
}: NotificationsSettingsProps) {
|
}) => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
|
||||||
const alarmEmailsEnabled = useAppSelector(
|
const alarmEmailsEnabled = useAppSelector(
|
||||||
state => state.user?.alarmEmailsEnabled ?? true
|
state => state.user?.alarmEmailsEnabled ?? true
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleAlarmEmailsToggle = (
|
const handleAlarmEmailsToggle = (
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
) => {
|
): void => {
|
||||||
const newValue = event.target.checked
|
const newValue = event.target.checked
|
||||||
const previousValue = alarmEmailsEnabled
|
const previousValue = alarmEmailsEnabled
|
||||||
dispatch(setAlarmEmails(newValue))
|
dispatch(setAlarmEmails(newValue))
|
||||||
@@ -48,7 +52,11 @@ export function NotificationsSettings({
|
|||||||
}
|
}
|
||||||
label={t('settings.notifications.email')}
|
label={t('settings.notifications.email')}
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
sx={{ minWidth: 400, justifyContent: 'space-between', marginLeft: 0 }}
|
sx={{
|
||||||
|
minWidth: isMobile ? '100%' : 400,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginLeft: 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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)
|
height calc(100vh - 70px)
|
||||||
padding 0 10px 10px 0;
|
padding 0 10px 10px 0;
|
||||||
|
|
||||||
|
&--mobile
|
||||||
|
padding 0
|
||||||
|
|
||||||
.main-layout.settings-layout.isInIframe
|
.main-layout.settings-layout.isInIframe
|
||||||
height 100vh
|
height 100vh
|
||||||
|
|
||||||
@@ -106,3 +109,7 @@
|
|||||||
.MuiTabs-indicator
|
.MuiTabs-indicator
|
||||||
background-color #F67E35
|
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 { useState } from 'react'
|
||||||
import { useI18n } from 'twake-i18n'
|
|
||||||
import { GeneralSettings } from './GeneralSettings'
|
|
||||||
import { NotificationsSettings } from './NotificationSettings'
|
|
||||||
import './SettingsPage.styl'
|
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'
|
export type SidebarNavItem = 'settings' | 'sync'
|
||||||
type SettingsSubTab = 'settings' | 'notifications'
|
export type SettingsSubTab = 'settings' | 'notifications'
|
||||||
|
|
||||||
export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
const SettingsPage: React.FC<{ isInIframe?: boolean }> = ({ isInIframe }) => {
|
||||||
const dispatch = useAppDispatch()
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const [activeNavItem, setActiveNavItem] = useState<SidebarNavItem>('settings')
|
|
||||||
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
||||||
useState<SettingsSubTab>('settings')
|
useState<SettingsSubTab>('settings')
|
||||||
|
|
||||||
@@ -40,163 +23,73 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
useState(false)
|
useState(false)
|
||||||
const [workingDaysErrorOpen, setWorkingDaysErrorOpen] = 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 = (
|
const handleSettingsSubTabChange = (
|
||||||
_event: React.SyntheticEvent,
|
_event: React.SyntheticEvent,
|
||||||
newValue: SettingsSubTab
|
newValue: SettingsSubTab
|
||||||
) => {
|
): void => {
|
||||||
setActiveSettingsSubTab(newValue)
|
setActiveSettingsSubTab(newValue)
|
||||||
}
|
}
|
||||||
const handleLanguageErrorClose = () => {
|
const handleLanguageErrorClose = (): void => {
|
||||||
setLanguageErrorOpen(false)
|
setLanguageErrorOpen(false)
|
||||||
}
|
}
|
||||||
const handleTimeZoneErrorClose = () => {
|
const handleTimeZoneErrorClose = (): void => {
|
||||||
setTimeZoneErrorOpen(false)
|
setTimeZoneErrorOpen(false)
|
||||||
}
|
}
|
||||||
const handleHideDeclinedEventsErrorClose = () => {
|
const handleHideDeclinedEventsErrorClose = (): void => {
|
||||||
setHideDeclinedEventsErrorOpen(false)
|
setHideDeclinedEventsErrorOpen(false)
|
||||||
}
|
}
|
||||||
const handleAlarmEmailsErrorClose = () => {
|
const handleAlarmEmailsErrorClose = (): void => {
|
||||||
setAlarmEmailsErrorOpen(false)
|
setAlarmEmailsErrorOpen(false)
|
||||||
}
|
}
|
||||||
const handleDisplayWeekNumbersErrorClose = () => {
|
const handleDisplayWeekNumbersErrorClose = (): void => {
|
||||||
setDisplayWeekNumbersErrorOpen(false)
|
setDisplayWeekNumbersErrorOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className={`main-layout settings-layout${isInIframe ? ' isInIframe' : ''}`}
|
className={`main-layout settings-layout${isInIframe ? ' isInIframe' : ''} ${isMobile ? 'settings-layout--mobile' : ''}`}
|
||||||
>
|
>
|
||||||
<Box className="settings-sidebar">
|
{isMobile ? (
|
||||||
<List>
|
<MobileSettingsPage
|
||||||
<ListItem
|
activeSettingsSubTab={activeSettingsSubTab}
|
||||||
className={`settings-nav-item ${activeNavItem === 'settings' ? 'active' : ''}`}
|
setLanguageErrorOpen={setLanguageErrorOpen}
|
||||||
onClick={() => handleNavItemClick('settings')}
|
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
|
||||||
sx={{ cursor: 'pointer' }}
|
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
|
||||||
>
|
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
|
||||||
<ListItemIcon>
|
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
|
||||||
<SettingsIcon />
|
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
|
||||||
</ListItemIcon>
|
handleSettingsSubTabChange={handleSettingsSubTabChange}
|
||||||
<ListItemText primary={t('settings.title') || 'Settings'} />
|
setActiveSettingsSubTab={setActiveSettingsSubTab}
|
||||||
</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>
|
) : (
|
||||||
)}
|
<DesktopSettingsPage
|
||||||
</Box>
|
activeSettingsSubTab={activeSettingsSubTab}
|
||||||
<Box className="settings-content-body">
|
setLanguageErrorOpen={setLanguageErrorOpen}
|
||||||
{activeNavItem === 'settings' && (
|
setTimeZoneErrorOpen={setTimeZoneErrorOpen}
|
||||||
<>
|
setAlarmEmailsErrorOpen={setAlarmEmailsErrorOpen}
|
||||||
{activeSettingsSubTab === 'settings' && (
|
setHideDeclinedEventsErrorOpen={setHideDeclinedEventsErrorOpen}
|
||||||
<GeneralSettings
|
setDisplayWeekNumbersErrorOpen={setDisplayWeekNumbersErrorOpen}
|
||||||
onLanguageError={() => setLanguageErrorOpen(true)}
|
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
|
||||||
onTimeZoneError={() => setTimeZoneErrorOpen(true)}
|
handleSettingsSubTabChange={handleSettingsSubTabChange}
|
||||||
onHideDeclinedEventsError={() =>
|
setActiveSettingsSubTab={setActiveSettingsSubTab}
|
||||||
setHideDeclinedEventsErrorOpen(true)
|
|
||||||
}
|
|
||||||
onDisplayWeekNumbersError={() =>
|
|
||||||
setDisplayWeekNumbersErrorOpen(true)
|
|
||||||
}
|
|
||||||
onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeSettingsSubTab === 'notifications' && (
|
<SettingErrorSnackbars
|
||||||
<NotificationsSettings
|
languageErrorOpen={languageErrorOpen}
|
||||||
onAlarmEmailsError={() => setAlarmEmailsErrorOpen(true)}
|
timeZoneErrorOpen={timeZoneErrorOpen}
|
||||||
/>
|
alarmEmailsErrorOpen={alarmEmailsErrorOpen}
|
||||||
)}
|
hideDeclinedEventsErrorOpen={hideDeclinedEventsErrorOpen}
|
||||||
</>
|
displayWeekNumbersErrorOpen={displayWeekNumbersErrorOpen}
|
||||||
)}
|
workingDaysErrorOpen={workingDaysErrorOpen}
|
||||||
</Box>
|
handleLanguageErrorClose={handleLanguageErrorClose}
|
||||||
{activeNavItem === 'sync' && (
|
handleTimeZoneErrorClose={handleTimeZoneErrorClose}
|
||||||
<Box className="settings-tab-content">
|
handleAlarmEmailsErrorClose={handleAlarmEmailsErrorClose}
|
||||||
<Typography variant="body1" color="text.secondary">
|
handleHideDeclinedEventsErrorClose={handleHideDeclinedEventsErrorClose}
|
||||||
{t('settings.sync.empty') || 'Sync settings coming soon'}
|
handleDisplayWeekNumbersErrorClose={handleDisplayWeekNumbersErrorClose}
|
||||||
</Typography>
|
setWorkingDaysErrorOpen={setWorkingDaysErrorOpen}
|
||||||
</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')}
|
|
||||||
/>
|
/>
|
||||||
</main>
|
</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",
|
"saturday": "SA",
|
||||||
"sunday": "SU"
|
"sunday": "SU"
|
||||||
},
|
},
|
||||||
|
"fullDays": {
|
||||||
|
"monday": "Monday",
|
||||||
|
"tuesday": "Tuesday",
|
||||||
|
"wednesday": "Wednesday",
|
||||||
|
"thursday": "Thursday",
|
||||||
|
"friday": "Friday",
|
||||||
|
"saturday": "Saturday",
|
||||||
|
"sunday": "Sunday"
|
||||||
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"label": "How many times to repeat",
|
"label": "How many times to repeat",
|
||||||
"never": "Always",
|
"never": "Always",
|
||||||
|
|||||||
@@ -130,6 +130,15 @@
|
|||||||
"saturday": "SA",
|
"saturday": "SA",
|
||||||
"sunday": "DI"
|
"sunday": "DI"
|
||||||
},
|
},
|
||||||
|
"fullDays": {
|
||||||
|
"monday": "Lundi",
|
||||||
|
"tuesday": "Mardi",
|
||||||
|
"wednesday": "Mercredi",
|
||||||
|
"thursday": "Jeudi",
|
||||||
|
"friday": "Vendredi",
|
||||||
|
"saturday": "Samedi",
|
||||||
|
"sunday": "Dimanche"
|
||||||
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"label": "Fin :",
|
"label": "Fin :",
|
||||||
"never": "Jamais",
|
"never": "Jamais",
|
||||||
|
|||||||
@@ -130,6 +130,15 @@
|
|||||||
"saturday": "СБ",
|
"saturday": "СБ",
|
||||||
"sunday": "ВС"
|
"sunday": "ВС"
|
||||||
},
|
},
|
||||||
|
"fullDays": {
|
||||||
|
"monday": "Понедельник",
|
||||||
|
"tuesday": "Вторник",
|
||||||
|
"wednesday": "Среда",
|
||||||
|
"thursday": "Четверг",
|
||||||
|
"friday": "Пятница",
|
||||||
|
"saturday": "Суббота",
|
||||||
|
"sunday": "Воскресенье"
|
||||||
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"label": "Сколько раз повторить",
|
"label": "Сколько раз повторить",
|
||||||
"never": "Всегда",
|
"never": "Всегда",
|
||||||
|
|||||||
@@ -128,6 +128,15 @@
|
|||||||
"saturday": "Bảy",
|
"saturday": "Bảy",
|
||||||
"sunday": "CN"
|
"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": {
|
"end": {
|
||||||
"label": "Lặp lại bao nhiêu lần",
|
"label": "Lặp lại bao nhiêu lần",
|
||||||
"never": "Luôn luôn",
|
"never": "Luôn luôn",
|
||||||
|
|||||||
Reference in New Issue
Block a user