From 4e57f6b3425f445b4afd754fdb4ed82fb4b4c3f1 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Fri, 3 Apr 2026 08:49:37 +0200 Subject: [PATCH] [#668] Tablet view (#716) * draft for tablet view * adapt sidebar to tablet view * split menubar to handle tablet and desktop separatly * split sidebar to handle tablet and desktop separatly * prettier day navigationin tablet view * added missing translations * coderabbit comments * fix UI of view switcher on tablet, remove hard text * fix style of selected view option * fix wrong I18n key * extracted menubar recurring components to own files * adjust test case of calendar component * refactor function change calendar view Co-authored-by: Camille Moussu Co-authored-by: lethemanh --- __test__/components/Calendar.test.tsx | 108 ++++- .../components/Calendar.timezone.test.tsx | 3 +- .../components/EventModifications.test.tsx | 120 +++-- __test__/components/Menubar.test.tsx | 107 +++-- .../components/MiniCalendarColor.test.tsx | 9 +- .../Calendars/TimezoneInGrid.test.tsx | 18 +- src/App.tsx | 2 +- src/components/Calendar/Calendar.tsx | 152 +++--- src/components/Calendar/CalendarLayout.tsx | 47 +- src/components/Calendar/MiniCalendar.tsx | 5 +- src/components/Calendar/SideBar.tsx | 148 ++++++ .../Calendar/Sidebar/DesktopSidebar.tsx | 82 ++++ src/components/Calendar/Sidebar/SideBar.tsx | 32 ++ .../Calendar/Sidebar/SidebarCommonContent.tsx | 37 ++ .../Calendar/Sidebar/TabletSidebar.tsx | 121 +++++ .../Calendar/handlers/eventHandlers.ts | 2 +- .../Calendar/handlers/viewHandlers.ts | 12 +- src/components/Calendar/utils/constants.ts | 5 + src/components/Event/EventChip/EventChip.tsx | 3 +- src/components/Menubar/AppIcon.tsx | 40 ++ src/components/Menubar/AppListMenu.tsx | 53 +++ src/components/Menubar/DesktopMenubar.tsx | 123 +++++ src/components/Menubar/MainTitle.tsx | 62 +++ src/components/Menubar/Menubar.styl | 1 - src/components/Menubar/Menubar.tsx | 450 +++--------------- src/components/Menubar/NavigationControls.tsx | 44 ++ src/components/Menubar/SelectView.tsx | 42 ++ src/components/Menubar/TabletMenubar.tsx | 124 +++++ src/components/Menubar/UserMenu.tsx | 134 ++++++ src/features/Calendars/useCalendarLoader.ts | 3 +- src/locales/en.json | 6 + src/locales/fr.json | 8 +- src/locales/ru.json | 8 +- src/locales/vi.json | 8 +- src/setupTests.ts | 3 + src/utils/dateUtils.ts | 3 +- 36 files changed, 1549 insertions(+), 576 deletions(-) create mode 100644 src/components/Calendar/SideBar.tsx create mode 100644 src/components/Calendar/Sidebar/DesktopSidebar.tsx create mode 100644 src/components/Calendar/Sidebar/SideBar.tsx create mode 100644 src/components/Calendar/Sidebar/SidebarCommonContent.tsx create mode 100644 src/components/Calendar/Sidebar/TabletSidebar.tsx create mode 100644 src/components/Calendar/utils/constants.ts create mode 100644 src/components/Menubar/AppIcon.tsx create mode 100644 src/components/Menubar/AppListMenu.tsx create mode 100644 src/components/Menubar/DesktopMenubar.tsx create mode 100644 src/components/Menubar/MainTitle.tsx create mode 100644 src/components/Menubar/NavigationControls.tsx create mode 100644 src/components/Menubar/SelectView.tsx create mode 100644 src/components/Menubar/TabletMenubar.tsx create mode 100644 src/components/Menubar/UserMenu.tsx diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx index 484be77..13ae428 100644 --- a/__test__/components/Calendar.test.tsx +++ b/__test__/components/Calendar.test.tsx @@ -1,5 +1,6 @@ import CalendarApp from '@/components/Calendar/Calendar' import CalendarLayout from '@/components/Calendar/CalendarLayout' +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import * as calendarDetailThunks from '@/features/Calendars/services' import * as servicesModule from '@/features/Calendars/services' import { searchUsers } from '@/features/User/userAPI' @@ -14,7 +15,16 @@ const mockedSearchUsers = searchUsers as jest.MockedFunction // Test wrapper component to provide calendarRef function CalendarTestWrapper() { const calendarRef = useRef(null) - return + return ( + + ) } describe('CalendarSelection', () => { @@ -143,7 +153,14 @@ describe('CalendarSelection', () => { const mockCalendarRef = { current: null } await act(async () => { renderWithProviders( - , + , preloadedState ) }) @@ -159,7 +176,14 @@ describe('CalendarSelection', () => { const mockCalendarRef = { current: null } await act(async () => { renderWithProviders( - , + , preloadedState ) }) @@ -194,7 +218,14 @@ describe('CalendarSelection', () => { ) await act(async () => { renderWithProviders( - , + , preloadedState ) }) @@ -207,7 +238,14 @@ describe('CalendarSelection', () => { const mockCalendarRef = { current: null } await act(async () => { renderWithProviders( - , + , preloadedState ) }) @@ -423,8 +461,9 @@ describe('calendar Availability search', () => { }) const calendarApi = calendarRef.current + expect(calendarApi).not.toBeNull() await act(async () => { - calendarApi.changeView('dayGridMonth') + calendarApi?.changeView(CALENDAR_VIEWS.dayGridMonth) }) await waitFor(() => { expect(screen.queryAllByRole('columnheader').length).toBe(14) @@ -472,9 +511,10 @@ describe('calendar Availability search', () => { const calendarRef = window.__calendarRef const calendarApi = calendarRef.current + expect(calendarApi).not.toBeNull() await act(async () => { - calendarApi.changeView('dayGridMonth') + calendarApi?.changeView(CALENDAR_VIEWS.dayGridMonth) fireEvent.click(screen.getByTestId('ChevronRightIcon')) }) @@ -554,7 +594,14 @@ describe('calendar Availability search', () => { await act(async () => { renderWithProviders( - , + , stateWithManyCalendars ) }) @@ -612,7 +659,14 @@ describe('calendar Availability search', () => { await act(async () => { renderWithProviders( - , + , calendarsWithSelected ) }) @@ -646,10 +700,20 @@ describe('calendar Availability search', () => { ) await act(async () => { - renderWithProviders(, { - ...preloadedState, - calendars: { ...preloadedState.calendars, pending: false } - }) + renderWithProviders( + , + { + ...preloadedState, + calendars: { ...preloadedState.calendars, pending: false } + } + ) }) await waitFor( @@ -682,7 +746,14 @@ describe('calendar Availability search', () => { await act(async () => { renderWithProviders( - , + , stateWithUndefinedCalendars ) }) @@ -701,7 +772,14 @@ describe('calendar Availability search', () => { await act(async () => { renderWithProviders( - , + , stateWithUndefinedTemp ) }) diff --git a/__test__/components/Calendar.timezone.test.tsx b/__test__/components/Calendar.timezone.test.tsx index f688f21..ccbe68c 100644 --- a/__test__/components/Calendar.timezone.test.tsx +++ b/__test__/components/Calendar.timezone.test.tsx @@ -1,4 +1,5 @@ import CalendarLayout from '@/components/Calendar/CalendarLayout' +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import { act, waitFor } from '@testing-library/react' import { renderWithProviders } from '../utils/Renderwithproviders' @@ -51,7 +52,7 @@ describe('Calendar – dayHeaderContent respects selected timezone', () => { await waitFor(() => expect(calendarRef?.current).not.toBeNull()) await act(async () => { - calendarRef.current?.changeView('timeGridWeek') + calendarRef.current?.changeView(CALENDAR_VIEWS.timeGridWeek) }) await waitFor(() => { diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx index c7b58bd..2ea685a 100644 --- a/__test__/components/EventModifications.test.tsx +++ b/__test__/components/EventModifications.test.tsx @@ -75,7 +75,14 @@ describe('CalendarApp integration', () => { const mockCalendarRef = { current: null } renderWithProviders( - , + , preloadedState ) } @@ -172,7 +179,14 @@ describe('CalendarApp integration', () => { }) const mockCalendarRef = { current: null } renderWithProviders( - , + , preloadedState ) const card = screen.getByTestId('event-card-event1') @@ -191,7 +205,14 @@ describe('CalendarApp integration', () => { }) const mockCalendarRef = { current: null } renderWithProviders( - , + , preloadedState ) @@ -211,7 +232,14 @@ describe('CalendarApp integration', () => { }) const mockCalendarRef = { current: null } renderWithProviders( - , + , preloadedState ) @@ -222,46 +250,56 @@ describe('CalendarApp integration', () => { it('does render a title for events without any attendees or user as organizer', async () => { const mockCalendarRef = { current: null } - renderWithProviders(, { - user: { - userData: { - sub: 'test', - email: 'test@test.com', - sid: 'mockSid', - openpaasId: '667037022b752d0026472254' - }, - tokens: { - accessToken: 'token' - } - }, - calendars: { - list: { - '667037022b752d0026472254/cal1': { - name: 'Calendar 1', - id: '667037022b752d0026472254/cal1', - color: { light: '#FF0000', dark: '#000' }, - owner: { emails: ['alice@example.com'] }, - events: { - event1: { - id: 'event1', - calId: '667037022b752d0026472254/cal1', - uid: 'event1', - start: new Date().toISOString(), - end: new Date(Date.now() + 3600000).toISOString(), - partstat: 'ACCEPTED', - organizer: { - cn: 'Alice', - cal_address: 'alice@example.com' - }, - class: 'PUBLIC', - title: 'Public Event' - } - } + renderWithProviders( + , + { + user: { + userData: { + sub: 'test', + email: 'test@test.com', + sid: 'mockSid', + openpaasId: '667037022b752d0026472254' + }, + tokens: { + accessToken: 'token' } }, - pending: false + calendars: { + list: { + '667037022b752d0026472254/cal1': { + name: 'Calendar 1', + id: '667037022b752d0026472254/cal1', + color: { light: '#FF0000', dark: '#000' }, + owner: { emails: ['alice@example.com'] }, + events: { + event1: { + id: 'event1', + calId: '667037022b752d0026472254/cal1', + uid: 'event1', + start: new Date().toISOString(), + end: new Date(Date.now() + 3600000).toISOString(), + partstat: 'ACCEPTED', + organizer: { + cn: 'Alice', + cal_address: 'alice@example.com' + }, + class: 'PUBLIC', + title: 'Public Event' + } + } + } + }, + pending: false + } } - }) + ) expect(screen.getByText('Public Event')).toBeInTheDocument() }) diff --git a/__test__/components/Menubar.test.tsx b/__test__/components/Menubar.test.tsx index 4fdf15f..2a71ece 100644 --- a/__test__/components/Menubar.test.tsx +++ b/__test__/components/Menubar.test.tsx @@ -1,3 +1,4 @@ +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import { Menubar } from '@/components/Menubar/Menubar' import * as oidcAuth from '@/features/User/oidcAuth' import { redirectTo } from '@/utils/navigation' @@ -35,8 +36,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -53,8 +55,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -71,8 +74,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -100,8 +104,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -131,8 +136,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -161,8 +167,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -191,8 +198,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -221,8 +229,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -251,8 +260,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -283,8 +293,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -315,8 +326,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -335,8 +347,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -356,8 +369,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -377,8 +391,9 @@ describe('Calendar App Component Display Tests', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -427,8 +442,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -447,8 +463,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -467,8 +484,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -487,8 +505,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -509,8 +528,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -531,8 +551,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -553,8 +574,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -579,8 +601,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -618,8 +641,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -650,8 +674,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedStateWithSettings ) @@ -681,8 +706,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -707,8 +733,9 @@ describe('Menubar interaction with expanded Dialog', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -745,8 +772,9 @@ describe('Menubar logout flow', () => { {}} + onToggleSidebar={() => {}} currentDate={new Date()} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -815,8 +843,9 @@ describe('Logo click navigation to current week', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} onViewChange={mockOnViewChange} onDateChange={mockOnDateChange} />, @@ -832,9 +861,9 @@ describe('Logo click navigation to current week', () => { expect(store.getState().settings.view).toBe('calendar') }) - expect(mockChangeView).toHaveBeenCalledWith('timeGridWeek') + expect(mockChangeView).toHaveBeenCalledWith(CALENDAR_VIEWS.timeGridWeek) expect(mockToday).toHaveBeenCalled() - expect(mockOnViewChange).toHaveBeenCalledWith('timeGridWeek') + expect(mockOnViewChange).toHaveBeenCalledWith(CALENDAR_VIEWS.timeGridWeek) expect(mockOnDateChange).toHaveBeenCalled() }) @@ -857,8 +886,9 @@ describe('Logo click navigation to current week', () => { {}} currentDate={mockCurrentDate} - currentView="timeGridDay" + currentView={CALENDAR_VIEWS.timeGridDay} onViewChange={mockOnViewChange} onDateChange={mockOnDateChange} />, @@ -874,9 +904,9 @@ describe('Logo click navigation to current week', () => { expect(store.getState().settings.view).toBe('calendar') }) - expect(mockChangeView).toHaveBeenCalledWith('timeGridWeek') + expect(mockChangeView).toHaveBeenCalledWith(CALENDAR_VIEWS.timeGridWeek) expect(mockToday).toHaveBeenCalled() - expect(mockOnViewChange).toHaveBeenCalledWith('timeGridWeek') + expect(mockOnViewChange).toHaveBeenCalledWith(CALENDAR_VIEWS.timeGridWeek) expect(mockOnDateChange).toHaveBeenCalled() }) @@ -899,8 +929,9 @@ describe('Logo click navigation to current week', () => { {}} currentDate={mockCurrentDate} - currentView="timeGridWeek" + currentView={CALENDAR_VIEWS.timeGridWeek} onViewChange={mockOnViewChange} onDateChange={mockOnDateChange} />, @@ -932,8 +963,9 @@ describe('Logo click navigation to current week', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} onViewChange={mockOnViewChange} onDateChange={mockOnDateChange} />, @@ -970,8 +1002,9 @@ describe('Logo click navigation to current week', () => { {}} currentDate={mockCurrentDate} - currentView="dayGridMonth" + currentView={CALENDAR_VIEWS.dayGridMonth} />, preloadedState ) @@ -985,7 +1018,7 @@ describe('Logo click navigation to current week', () => { expect(store.getState().settings.view).toBe('calendar') }) - expect(mockChangeView).toHaveBeenCalledWith('timeGridWeek') + expect(mockChangeView).toHaveBeenCalledWith(CALENDAR_VIEWS.timeGridWeek) expect(mockToday).toHaveBeenCalled() }) }) diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 071fd1d..1b93bdf 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -49,7 +49,14 @@ describe('MiniCalendar', () => { } const mockCalendarRef = { current: null } renderWithProviders( - , + , preloadedState ) } diff --git a/__test__/features/Calendars/TimezoneInGrid.test.tsx b/__test__/features/Calendars/TimezoneInGrid.test.tsx index 790a0a5..6c8d364 100644 --- a/__test__/features/Calendars/TimezoneInGrid.test.tsx +++ b/__test__/features/Calendars/TimezoneInGrid.test.tsx @@ -43,7 +43,14 @@ describe('Calendar - Timezone Integration', () => { it('renders TimezoneSelector in week view', async () => { renderWithProviders( - , + , baseState ) @@ -57,7 +64,14 @@ describe('Calendar - Timezone Integration', () => { const setTimeZoneSpy = jest.spyOn(SettingsSlice, 'setTimeZone') renderWithProviders( - , + , baseState ) diff --git a/src/App.tsx b/src/App.tsx index d0ad553..13db4b8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,7 +43,7 @@ const isValidLanguage = ( return !!lang && SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage) } -function App() { +function App(): JSX.Element { const error = useAppSelector(state => state.user.error) const appLoading = useAppSelector(state => state.loading.isLoading) const userLanguage = useAppSelector(state => state.user.coreConfig.language) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 993b3e1..6e367d6 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -5,6 +5,7 @@ import { CalendarEvent } from '@/features/Events/EventsTypes' import ImportAlert from '@/features/Events/ImportAlert' import SearchResultsPage from '@/features/Search/SearchResultsPage' import { setTimeZone } from '@/features/Settings/SettingsSlice' +import { useScreenSizeDetection } from '@/useScreenSizeDetection' import { setDisplayedDateAndRange } from '@/utils/CalendarRangeManager' import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' import { setSelectedCalendars as setSelectedCalendarsToStorage } from '@/utils/storage/setSelectedCalendars' @@ -20,7 +21,7 @@ import interactionPlugin from '@fullcalendar/interaction' import momentTimezonePlugin from '@fullcalendar/moment-timezone' import FullCalendar from '@fullcalendar/react' import timeGridPlugin from '@fullcalendar/timegrid' -import { Box, Button, radius } from '@linagora/twake-mui' +import { Fab } from '@linagora/twake-mui' import AddIcon from '@mui/icons-material/Add' import moment from 'moment-timezone' import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' @@ -32,18 +33,17 @@ import { EventErrorHandler } from '../Error/EventErrorHandler' import { EditModeDialog } from '../Event/EditModeDialog' import { Menubar, MenubarProps } from '../Menubar/Menubar' import './Calendar.styl' -import CalendarSelection from './CalendarSelection' import './CustomCalendar.styl' import { useCalendarEventHandlers } from './hooks/useCalendarEventHandlers' import { useCalendarViewHandlers } from './hooks/useCalendarViewHandlers' -import { MiniCalendar } from './MiniCalendar' -import { TempCalendarsInput } from './TempCalendarsInput' +import Sidebar from './Sidebar/SideBar' import { TimezoneSelector } from './TimezoneSelector' import { eventToFullCalendarFormat, extractEvents, updateSlotLabelVisibility } from './utils/calendarUtils' +import { CALENDAR_VIEWS } from './utils/constants' const localeMap: Record = { fr: frLocale, @@ -55,16 +55,24 @@ const localeMap: Record = { interface CalendarAppProps { calendarRef: MutableRefObject onDateChange?: (date: Date) => void - onViewChange?: (view: string) => void + onViewChange: (view: string) => void menubarProps?: MenubarProps + openSidebar: boolean + onCloseSidebar: () => void + setCurrentView: (view: string) => void + currentView: string } export default function CalendarApp({ calendarRef, onDateChange, onViewChange, - menubarProps -}: CalendarAppProps) { + menubarProps, + openSidebar, + onCloseSidebar, + setCurrentView, + currentView +}: CalendarAppProps): JSX.Element { const [selectedDate, setSelectedDate] = useState(new Date()) const [debouncedDate, setDebouncedDate] = useState(new Date()) useEffect(() => { @@ -84,6 +92,9 @@ export default function CalendarApp({ const hideDeclinedEvents = useAppSelector( state => state.settings.hideDeclinedEvents ) + + const { isTablet } = useScreenSizeDetection() + const hiddenDays = useMemo(() => { if (!hideWorkingDays || !workingDays || workingDays.length === 0) return [] const validWorkingDays = workingDays.filter(d => d >= 0 && d <= 6) @@ -113,7 +124,6 @@ export default function CalendarApp({ [calendarIdsString] ) - const [currentView, setCurrentView] = useState('timeGridWeek') const timezone = useAppSelector(state => state.settings.timeZone) ?? browserDefaultTimeZone @@ -297,6 +307,20 @@ export default function CalendarApp({ const [tempUsers, setTempUsers] = useState([]) const [tempEvent, setTempEvent] = useState({} as CalendarEvent) + useEffect(() => { + if (view !== 'calendar') return + const targetView = + currentView || + (isTablet ? CALENDAR_VIEWS.timeGridDay : CALENDAR_VIEWS.timeGridWeek) + + if (calendarRef.current?.view.type === targetView) return + const id = requestAnimationFrame(() => { + if (calendarRef.current?.view.type !== targetView) { + calendarRef.current?.changeView(targetView) + } + }) + return () => cancelAnimationFrame(id) + }, [view, isTablet, currentView, calendarRef]) // Event handlers const eventHandlers = useCalendarEventHandlers({ setSelectedRange, @@ -321,7 +345,7 @@ export default function CalendarApp({ calendarRef, setSelectedDate, setSelectedMiniDate, - onViewChange, + onViewChange: setCurrentView, calendars, tempcalendars, // Note: To preserve current logic, this will temporarily disable eslint for react-hooks/refs @@ -341,68 +365,40 @@ export default function CalendarApp({
- - - - - - - - { - eventHandlers.handleDateSelect(null as unknown as DateSelectArg) - }} - /> - -
- -
-
+ eventHandlers.handleDateSelect(null)} + onViewChange={onViewChange} + selectedMiniDate={selectedMiniDate} + setSelectedMiniDate={setSelectedMiniDate} + selectedCalendars={selectedCalendars} + setSelectedCalendars={setSelectedCalendars} + tempUsers={tempUsers} + setTempUsers={setTempUsers} + currentView={currentView} + />
{menubarProps?.isIframe && } + {isTablet && ( + eventHandlers.handleDateSelect(null)} + sx={{ + position: 'fixed', + bottom: 24, + right: 24, + zIndex: 20, + borderRadius: '16px' + }} + > + + + )} {view === 'calendar' && ( { @@ -474,7 +476,7 @@ export default function CalendarApp({ month: 'short', timeZone: timezone }) - if (arg.view.type === 'dayGridMonth') { + if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) { return ( { @@ -543,7 +543,7 @@ export default function CalendarApp({ return (
{weekDay} - {arg.view.type !== 'dayGridMonth' && ( + {arg.view.type !== CALENDAR_VIEWS.dayGridMonth && ( diff --git a/src/components/Calendar/CalendarLayout.tsx b/src/components/Calendar/CalendarLayout.tsx index b664532..2dcbd74 100644 --- a/src/components/Calendar/CalendarLayout.tsx +++ b/src/components/Calendar/CalendarLayout.tsx @@ -1,5 +1,6 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks' import SettingsPage from '@/features/Settings/SettingsPage' +import { useScreenSizeDetection } from '@/useScreenSizeDetection' import { getViewRange } from '@/utils/dateUtils' import type { CalendarApi } from '@fullcalendar/core' import CozyBridge from 'cozy-external-bridge' @@ -8,6 +9,8 @@ import { ErrorSnackbar } from '../Error/ErrorSnackbar' import { refreshCalendars } from '../Event/utils/eventUtils' import { Menubar, MenubarProps } from '../Menubar/Menubar' import CalendarApp from './Calendar' +import { CALENDAR_VIEWS } from './utils/constants' +import { setView } from '@/features/Settings/SettingsSlice' export default function CalendarLayout() { const calendarRef = useRef(null) @@ -16,8 +19,31 @@ export default function CalendarLayout() { const selectedCalendars = useAppSelector(state => state.calendars.list) const tempcalendars = useAppSelector(state => state.calendars.templist) const view = useAppSelector(state => state.settings.view) + + const { isTablet } = useScreenSizeDetection() + const [openSidebar, setOpenSideBar] = useState(false) + const [currentDate, setCurrentDate] = useState(new Date()) - const [currentView, setCurrentView] = useState('timeGridWeek') + const [currentView, setCurrentView] = useState( + isTablet ? CALENDAR_VIEWS.timeGridDay : CALENDAR_VIEWS.timeGridWeek + ) + + useEffect(() => { + const setView = () => + setCurrentView(prev => { + if ( + prev !== CALENDAR_VIEWS.timeGridDay && + prev !== CALENDAR_VIEWS.timeGridWeek + ) { + return prev + } + + return isTablet + ? CALENDAR_VIEWS.timeGridDay + : CALENDAR_VIEWS.timeGridWeek + }) + setView() + }, [isTablet]) const isInIframe = useMemo(() => new CozyBridge().isInIframe(), []) const handleRefresh = async () => { @@ -47,8 +73,18 @@ export default function CalendarLayout() { setCurrentDate(date) } - const handleViewChange = (view: string) => { + const handleViewChange = (view: string): void => { + if (!calendarRef.current) return + dispatch(setView('calendar')) + + calendarRef.current.changeView(view) + + // Notify parent about view change setCurrentView(view) + + // Notify parent about date change after view change + const newDate = calendarRef.current.getDate() + handleDateChange(newDate) } // Hide topbar navigation elements when in settings view (same as fullscreen dialog mode) @@ -72,7 +108,8 @@ export default function CalendarLayout() { onDateChange: handleDateChange, currentView, onViewChange: handleViewChange, - isIframe: isInIframe + isIframe: isInIframe, + onToggleSidebar: () => setOpenSideBar(true) } return ( @@ -84,6 +121,10 @@ export default function CalendarLayout() { onDateChange={handleDateChange} onViewChange={handleViewChange} menubarProps={menubarProps} + openSidebar={openSidebar} + onCloseSidebar={() => setOpenSideBar(false)} + setCurrentView={setCurrentView} + currentView={currentView} /> )} {view === 'settings' && } diff --git a/src/components/Calendar/MiniCalendar.tsx b/src/components/Calendar/MiniCalendar.tsx index 09721e7..1ae3aff 100644 --- a/src/components/Calendar/MiniCalendar.tsx +++ b/src/components/Calendar/MiniCalendar.tsx @@ -9,6 +9,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import moment from 'moment' import { useEffect, useState } from 'react' import { useI18n } from 'twake-i18n' +import { CALENDAR_VIEWS } from './utils/constants' export function MiniCalendar({ calendarRef, @@ -70,11 +71,11 @@ export function MiniCalendar({ const isToday = date.getTime() === today.getTime() const isSelectedDay = - calendarRef.current?.view.type === 'timeGridDay' && + calendarRef.current?.view.type === CALENDAR_VIEWS.timeGridDay && date.getTime() === selected.getTime() const isInSelectedWeek = - calendarRef.current?.view.type === 'timeGridWeek' || + calendarRef.current?.view.type === CALENDAR_VIEWS.timeGridWeek || calendarRef.current?.view.type === undefined ? (() => { const startOfWeek = computeStartOfTheWeek(selected) diff --git a/src/components/Calendar/SideBar.tsx b/src/components/Calendar/SideBar.tsx new file mode 100644 index 0000000..4b8ef01 --- /dev/null +++ b/src/components/Calendar/SideBar.tsx @@ -0,0 +1,148 @@ +import { CalendarApi } from '@fullcalendar/core' +import { Box, Button, Drawer, radius } from '@linagora/twake-mui' +import AddIcon from '@mui/icons-material/Add' +import CalendarViewDayOutlinedIcon from '@mui/icons-material/CalendarViewDayOutlined' +import CalendarViewMonthOutlinedIcon from '@mui/icons-material/CalendarViewMonthOutlined' +import CalendarViewWeekOutlinedIcon from '@mui/icons-material/CalendarViewWeekOutlined' +import { Dispatch, MutableRefObject, SetStateAction } from 'react' +import { useI18n } from 'twake-i18n' +import { User } from '../Attendees/PeopleSearch' +import { FieldWithLabel } from '../Event/components/FieldWithLabel' +import CalendarSelection from './CalendarSelection' +import { MiniCalendar } from './MiniCalendar' +import { TempCalendarsInput } from './TempCalendarsInput' + +interface CalendarSidebarProps { + isTablet: boolean + open: boolean + onClose: () => void + calendarRef: MutableRefObject + isIframe?: boolean + onCreateEvent: () => void + onViewChange: (view: string) => void + selectedMiniDate: Date + setSelectedMiniDate: (date: Date) => void + selectedCalendars: string[] + setSelectedCalendars: Dispatch> + tempUsers: User[] + setTempUsers: Dispatch> +} + +export default function Sidebar({ + isTablet, + open, + onClose, + calendarRef, + isIframe, + onCreateEvent, + onViewChange, + selectedMiniDate, + setSelectedMiniDate, + selectedCalendars, + setSelectedCalendars, + tempUsers, + setTempUsers +}: CalendarSidebarProps) { + const { t } = useI18n() + + return ( + + {!isTablet && ( + <> + + + + + + + + )} + + {isTablet && ( + + + + + + )} + + + + + + + + + + ) +} diff --git a/src/components/Calendar/Sidebar/DesktopSidebar.tsx b/src/components/Calendar/Sidebar/DesktopSidebar.tsx new file mode 100644 index 0000000..3ce4465 --- /dev/null +++ b/src/components/Calendar/Sidebar/DesktopSidebar.tsx @@ -0,0 +1,82 @@ +import { Box, Button, Drawer, radius } from '@linagora/twake-mui' +import AddIcon from '@mui/icons-material/Add' +import { useI18n } from 'twake-i18n' +import { MiniCalendar } from '../MiniCalendar' +import { CalendarSidebarProps } from './SideBar' +import { SidebarCommonContent } from './SidebarCommonContent' + +export function DesktopSidebar({ + calendarRef, + isIframe, + onCreateEvent, + selectedMiniDate, + setSelectedMiniDate, + tempUsers, + setTempUsers, + selectedCalendars, + setSelectedCalendars +}: CalendarSidebarProps) { + const { t } = useI18n() + + return ( + + + + + + + + + + + + ) +} diff --git a/src/components/Calendar/Sidebar/SideBar.tsx b/src/components/Calendar/Sidebar/SideBar.tsx new file mode 100644 index 0000000..4db5aea --- /dev/null +++ b/src/components/Calendar/Sidebar/SideBar.tsx @@ -0,0 +1,32 @@ +import { useScreenSizeDetection } from '@/useScreenSizeDetection' +import { CalendarApi } from '@fullcalendar/core' +import { Dispatch, MutableRefObject, SetStateAction } from 'react' +import { User } from '../../Attendees/PeopleSearch' +import { DesktopSidebar } from './DesktopSidebar' +import { TabletSidebar } from './TabletSidebar' + +export interface CalendarSidebarProps { + open: boolean + onClose: () => void + calendarRef: MutableRefObject + isIframe?: boolean + onCreateEvent: () => void + onViewChange: (view: string) => void + selectedMiniDate: Date + setSelectedMiniDate: (date: Date) => void + selectedCalendars: string[] + setSelectedCalendars: Dispatch> + tempUsers: User[] + setTempUsers: Dispatch> + currentView: string +} + +export default function Sidebar(sharedProps: CalendarSidebarProps) { + const { isTablet } = useScreenSizeDetection() + + return isTablet ? ( + + ) : ( + + ) +} diff --git a/src/components/Calendar/Sidebar/SidebarCommonContent.tsx b/src/components/Calendar/Sidebar/SidebarCommonContent.tsx new file mode 100644 index 0000000..94d0f99 --- /dev/null +++ b/src/components/Calendar/Sidebar/SidebarCommonContent.tsx @@ -0,0 +1,37 @@ +import { Box } from '@linagora/twake-mui' +import CalendarSelection from '../CalendarSelection' +import { TempCalendarsInput } from '../TempCalendarsInput' +import { CalendarSidebarProps } from './SideBar' + +export function SidebarCommonContent({ + onCreateEvent, + tempUsers, + setTempUsers, + selectedCalendars, + setSelectedCalendars +}: Pick< + CalendarSidebarProps, + | 'onCreateEvent' + | 'tempUsers' + | 'setTempUsers' + | 'selectedCalendars' + | 'setSelectedCalendars' +>) { + return ( + <> + + + + + + + + ) +} diff --git a/src/components/Calendar/Sidebar/TabletSidebar.tsx b/src/components/Calendar/Sidebar/TabletSidebar.tsx new file mode 100644 index 0000000..54b5530 --- /dev/null +++ b/src/components/Calendar/Sidebar/TabletSidebar.tsx @@ -0,0 +1,121 @@ +import { + Drawer, + ListItemIcon, + ListItemText, + MenuItem, + MenuList, + useTheme +} from '@linagora/twake-mui' +import CalendarViewDayOutlinedIcon from '@mui/icons-material/CalendarViewDayOutlined' +import CalendarViewMonthOutlinedIcon from '@mui/icons-material/CalendarViewMonthOutlined' +import CalendarViewWeekOutlinedIcon from '@mui/icons-material/CalendarViewWeekOutlined' +import { useI18n } from 'twake-i18n' +import { FieldWithLabel } from '../../Event/components/FieldWithLabel' +import { CALENDAR_VIEWS } from '../utils/constants' +import { CalendarSidebarProps } from './SideBar' +import { SidebarCommonContent } from './SidebarCommonContent' + +const VIEW_OPTIONS = [ + { + label: 'menubar.views.day', + value: CALENDAR_VIEWS.timeGridDay, + icon: + }, + { + label: 'menubar.views.week', + value: CALENDAR_VIEWS.timeGridWeek, + icon: + }, + { + label: 'menubar.views.month', + value: CALENDAR_VIEWS.dayGridMonth, + icon: + } +] + +export function TabletSidebar({ + open, + onClose, + onViewChange, + onCreateEvent, + tempUsers, + setTempUsers, + selectedCalendars, + setSelectedCalendars, + currentView +}: CalendarSidebarProps) { + const { t } = useI18n() + const theme = useTheme() + + const changeViewAndClose = (view: string): void => { + onViewChange(view) + onClose() + } + + return ( + + + + {VIEW_OPTIONS.map(option => { + const isSelected = option.value === currentView + return ( + changeViewAndClose(option.value)} + > + + {option.icon} + + + + ) + })} + + + + + + ) +} diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index 1ff1376..1dec19f 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -63,7 +63,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { timezone } = props - const handleDateSelect = (selectInfo: DateSelectArg) => { + const handleDateSelect = (selectInfo: DateSelectArg | null) => { setSelectedRange(selectInfo) if (tempUsers) { const newEvent: CalendarEvent = { diff --git a/src/components/Calendar/handlers/viewHandlers.ts b/src/components/Calendar/handlers/viewHandlers.ts index 31320ac..5a97127 100644 --- a/src/components/Calendar/handlers/viewHandlers.ts +++ b/src/components/Calendar/handlers/viewHandlers.ts @@ -11,6 +11,7 @@ import { ViewMountArg } from '@fullcalendar/core' import React from 'react' +import { CALENDAR_VIEWS } from '../utils/constants' import { createMouseHandlers } from './mouseHandlers' export interface ViewHandlersProps { @@ -54,16 +55,16 @@ export const createViewHandlers = (props: ViewHandlersProps) => { } const handleDayHeaderDidMount = (arg: DayHeaderMountArg) => { - if (arg.view.type === 'timeGridWeek') { + if (arg.view.type === CALENDAR_VIEWS.timeGridWeek) { const headerEl = arg.el const handleDayHeaderClick = () => { - calendarRef.current?.changeView('timeGridDay', arg.date) + calendarRef.current?.changeView(CALENDAR_VIEWS.timeGridDay, arg.date) setSelectedDate(new Date(arg.date)) setSelectedMiniDate(new Date(arg.date)) if (onViewChange) { - onViewChange('timeGridDay') + onViewChange(CALENDAR_VIEWS.timeGridDay) } } @@ -81,7 +82,10 @@ export const createViewHandlers = (props: ViewHandlersProps) => { } const handleViewDidMount = (arg: ViewMountArg) => { - if (arg.view.type === 'timeGridWeek' || arg.view.type === 'timeGridDay') { + if ( + arg.view.type === CALENDAR_VIEWS.timeGridWeek || + arg.view.type === CALENDAR_VIEWS.timeGridDay + ) { const calendarEl = document.querySelector('.fc') as HTMLElement if (calendarEl) { const mouseHandlers = createMouseHandlers({ calendarEl }) diff --git a/src/components/Calendar/utils/constants.ts b/src/components/Calendar/utils/constants.ts new file mode 100644 index 0000000..987c7cc --- /dev/null +++ b/src/components/Calendar/utils/constants.ts @@ -0,0 +1,5 @@ +export const CALENDAR_VIEWS = { + dayGridMonth: 'dayGridMonth', + timeGridWeek: 'timeGridWeek', + timeGridDay: 'timeGridDay' +} diff --git a/src/components/Event/EventChip/EventChip.tsx b/src/components/Event/EventChip/EventChip.tsx index f314fd6..8c4aee2 100644 --- a/src/components/Event/EventChip/EventChip.tsx +++ b/src/components/Event/EventChip/EventChip.tsx @@ -1,3 +1,4 @@ +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import { Avatar, Box, @@ -76,7 +77,7 @@ export function EventChip({ } // View and time calculations - const isMonthView = arg.view.type === 'dayGridMonth' + const isMonthView = arg.view.type === CALENDAR_VIEWS.dayGridMonth const timeZone = arg.view.calendar?.getOption('timeZone') || 'UTC' const { startTime, endTime } = getEventTimes(event, timeZone) const eventLength = getEventDuration(event) diff --git a/src/components/Menubar/AppIcon.tsx b/src/components/Menubar/AppIcon.tsx new file mode 100644 index 0000000..76f2662 --- /dev/null +++ b/src/components/Menubar/AppIcon.tsx @@ -0,0 +1,40 @@ +import { Box, Typography } from '@linagora/twake-mui' + +export type AppIconProps = { + name: string + link: string + icon: string +} + +export function AppIcon({ prop }: { prop: AppIconProps }) { + return ( + + + + {prop.name} + + + ) +} diff --git a/src/components/Menubar/AppListMenu.tsx b/src/components/Menubar/AppListMenu.tsx new file mode 100644 index 0000000..db20091 --- /dev/null +++ b/src/components/Menubar/AppListMenu.tsx @@ -0,0 +1,53 @@ +import { IconButton, Popover } from '@linagora/twake-mui' +import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined' +import { useI18n } from 'twake-i18n' +import { AppIcon, AppIconProps } from './AppIcon' + +export function AppListMenu({ + anchorEl, + onAppMenuOpen, + onAppMenuClose +}: { + anchorEl: HTMLElement | null + onAppMenuOpen: (event: React.MouseEvent) => void + onAppMenuClose: () => void +}) { + const { t } = useI18n() + + const applist: AppIconProps[] = window.appList ?? [] + if (!(applist.length > 0)) { + return null + } + + return ( + <> + + + + + +
+ {applist.map((prop: AppIconProps) => ( + + ))} +
+
+ + ) +} diff --git a/src/components/Menubar/DesktopMenubar.tsx b/src/components/Menubar/DesktopMenubar.tsx new file mode 100644 index 0000000..4c11f73 --- /dev/null +++ b/src/components/Menubar/DesktopMenubar.tsx @@ -0,0 +1,123 @@ +import { IconButton } from '@linagora/twake-mui' +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' +import RefreshIcon from '@mui/icons-material/Refresh' +import { useI18n } from 'twake-i18n' +import { AppListMenu } from './AppListMenu' +import SearchBar from './EventSearchBar' +import { MainTitle } from './MainTitle' +import { SharedMenubarProps } from './Menubar' +import { NavigationControls } from './NavigationControls' +import { SelectView } from './SelectView' +import { UserMenu } from './UserMenu' + +export function DesktopMenubar({ + calendarRef, + currentView, + isIframe, + dateLabel, + supportLink, + anchorEl, + onAppMenuOpen, + onAppMenuClose, + onUserMenuOpen, + onSettingsClick, + onLogoutClick, + onNavigate, + onRefresh, + onViewChange, + onDateChange, + user, + userMenuAnchorEl, + onUserMenuClose +}: SharedMenubarProps) { + const { t } = useI18n() + + return ( +
+
+ {!isIframe && ( +
+ +
+ )} + +
+ +
+ +
+
+

{dateLabel}

+
+
+
+ +
+
+ +
+ +
+ + + +
+ +
+ +
+ + {!isIframe && ( + <> + {supportLink && ( +
+ + + +
+ )} + +
+ +
+ + )} + +
+ +
+
+
+ ) +} diff --git a/src/components/Menubar/MainTitle.tsx b/src/components/Menubar/MainTitle.tsx new file mode 100644 index 0000000..a4d5649 --- /dev/null +++ b/src/components/Menubar/MainTitle.tsx @@ -0,0 +1,62 @@ +import { useAppDispatch } from '@/app/hooks' +import { setView } from '@/features/Settings/SettingsSlice' +import logo from '@/static/header-logo.svg' +import { CalendarApi } from '@fullcalendar/core' +import { Button } from '@linagora/twake-mui' +import React from 'react' +import { useI18n } from 'twake-i18n' +import { CALENDAR_VIEWS } from '../Calendar/utils/constants' + +export type MainTitleProps = { + calendarRef: React.RefObject + currentView: string + onViewChange?: (view: string) => void + onDateChange?: (date: Date) => void +} + +export function MainTitle({ + calendarRef, + currentView, + onViewChange, + onDateChange +}: MainTitleProps) { + const { t } = useI18n() + const dispatch = useAppDispatch() + + const handleLogoClick = async () => { + if (!calendarRef.current) return + + await dispatch(setView('calendar')) + + if (currentView !== CALENDAR_VIEWS.timeGridWeek) { + calendarRef.current.changeView(CALENDAR_VIEWS.timeGridWeek) + if (onViewChange) { + onViewChange(CALENDAR_VIEWS.timeGridWeek) + } + } + + calendarRef.current.today() + + if (onDateChange) { + const newDate = calendarRef.current.getDate() + onDateChange(newDate) + } + } + + return ( +
+ +
+ ) +} diff --git a/src/components/Menubar/Menubar.styl b/src/components/Menubar/Menubar.styl index 11c5305..a3122af 100644 --- a/src/components/Menubar/Menubar.styl +++ b/src/components/Menubar/Menubar.styl @@ -44,7 +44,6 @@ .menubar-item display flex align-items center - margin-right 65px .tc-home cursor pointer diff --git a/src/components/Menubar/Menubar.tsx b/src/components/Menubar/Menubar.tsx index d62abed..599b964 100644 --- a/src/components/Menubar/Menubar.tsx +++ b/src/components/Menubar/Menubar.tsx @@ -1,37 +1,16 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks' import { setView } from '@/features/Settings/SettingsSlice' import { Logout } from '@/features/User/oidcAuth' -import logo from '@/static/header-logo.svg' -import { getInitials, stringToGradient } from '@/utils/avatarUtils' -import { getUserDisplayName } from '@/utils/userUtils' +import { userData } from '@/features/User/userDataTypes' +import { useScreenSizeDetection } from '@/useScreenSizeDetection' import { redirectTo } from '@/utils/navigation' import { CalendarApi } from '@fullcalendar/core' -import { - Avatar, - Box, - Button, - ButtonGroup, - Divider, - FormControl, - IconButton, - Menu, - MenuItem, - Popover, - Select, - Typography -} from '@linagora/twake-mui' -import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined' -import HelpOutlineIcon from '@mui/icons-material/HelpOutline' -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import ChevronRightIcon from '@mui/icons-material/ChevronRight' -import LogoutIcon from '@mui/icons-material/Logout' -import RefreshIcon from '@mui/icons-material/Refresh' -import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined' import React, { useEffect, useState } from 'react' import { push } from 'redux-first-history' import { useI18n } from 'twake-i18n' -import SearchBar from './EventSearchBar' +import { DesktopMenubar } from './DesktopMenubar' import './Menubar.styl' +import { TabletMenubar } from './TabletMenubar' export type AppIconProps = { name: string @@ -47,26 +26,53 @@ export type MenubarProps = { currentView: string onViewChange?: (view: string) => void isIframe?: boolean + onToggleSidebar: () => void } -export function Menubar({ +export type SharedMenubarProps = MenubarProps & { + dateLabel: string + supportLink: string | undefined + anchorEl: HTMLElement | null + onAppMenuOpen: (event: React.MouseEvent) => void + onAppMenuClose: () => void + userMenuAnchorEl: HTMLElement | null + onUserMenuOpen: (event: React.MouseEvent) => void + onUserMenuClose: () => void + onSettingsClick: () => void + onLogoutClick: () => void + onNavigate: (action: 'prev' | 'next' | 'today') => void + onViewChange: (view: string) => void + user: userData | null +} + +export const Menubar: React.FC = ({ calendarRef, onRefresh, currentDate, onDateChange, currentView, onViewChange, - isIframe -}: MenubarProps) { + isIframe, + onToggleSidebar +}) => { const { t } = useI18n() // deliberately NOT using f() + const user = useAppSelector(state => state.user.userData) - const applist: AppIconProps[] = window.appList ?? [] const supportLink = window.SUPPORT_URL const [anchorEl, setAnchorEl] = useState(null) const [userMenuAnchorEl, setUserMenuAnchorEl] = useState( null ) const dispatch = useAppDispatch() + const { isTablet } = useScreenSizeDetection() + + useEffect(() => { + const resetMenuAnchorsOnResize = (): void => { + setAnchorEl(null) + setUserMenuAnchorEl(null) + } + resetMenuAnchorsOnResize() + }, [isTablet]) useEffect(() => { if (!user) { @@ -77,13 +83,6 @@ export function Menubar({ if (!user) { return null } - const handleOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget) - } - - const handleClose = () => { - setAnchorEl(null) - } const handleNavigation = async (action: 'prev' | 'next' | 'today') => { if (!calendarRef.current) return @@ -108,29 +107,19 @@ export function Menubar({ } const handleViewChange = async (view: string) => { - if (!calendarRef.current) return - await dispatch(setView('calendar')) - - calendarRef.current.changeView(view) - // Notify parent about view change if (onViewChange) { onViewChange(view) } - - // Notify parent about date change after view change - if (onDateChange) { - const newDate = calendarRef.current.getDate() - onDateChange(newDate) - } } - const open = Boolean(anchorEl) - const userMenuOpen = Boolean(userMenuAnchorEl) + const handleAppMenuOpen = (event: React.MouseEvent) => + setAnchorEl(event.currentTarget) - const handleUserMenuClick = (event: React.MouseEvent) => { + const handleAppMenuClose = () => setAnchorEl(null) + + const handleUserMenuOpen = (event: React.MouseEvent) => setUserMenuAnchorEl(event.currentTarget) - } const handleUserMenuClose = () => { setUserMenuAnchorEl(null) @@ -154,341 +143,32 @@ export function Menubar({ const monthName = t(`months.standalone.${monthIndex}`) const dateLabel = `${monthName} ${year}` - return ( - <> -
-
- {!isIframe && ( -
- -
- )} -
-
- - - - - -
-
-
-
-

{dateLabel}

-
-
-
-
-
- -
-
- - - -
-
- - - -
- {!isIframe && ( - <> - {supportLink && ( -
- - - -
- )} - -
- {applist.length > 0 && ( - - - - )} -
- - )} - -
- - {!isIframe ? ( - - {getInitials(getUserDisplayName(user))} - - ) : ( - - )} - -
-
-
- -
- {applist.map((prop: AppIconProps) => ( - - ))} -
-
- - - - - {getInitials(getUserDisplayName(user))} - - - {getUserDisplayName(user)} - - - {user?.email} - - - - - {t('menubar.settings') || 'Settings'} - - - - - {t('menubar.logout') || 'Logout'} - - - - ) -} - -export type MainTitleProps = { - calendarRef: React.RefObject - currentView: string - onViewChange?: (view: string) => void - onDateChange?: (date: Date) => void -} - -export function MainTitle({ - calendarRef, - currentView, - onViewChange, - onDateChange -}: MainTitleProps) { - const { t } = useI18n() - const dispatch = useAppDispatch() - - const handleLogoClick = async () => { - if (!calendarRef.current) return - - await dispatch(setView('calendar')) - - if (currentView !== 'timeGridWeek') { - calendarRef.current.changeView('timeGridWeek') - if (onViewChange) { - onViewChange('timeGridWeek') - } - } - - calendarRef.current.today() - - if (onDateChange) { - const newDate = calendarRef.current.getDate() - onDateChange(newDate) - } + const sharedProps: SharedMenubarProps = { + calendarRef, + onRefresh, + currentDate, + onDateChange, + currentView, + onViewChange: handleViewChange, + isIframe, + onToggleSidebar, + dateLabel, + supportLink, + anchorEl, + onAppMenuOpen: handleAppMenuOpen, + onAppMenuClose: handleAppMenuClose, + userMenuAnchorEl, + onUserMenuOpen: handleUserMenuOpen, + onUserMenuClose: handleUserMenuClose, + onSettingsClick: handleSettingsClick, + onLogoutClick: handleLogoutClick, + onNavigate: handleNavigation, + user } - return ( -
- {t('menubar.logoAlt')} -
- ) -} - -function AppIcon({ prop }: { prop: AppIconProps }) { - return ( - - - - {prop.name} - - + return isTablet ? ( + + ) : ( + ) } diff --git a/src/components/Menubar/NavigationControls.tsx b/src/components/Menubar/NavigationControls.tsx new file mode 100644 index 0000000..f365d5d --- /dev/null +++ b/src/components/Menubar/NavigationControls.tsx @@ -0,0 +1,44 @@ +import { Button, ButtonGroup } from '@linagora/twake-mui' +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' +import { useI18n } from 'twake-i18n' + +export function NavigationControls({ + onNavigate +}: { + onNavigate: (action: 'today' | 'next' | 'prev') => void +}) { + const { t } = useI18n() + return ( +
+ + + + + +
+ ) +} diff --git a/src/components/Menubar/SelectView.tsx b/src/components/Menubar/SelectView.tsx new file mode 100644 index 0000000..7bb1fa9 --- /dev/null +++ b/src/components/Menubar/SelectView.tsx @@ -0,0 +1,42 @@ +import { FormControl, MenuItem, Select } from '@linagora/twake-mui' +import { useI18n } from 'twake-i18n' +import { CALENDAR_VIEWS } from '../Calendar/utils/constants' + +export function SelectView({ + currentView, + onViewChange +}: { + currentView: string + onViewChange: (view: string) => void +}) { + const { t } = useI18n() + return ( + + + + ) +} diff --git a/src/components/Menubar/TabletMenubar.tsx b/src/components/Menubar/TabletMenubar.tsx new file mode 100644 index 0000000..cd763b2 --- /dev/null +++ b/src/components/Menubar/TabletMenubar.tsx @@ -0,0 +1,124 @@ +import { IconButton, Stack } from '@linagora/twake-mui' +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' +import MenuIcon from '@mui/icons-material/Menu' +import RefreshIcon from '@mui/icons-material/Refresh' +import TodayIcon from '@mui/icons-material/Today' +import { useI18n } from 'twake-i18n' +import SearchBar from './EventSearchBar' +import { MainTitle } from './MainTitle' +import { SharedMenubarProps } from './Menubar' +import { UserMenu } from './UserMenu' + +export function TabletMenubar({ + calendarRef, + currentView, + isIframe, + dateLabel, + onUserMenuOpen, + onSettingsClick, + onLogoutClick, + onNavigate, + onRefresh, + onToggleSidebar, + user, + userMenuAnchorEl, + onUserMenuClose, + onViewChange, + onDateChange +}: SharedMenubarProps) { + const { t } = useI18n() + + return ( +
+
+ + + + + {!isIframe && ( +
+ +
+ )} + +
+
+ + onNavigate('prev')} + aria-label={t('menubar.prev')} + title={t('menubar.prev')} + > + + + onNavigate('today')} + aria-label={t('menubar.today')} + title={t('menubar.today')} + > + + + onNavigate('next')} + aria-label={t('menubar.next')} + title={t('menubar.next')} + > + + + +
+
+ +
+
+

{dateLabel}

+
+
+
+ +
+
+ +
+
+ + + +
+ +
+ +
+
+
+ ) +} diff --git a/src/components/Menubar/UserMenu.tsx b/src/components/Menubar/UserMenu.tsx new file mode 100644 index 0000000..b382f30 --- /dev/null +++ b/src/components/Menubar/UserMenu.tsx @@ -0,0 +1,134 @@ +import { userData } from '@/features/User/userDataTypes' +import { getInitials, stringToGradient } from '@/utils/avatarUtils' +import { getUserDisplayName } from '@/utils/userUtils' +import { + alpha, + Avatar, + Box, + Divider, + IconButton, + Menu, + MenuItem, + Typography, + useTheme +} from '@linagora/twake-mui' +import LogoutIcon from '@mui/icons-material/Logout' +import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined' +import { MouseEvent } from 'react' +import { useI18n } from 'twake-i18n' + +export type UserMenuProps = { + anchorEl: HTMLElement | null + onClose: () => void + onSettingsClick: () => void + onLogoutClick: () => void + onUserMenuOpen: (event: MouseEvent) => void + user: userData | null + isIframe?: boolean +} + +export function UserMenu({ + anchorEl, + onClose, + onSettingsClick, + onLogoutClick, + onUserMenuOpen, + user, + isIframe = false +}: UserMenuProps): JSX.Element { + const { t } = useI18n() + const theme = useTheme() + const displayName = getUserDisplayName(user) + + const open = Boolean(anchorEl) + + return ( + <> + + {!isIframe ? ( + + {getInitials(displayName)} + + ) : ( + + )} + + + + + + {getInitials(displayName)} + + + {displayName} + + + {user?.email} + + + + + + {t('menubar.settings') || 'Settings'} + + + + + + + {t('menubar.logout') || 'Logout'} + + + + ) +} diff --git a/src/features/Calendars/useCalendarLoader.ts b/src/features/Calendars/useCalendarLoader.ts index c562a49..855b998 100644 --- a/src/features/Calendars/useCalendarLoader.ts +++ b/src/features/Calendars/useCalendarLoader.ts @@ -1,4 +1,5 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks' +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import { getCalendarDetailAsync } from '@/features/Calendars/services' import { formatDateToYYYYMMDDTHHMMSS, @@ -78,7 +79,7 @@ export function useCalendarDataLoader({ const { visibleStart, visibleEnd, prefetchEnd } = useMemo(() => { const { start, end } = getViewRange(selectedDate, currentView) const prefetchEnd = - currentView === 'dayGridMonth' + currentView === CALENDAR_VIEWS.dayGridMonth ? end.getTime() : getAdjacentWeekRange(selectedDate).end.getTime() return { diff --git a/src/locales/en.json b/src/locales/en.json index 3b0e8f0..8bd0af7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -230,7 +230,10 @@ "duplicateEvent": "Duplicate event" }, "menubar": { + "toggleSidebar": "Toggle sidebar", "today": "Today", + "next": "Next", + "prev": "Previous", "refresh": "Refresh", "viewSelector": "Select view", "languageSelector": "Select language", @@ -378,5 +381,8 @@ "searchError": "Unable to fetch resources right now. Please try again.", "noResults": "No results", "loading": "Loading..." + }, + "sidebar": { + "displayMode": "Display mode" } } diff --git a/src/locales/fr.json b/src/locales/fr.json index 89bb347..359411b 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -232,6 +232,8 @@ }, "menubar": { "today": "Aujourd'hui", + "next": "Précédent", + "prev": "Suivant", "refresh": "Actualiser", "viewSelector": "Sélectionner la vue", "languageSelector": "Sélectionner la langue", @@ -245,7 +247,8 @@ "logoAlt": "Calendrier", "settings": "Paramètres", "help": "Aide", - "logout": "Déconnexion" + "logout": "Déconnexion", + "toggleSidebar": "Afficher/masquer la barre latérale" }, "settings": { "title": "Paramètres", @@ -379,5 +382,8 @@ "searchError": "Impossible de récupérer les ressources pour le moment. Veuillez réessayer.", "noResults": "Aucun résultat", "loading": "Chargement..." + }, + "sidebar": { + "displayMode": "Mode d'affichage" } } diff --git a/src/locales/ru.json b/src/locales/ru.json index 7d597fa..9bdf33e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -232,6 +232,8 @@ }, "menubar": { "today": "Сегодня", + "next": "Следующий", + "prev": "Предыдущий", "refresh": "Обновить", "viewSelector": "Выбор вида", "languageSelector": "Язык", @@ -245,7 +247,8 @@ "logoAlt": "Календарь", "settings": "Настройки", "help": "Помощь", - "logout": "Выйти" + "logout": "Выйти", + "toggleSidebar": "Показать/скрыть боковую панель" }, "settings": { "title": "Настройки", @@ -379,5 +382,8 @@ "searchError": "Не удалось получить ресурсы. Попробуйте снова.", "noResults": "Нет результатов", "loading": "Загрузка..." + }, + "sidebar": { + "displayMode": "Режим отображения" } } diff --git a/src/locales/vi.json b/src/locales/vi.json index b6a81dc..5ca45e9 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -230,6 +230,8 @@ }, "menubar": { "today": "Hôm nay", + "next": "Tiếp theo", + "prev": "Trước đó", "refresh": "Làm mới", "viewSelector": "Chọn chế độ xem", "languageSelector": "Chọn ngôn ngữ", @@ -243,7 +245,8 @@ "logoAlt": "Lịch", "settings": "Cài đặt", "help": "Giúp đỡ", - "logout": "Đăng xuất" + "logout": "Đăng xuất", + "toggleSidebar": "Ẩn/hiện thanh bên" }, "settings": { "title": "Cài đặt", @@ -377,5 +380,8 @@ "searchError": "Không thể lấy tài nguyên. Vui lòng thử lại.", "noResults": "Không có kết quả", "loading": "Đang tải..." + }, + "sidebar": { + "displayMode": "Chế độ hiển thị" } } diff --git a/src/setupTests.ts b/src/setupTests.ts index 3f2bbc5..77067b4 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -62,6 +62,9 @@ if (typeof window !== 'undefined') { window.WS_PING_PERIOD_MS = 5000 window.WS_PING_TIMEOUT_PERIOD_MS = 5000 } +jest.mock('@/useScreenSizeDetection', () => ({ + useScreenSizeDetection: jest.fn(() => ({ isMobile: false, isTablet: false })) +})) // Suppress jsdom CSS selector parsing errors for Emotion/MUI if (typeof window !== 'undefined' && window.getComputedStyle) { const originalGetComputedStyle = window.getComputedStyle diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index 8690426..6ab106e 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -1,3 +1,4 @@ +import { CALENDAR_VIEWS } from '@/components/Calendar/utils/constants' import moment from 'moment' export function formatDateToYYYYMMDDTHHMMSS(date: Date) { @@ -69,7 +70,7 @@ export function getViewRange( date = new Date(), view: string ): { start: Date; end: Date } { - if (view === 'dayGridMonth') { + if (view === CALENDAR_VIEWS.dayGridMonth) { return getCalendarRange(date) } return computeWeekRange(date)