From 078e9f9676ea63b82b2fae04887d9161163fb959 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:47:13 +0200 Subject: [PATCH] feat: show tasks with due dates in the calendar month view #1107 --- .../calendar-month-view-tasks.test.tsx | 155 ++++++++++++++++++ components/calendar/calendar-app.tsx | 85 ++++++++-- components/calendar/calendar-day-view.tsx | 26 ++- components/calendar/calendar-month-view.tsx | 77 ++++++++- components/calendar/calendar-week-view.tsx | 75 +++------ components/calendar/task-chip.tsx | 61 +++++++ lib/__tests__/calendar-tasks.test.ts | 86 ++++++++++ lib/calendar-tasks.ts | 48 ++++++ locales/ar/common.json | 2 +- locales/ca/common.json | 2 +- locales/cs/common.json | 2 +- locales/da/common.json | 2 +- locales/de/common.json | 2 +- locales/en/common.json | 2 +- locales/es/common.json | 2 +- locales/fa/common.json | 2 +- locales/fr/common.json | 2 +- locales/he/common.json | 2 +- locales/hu/common.json | 2 +- locales/it/common.json | 2 +- locales/ja/common.json | 2 +- locales/ko/common.json | 2 +- locales/lv/common.json | 2 +- locales/mn/common.json | 2 +- locales/nb/common.json | 2 +- locales/nl/common.json | 2 +- locales/pl/common.json | 2 +- locales/pt/common.json | 2 +- locales/ro/common.json | 2 +- locales/ru/common.json | 2 +- locales/sk/common.json | 2 +- locales/tr/common.json | 2 +- locales/uk/common.json | 2 +- locales/zh-TW/common.json | 2 +- locales/zh/common.json | 2 +- 35 files changed, 551 insertions(+), 116 deletions(-) create mode 100644 components/calendar/__tests__/calendar-month-view-tasks.test.tsx create mode 100644 components/calendar/task-chip.tsx create mode 100644 lib/__tests__/calendar-tasks.test.ts create mode 100644 lib/calendar-tasks.ts diff --git a/components/calendar/__tests__/calendar-month-view-tasks.test.tsx b/components/calendar/__tests__/calendar-month-view-tasks.test.tsx new file mode 100644 index 000000000..7575c638f --- /dev/null +++ b/components/calendar/__tests__/calendar-month-view-tasks.test.tsx @@ -0,0 +1,155 @@ +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { CalendarMonthView } from '../calendar-month-view'; +import type { CalendarEvent, Calendar, CalendarTask } from '@/lib/jmap/types'; + +// #1107: with "Show tasks on calendar" on, the month view shows each task +// on its due day, below that day's events, and lets it be completed or +// opened in the task editor. + +const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[]; + +function makeTask(id: string, due: string, overrides: Partial = {}): CalendarTask { + return { + id, + '@type': 'Task', + uid: id, + title: 'Task ' + id, + description: '', + due, + start: null, + duration: null, + timeZone: null, + showWithoutTime: true, + progress: 'needs-action', + priority: 0, + privacy: 'public', + keywords: null, + categories: null, + color: null, + created: null, + updated: '2026-09-01T00:00:00Z', + recurrenceRules: null, + alerts: null, + relatedTo: null, + calendarIds: { 'cal-1': true }, + ...overrides, + }; +} + +function makeAllDayEvent(id: string, start: string): CalendarEvent { + return { + id, + '@type': 'Event', + uid: id, + title: 'Event ' + id, + start, + duration: 'P1D', + showWithoutTime: true, + calendarIds: { 'cal-1': true }, + } as unknown as CalendarEvent; +} + +type Props = React.ComponentProps; + +function renderView(overrides: Partial = {}) { + const props: Props = { + focus: { date: new Date(2026, 8, 9), nonce: 0 }, + windowKey: 'month:2026-09-09', + // Monday 31 Aug to Sunday 4 Oct: the September grid. + rangeStart: new Date(2026, 7, 31), + rangeEnd: new Date(2026, 9, 4), + selectedDate: new Date(2026, 8, 9), + events: [], + calendars, + onSelectDate: vi.fn(), + onSelectEvent: vi.fn(), + ...overrides, + }; + return { props, ...render() }; +} + +function chipSlot(taskId: string): HTMLElement { + const chip = document.querySelector(`[data-calendar-task="${taskId}"]`); + if (!chip?.parentElement) throw new Error(`no chip for ${taskId}`); + return chip.parentElement; +} + +describe('CalendarMonthView tasks', () => { + afterEach(() => { + cleanup(); + }); + + it('shows a task on its due day, below that day\'s events', () => { + renderView({ + events: [makeAllDayEvent('e1', '2026-09-10')], + tasks: [ + makeTask('thu', '2026-09-10'), + makeTask('fri', '2026-09-11T09:00:00', { showWithoutTime: false }), + ], + }); + + expect(screen.getByText('Task thu')).toBeInTheDocument(); + expect(screen.getByText('Task fri')).toBeInTheDocument(); + + // Thursday is the fourth column of the week starting Monday 7 Sep and + // already has an event in row 0; Friday has no events. + // (jsdom rounds the calc() percentage.) + const thu = chipSlot('thu'); + expect(thu.style.left).toMatch(/^calc\(42\.857\d*% \+ 1px\)$/); + expect(thu.style.top).toBe('22px'); + const fri = chipSlot('fri'); + expect(fri.style.left).toMatch(/^calc\(57\.142\d*% \+ 1px\)$/); + expect(fri.style.top).toBe('0px'); + }); + + it('grows the week row to fit a crowded day', () => { + renderView({ + events: [makeAllDayEvent('e1', '2026-09-10')], + tasks: ['a', 'b', 'c'].map((id) => makeTask(id, '2026-09-10')), + }); + const row = document.querySelector('[data-week="2026-09-07"]'); + // overlay offset 30 + 4, one event row and three task rows of 22px, 8 below. + expect(row?.style.minHeight).toBe(`${30 + 4 + 4 * 22 + 8}px`); + expect(chipSlot('c').style.top).toBe(`${3 * 22}px`); + }); + + it('toggles completion from the circle and opens the editor from the title', () => { + const onToggleTaskComplete = vi.fn(); + const onSelectTask = vi.fn(); + const task = makeTask('t1', '2026-09-10'); + const { props } = renderView({ tasks: [task], onToggleTaskComplete, onSelectTask }); + + fireEvent.click(screen.getByRole('button', { name: 'tasks.mark_complete' })); + expect(onToggleTaskComplete).toHaveBeenCalledWith(task); + expect(onSelectTask).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Task t1' })); + expect(onSelectTask).toHaveBeenCalledWith(task); + expect(onToggleTaskComplete).toHaveBeenCalledTimes(1); + expect(props.onSelectDate).not.toHaveBeenCalled(); + }); + + it('marks completed tasks and offers to reopen them', () => { + renderView({ tasks: [makeTask('done', '2026-09-10', { progress: 'completed' })] }); + expect(screen.getByRole('button', { name: 'tasks.mark_incomplete' })).toBeInTheDocument(); + expect(screen.getByText('Task done')).toHaveClass('line-through'); + }); + + it('shows no task chips when no tasks are passed', () => { + renderView({ events: [makeAllDayEvent('e1', '2026-09-10')] }); + expect(document.querySelector('[data-calendar-task]')).toBeNull(); + }); + + it('shows tasks as rings next to the event dots on mobile', () => { + renderView({ + isMobile: true, + events: [makeAllDayEvent('e1', '2026-09-10')], + tasks: [makeTask('t1', '2026-09-10')], + }); + const ring = document.querySelector('[data-calendar-task="t1"]'); + expect(ring?.tagName).toBe('SPAN'); + expect(ring).toHaveClass('border'); + expect(screen.queryByText('Task t1')).toBeNull(); + }); +}); diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index ab2085588..c7b21de67 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -47,10 +47,11 @@ import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; import { useSidebarApps } from "@/hooks/use-sidebar-apps"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; -import { useIsFocusedProTab } from "@/hooks/use-pane-context"; +import { useIsFocusedProTab, useIsPaneScoped } from "@/hooks/use-pane-context"; import { useProMultiAccountCalendars } from "@/hooks/use-pro-multi-account-calendars"; import { ResizeHandle } from "@/components/layout/resize-handle"; import { sanitizeOutgoingCalendarEventData } from "@/lib/calendar-event-normalization"; +import { filterTasksByCalendars } from "@/lib/calendar-tasks"; import { baseEventStoreId, buildFallbackExcludePatch, @@ -66,7 +67,7 @@ import { displayNow } from "@/lib/timezone"; import { useTaskStore } from "@/stores/task-store"; import { useContactStore } from "@/stores/contact-store"; import { cn } from "@/lib/utils"; -import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights } from "@/lib/jmap/types"; +import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights, CalendarTask } from "@/lib/jmap/types"; import { ShareCollectionDialog } from "@/components/settings/share-collection-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; @@ -138,6 +139,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const removeSharedCalendarColor = useSettingsStore((s) => s.removeSharedCalendarColor); const taskStore = useTaskStore(); const fetchTasksFn = useTaskStore(state => state.fetchTasks); + const toggleTaskCompleteFn = useTaskStore(state => state.toggleTaskComplete); const { identities } = useIdentityStore(); const contacts = useContactStore((s) => s.contacts); const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month"; @@ -576,24 +578,45 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = setDefaultModalEndDate(endDate); setDefaultModalAllDay(allDay ?? false); setSelectedDate(d); + setShowTaskModal(false); + setEditTask(null); setShowEventModal(true); }, [selectedDate, setSelectedDate]); const openEditModal = useCallback((event: CalendarEvent) => { setEditEvent(event); setDefaultModalDate(undefined); + setShowTaskModal(false); + setEditTask(null); setShowEventModal(true); }, []); - const openCreateTaskModal = useCallback(() => { - setEditTask(null); - setShowTaskModal(true); + // The event and task panels share the space beside the grid; now that + // tasks open from the month, week and day views (#1107), opening one + // closes the other. + const closeEventPanelForTask = useCallback(() => { + setShowEventModal(false); + setEditEvent(null); + setPendingPreview(null); + setDefaultCalendarIdForCreate(undefined); + setDefaultModalAllDay(false); }, []); - const openEditTaskModal = useCallback((task: import("@/lib/jmap/types").CalendarTask) => { + const openCreateTaskModal = useCallback(() => { + closeEventPanelForTask(); + setEditTask(null); + setShowTaskModal(true); + }, [closeEventPanelForTask]); + + const openEditTaskModal = useCallback((task: CalendarTask) => { + closeEventPanelForTask(); setEditTask(task); setShowTaskModal(true); - }, []); + }, [closeEventPanelForTask]); + + const handleToggleTaskComplete = useCallback((task: CalendarTask) => { + if (client) toggleTaskCompleteFn(client, task); + }, [client, toggleTaskCompleteFn]); const handleSaveTask = useCallback(async (data: Partial) => { if (!client) return; @@ -746,6 +769,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = // that renders while Pro takes over a route stays silent. const proInterfaceActive = useProInterfaceActive(); const isFocusedProTab = useIsFocusedProTab(); + const isPaneScoped = useIsPaneScoped(); const eventLinkId = showEventModal && editEvent ? editEvent.id : null; const calendarLinkPath = appPath(buildCalendarPath({ view: normalizedViewMode, @@ -1353,6 +1377,13 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = return filtered; }, [events, selectedCalendarIds, showBirthdayCalendar, birthdayEvents]); + // Tasks on the month, week and day grids follow the calendar filter like + // the events do (#1107). + const calendarTasks = useMemo(() => { + if (!enableCalendarTasks || !showTasksOnCalendar) return undefined; + return filterTasksByCalendars(taskStore.tasks, selectedCalendarIds); + }, [enableCalendarTasks, showTasksOnCalendar, taskStore.tasks, selectedCalendarIds]); + useEffect(() => { const hiddenEvents = events.filter((event) => { if (!event.start || !event.calendarIds) { @@ -1460,6 +1491,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = firstDayOfWeek={firstDayOfWeek} isMobile={isMobile} pendingPreview={pendingPreview} + tasks={calendarTasks} + onToggleTaskComplete={handleToggleTaskComplete} + onSelectTask={openEditTaskModal} /> ); case "week": @@ -1480,8 +1514,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = timeFormat={timeFormat} isMobile={isMobile} pendingPreview={pendingPreview} - tasks={enableCalendarTasks && showTasksOnCalendar ? taskStore.tasks : undefined} - onToggleTaskComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }} + tasks={calendarTasks} + onToggleTaskComplete={handleToggleTaskComplete} + onSelectTask={openEditTaskModal} /> ); case "day": @@ -1500,8 +1535,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = timeFormat={timeFormat} isMobile={isMobile} pendingPreview={pendingPreview} - tasks={enableCalendarTasks && showTasksOnCalendar ? taskStore.tasks : undefined} - onToggleTaskComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }} + tasks={calendarTasks} + onToggleTaskComplete={handleToggleTaskComplete} + onSelectTask={openEditTaskModal} /> ); case "agenda": @@ -1534,7 +1570,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = filter={taskStore.filter} showCompleted={taskStore.showCompleted} onSelectTask={openEditTaskModal} - onToggleComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }} + onToggleComplete={handleToggleTaskComplete} selectedTaskId={taskStore.selectedTaskId} onQuickCreate={(title) => { if (client) { @@ -1851,10 +1887,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = d.setHours(0, 0, 0, 0); openCreateModal(d, undefined, true); }} - onNewTask={enableCalendarTasks ? () => { - setEditTask(null); - setShowTaskModal(true); - } : undefined} + onNewTask={enableCalendarTasks ? openCreateTaskModal : undefined} onGoToToday={goToToday} /> ); @@ -1900,6 +1933,26 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = /> )} + {/* Mobile task editor: full screen, like the event editor. */} + {showTaskModal && isMobile && ( +
+ { setShowTaskModal(false); setEditTask(null); }} + isMobile={true} + /> +
+ )} + {showImportModal && client && ( void; + onSelectTask?: (task: CalendarTask) => void; } const HOUR_HEIGHT = 64; @@ -60,6 +62,7 @@ export function CalendarDayView({ pendingPreview, tasks, onToggleTaskComplete, + onSelectTask, }: CalendarDayViewProps) { const t = useTranslations("calendar"); // Grid days / event dates are display dates (local fields = wall-clock in @@ -142,20 +145,7 @@ export function CalendarDayView({ return map; }, [events]); - const tasksByDay = useMemo(() => { - const map = new Map(); - if (!tasks?.length) return map; - for (const task of tasks) { - if (!task.due) continue; - try { - const key = format(parseISO(task.due), "yyyy-MM-dd"); - const existing = map.get(key) || []; - existing.push(task); - map.set(key, existing); - } catch { /* skip */ } - } - return map; - }, [tasks]); + const tasksByDay = useMemo(() => groupTasksByDueDay(tasks), [tasks]); // Column layouts are the costly part of a render; with months of columns // they must not be redone on every scroll-driven re-render. @@ -355,10 +345,13 @@ export function CalendarDayView({ return (
onSelectTask?.(task)} className="flex items-center gap-1.5 px-1.5 py-0.5 rounded text-xs cursor-pointer hover:bg-muted/50 transition-colors" - style={{ borderLeft: `3px solid ${color}` }} + style={{ borderInlineStart: `3px solid ${color}` }} > diff --git a/components/calendar/calendar-month-view.tsx b/components/calendar/calendar-month-view.tsx index ea69cd528..ea285f34c 100644 --- a/components/calendar/calendar-month-view.tsx +++ b/components/calendar/calendar-month-view.tsx @@ -5,8 +5,10 @@ import { useTranslations } from "next-intl"; import { format, parseISO, eachDayOfInterval, addDays } from "date-fns"; import { cn } from "@/lib/utils"; import { EventCard } from "./event-card"; +import { CalendarTaskChip } from "./task-chip"; import { buildWeekSegments, getEventDayBounds, getPrimaryCalendarId } from "@/lib/calendar-utils"; -import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; +import { groupTasksByDueDay } from "@/lib/calendar-tasks"; +import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types"; import { useAuthStore } from "@/stores/auth-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { useSettingsStore } from "@/stores/settings-store"; @@ -30,6 +32,9 @@ interface CalendarMonthViewProps extends ScrollWindowViewProps { firstDayOfWeek?: number; isMobile?: boolean; pendingPreview?: PendingEventPreview | null; + tasks?: CalendarTask[]; + onToggleTaskComplete?: (task: CalendarTask) => void; + onSelectTask?: (task: CalendarTask) => void; } /** Fraction of the viewport height at which the "current month" is sampled. */ @@ -56,6 +61,9 @@ export function CalendarMonthView({ onCreateAtTime, isMobile, pendingPreview, + tasks, + onToggleTaskComplete, + onSelectTask, }: CalendarMonthViewProps) { const t = useTranslations("calendar"); const showTimeInMonthView = useSettingsStore((state) => state.showTimeInMonthView); @@ -115,13 +123,28 @@ export function CalendarMonthView({ return map; }, [events]); + const tasksByDate = useMemo(() => groupTasksByDueDay(tasks), [tasks]); + const weekSegments = useMemo(() => { return weeks.map((week) => { const segments = buildWeekSegments(events, week); const rowCount = segments.reduce((maxRows, segment) => Math.max(maxRows, segment.row + 1), 0); - return { week, segments, rowCount }; + // Tasks stack under the lowest event of their own day rather than of + // the whole week, so a busy Monday does not push Friday's tasks down. + const dayEventRows = week.map((_, dayIndex) => segments.reduce( + (rows, segment) => dayIndex >= segment.startIndex && dayIndex < segment.startIndex + segment.span + ? Math.max(rows, segment.row + 1) + : rows, + 0, + )); + const dayTasks = week.map((day) => tasksByDate.get(format(day, "yyyy-MM-dd")) ?? []); + const contentRows = dayTasks.reduce( + (rows, dayTaskList, dayIndex) => Math.max(rows, dayEventRows[dayIndex] + dayTaskList.length), + rowCount, + ); + return { week, segments, rowCount, dayEventRows, dayTasks, contentRows }; }); - }, [events, weeks]); + }, [events, weeks, tasksByDate]); const scrollRef = useRef(null); const topSentinelRef = useRef(null); @@ -258,10 +281,10 @@ export function CalendarMonthView({ onScroll={handleScroll} >
- {weekSegments.map(({ week, segments, rowCount }) => ( + {weekSegments.map(({ week, segments, rowCount, dayEventRows, dayTasks, contentRows }) => (
@@ -271,6 +294,7 @@ export function CalendarMonthView({ const today = checkIsToday(day); const key = format(day, "yyyy-MM-dd"); const dayEvents = eventsByDate.get(key) || []; + const dayTaskList = dayTasks[dayIndex]; const fullDateLabel = formatFullDate(day); const previous = dayIndex > 0 ? week[dayIndex - 1] : addDays(day, -1); const firstOfMonth = !checkIsSameMonth(day, previous); @@ -328,7 +352,19 @@ export function CalendarMonthView({ /> ); })} - {dayEvents.length > 3 && ( + {/* Tasks are rings where events are filled dots. */} + {dayTaskList.slice(0, Math.max(0, 3 - dayEvents.length)).map((task) => { + const calId = Object.keys(task.calendarIds).find((id) => calendarMap.has(id)); + return ( + + ); + })} + {dayEvents.length + dayTaskList.length > 3 && ( )} {pendingPreview && checkIsSameDay(pendingPreview.start, day) && ( @@ -347,7 +383,7 @@ export function CalendarMonthView({ {showChips && pendingPreview && (() => { const previewDayIdx = week.findIndex(d => checkIsSameDay(d, pendingPreview.start)); if (previewDayIdx === -1) return null; - const previewRow = rowCount; + const previewRow = Math.max(rowCount, dayEventRows[previewDayIdx] + dayTasks[previewDayIdx].length); const cal = calendarMap.get(pendingPreview.calendarId); const color = cal?.color || "#3b82f6"; return ( @@ -408,6 +444,33 @@ export function CalendarMonthView({ })}
)} + + {showChips && dayTasks.some((dayTaskList) => dayTaskList.length > 0) && ( +
+ {dayTasks.map((dayTaskList, dayIndex) => dayTaskList.map((task, taskIndex) => { + const calId = Object.keys(task.calendarIds).find((id) => calendarMap.has(id)); + return ( +
+ +
+ ); + }))} +
+ )}
))}
diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx index 522643d0d..093f65e4d 100644 --- a/components/calendar/calendar-week-view.tsx +++ b/components/calendar/calendar-week-view.tsx @@ -4,12 +4,13 @@ import { useMemo, useEffect, useLayoutEffect, useRef, useState, useCallback } fr import { useTranslations } from "next-intl"; import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter"; import { - startOfWeek, format, isSameDay, parseISO, eachDayOfInterval, differenceInCalendarDays, + startOfWeek, format, isSameDay, eachDayOfInterval, differenceInCalendarDays, } from "date-fns"; import { cn } from "@/lib/utils"; -import { Check } from "@/components/icons"; import { EventCard } from "./event-card"; import { QuickEventInput } from "./quick-event-input"; +import { CalendarTaskChip } from "./task-chip"; +import { groupTasksByDueDay } from "@/lib/calendar-tasks"; import { buildTimedFullDayWeekSegments, buildWeekSegmentsRaw, formatSnapTime, getEventDayBounds, getPrimaryCalendarId, isTimedEventFullDayOnDate, layoutOverlappingEvents, packWeekSegments } from "@/lib/calendar-utils"; import { displayNow, isDisplayToday } from "@/lib/timezone"; import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types"; @@ -35,6 +36,7 @@ interface CalendarWeekViewProps extends ScrollWindowViewProps { pendingPreview?: PendingEventPreview | null; tasks?: CalendarTask[]; onToggleTaskComplete?: (task: CalendarTask) => void; + onSelectTask?: (task: CalendarTask) => void; } const HOUR_HEIGHT = 60; @@ -67,6 +69,7 @@ export function CalendarWeekView({ pendingPreview, tasks, onToggleTaskComplete, + onSelectTask, }: CalendarWeekViewProps) { const t = useTranslations("calendar"); // Grid days / event dates are display dates (local fields = wall-clock in @@ -175,21 +178,7 @@ export function CalendarWeekView({ return allDaySegments.reduce((maxRows, segment) => Math.max(maxRows, segment.row + 1), 0); }, [allDaySegments]); - // Tasks grouped by day - const tasksByDay = useMemo(() => { - if (!tasks?.length) return new Map(); - const map = new Map(); - for (const task of tasks) { - if (!task.due) continue; - try { - const key = format(parseISO(task.due), "yyyy-MM-dd"); - const existing = map.get(key) || []; - existing.push(task); - map.set(key, existing); - } catch { /* skip */ } - } - return map; - }, [tasks]); + const tasksByDay = useMemo(() => groupTasksByDueDay(tasks), [tasks]); // Max tasks on any single loaded day const taskRowCount = useMemo(() => { @@ -384,39 +373,25 @@ export function CalendarWeekView({ {days.map((day, dayIndex) => { const key = format(day, "yyyy-MM-dd"); const dayTasks = tasksByDay.get(key) || []; - return dayTasks.map((task, taskIndex) => { - const isCompleted = task.progress === "completed"; - const cal = calendars.find(c => task.calendarIds[c.id]); - const color = cal?.color || "#3b82f6"; - return ( -
-
onToggleTaskComplete?.(task)} - > - - {isCompleted && } - - - {task.title} - -
-
- ); - }); + return dayTasks.map((task, taskIndex) => ( +
+ task.calendarIds[c.id])} + onToggleComplete={onToggleTaskComplete} + onSelect={onSelectTask} + /> +
+ )); })}
)} diff --git a/components/calendar/task-chip.tsx b/components/calendar/task-chip.tsx new file mode 100644 index 000000000..ccea5a78d --- /dev/null +++ b/components/calendar/task-chip.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { cn } from "@/lib/utils"; +import { Check } from "@/components/icons"; +import type { Calendar, CalendarTask } from "@/lib/jmap/types"; + +interface CalendarTaskChipProps { + task: CalendarTask; + calendar?: Calendar; + onToggleComplete?: (task: CalendarTask) => void; + onSelect?: (task: CalendarTask) => void; + className?: string; +} + +/** + * A task on its due day in the month and week grids (#1107). The circle + * toggles completion; the title opens the task in the editor. + */ +export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, className }: CalendarTaskChipProps) { + const t = useTranslations("calendar"); + const isCompleted = task.progress === "completed"; + const color = calendar?.color || "#3b82f6"; + const title = task.title || t("tasks.no_title"); + + return ( +
+ + +
+ ); +} diff --git a/lib/__tests__/calendar-tasks.test.ts b/lib/__tests__/calendar-tasks.test.ts new file mode 100644 index 000000000..384710db4 --- /dev/null +++ b/lib/__tests__/calendar-tasks.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from 'vitest'; +import { filterTasksByCalendars, groupTasksByDueDay, taskDueDayKey } from '../calendar-tasks'; +import type { CalendarTask } from '@/lib/jmap/types'; + +// #1107: the month, week and day views place tasks on their due day and +// hide tasks from calendars switched off in the sidebar. + +function makeTask(id: string, overrides: Partial = {}): CalendarTask { + return { + id, + '@type': 'Task', + uid: id, + title: 'Task ' + id, + description: '', + due: '2026-09-10', + start: null, + duration: null, + timeZone: null, + showWithoutTime: true, + progress: 'needs-action', + priority: 0, + privacy: 'public', + keywords: null, + categories: null, + color: null, + created: null, + updated: '2026-09-01T00:00:00Z', + recurrenceRules: null, + alerts: null, + relatedTo: null, + calendarIds: { 'cal-1': true }, + ...overrides, + }; +} + +describe('filterTasksByCalendars', () => { + it('keeps tasks in at least one visible calendar', () => { + const tasks = [ + makeTask('a', { calendarIds: { 'cal-1': true } }), + makeTask('b', { calendarIds: { 'cal-2': true } }), + makeTask('c', { calendarIds: { 'cal-2': true, 'cal-3': true } }), + ]; + expect(filterTasksByCalendars(tasks, ['cal-1', 'cal-3']).map((t) => t.id)).toEqual(['a', 'c']); + expect(filterTasksByCalendars(tasks, [])).toEqual([]); + }); +}); + +describe('taskDueDayKey', () => { + it('is the local day of the due date, with or without a time', () => { + expect(taskDueDayKey(makeTask('a', { due: '2026-09-10' }))).toBe('2026-09-10'); + expect(taskDueDayKey(makeTask('a', { due: '2026-09-10T23:30:00', showWithoutTime: false }))).toBe('2026-09-10'); + }); + + it('is null for tasks without a usable due date', () => { + expect(taskDueDayKey(makeTask('a', { due: null }))).toBeNull(); + expect(taskDueDayKey(makeTask('a', { due: 'not a date' }))).toBeNull(); + }); +}); + +describe('groupTasksByDueDay', () => { + it('groups by due day and skips tasks without one', () => { + const map = groupTasksByDueDay([ + makeTask('a', { due: '2026-09-10' }), + makeTask('b', { due: '2026-09-11' }), + makeTask('c', { due: null }), + makeTask('d', { due: '2026-09-10T09:00:00', showWithoutTime: false }), + ]); + expect([...map.keys()].sort()).toEqual(['2026-09-10', '2026-09-11']); + expect(map.get('2026-09-10')?.map((t) => t.id).sort()).toEqual(['a', 'd']); + }); + + it('orders a day by untimed first, then due time, then title', () => { + const map = groupTasksByDueDay([ + makeTask('late', { due: '2026-09-10T17:00:00', showWithoutTime: false, title: 'A' }), + makeTask('early', { due: '2026-09-10T08:00:00', showWithoutTime: false, title: 'Z' }), + makeTask('allday-b', { due: '2026-09-10', title: 'B' }), + makeTask('allday-a', { due: '2026-09-10', title: 'A' }), + ]); + expect(map.get('2026-09-10')?.map((t) => t.id)).toEqual(['allday-a', 'allday-b', 'early', 'late']); + }); + + it('is empty for no tasks', () => { + expect(groupTasksByDueDay(undefined).size).toBe(0); + expect(groupTasksByDueDay([]).size).toBe(0); + }); +}); diff --git a/lib/calendar-tasks.ts b/lib/calendar-tasks.ts new file mode 100644 index 000000000..ab81cbf39 --- /dev/null +++ b/lib/calendar-tasks.ts @@ -0,0 +1,48 @@ +import { format, parseISO } from "date-fns"; +import type { CalendarTask } from "@/lib/jmap/types"; + +/** + * Tasks shown on the calendar grid (#1107): only tasks with a due date, and + * only those in a calendar the sidebar currently shows, the same rule the + * events follow. + */ +export function filterTasksByCalendars(tasks: CalendarTask[], selectedCalendarIds: string[]): CalendarTask[] { + const selected = new Set(selectedCalendarIds); + return tasks.filter((task) => Object.keys(task.calendarIds ?? {}).some((id) => selected.has(id))); +} + +/** yyyy-MM-dd of the day a task is due, or null when it has no valid due date. */ +export function taskDueDayKey(task: CalendarTask): string | null { + if (!task.due) return null; + const due = parseISO(task.due); + if (Number.isNaN(due.getTime())) return null; + return format(due, "yyyy-MM-dd"); +} + +/** + * Tasks keyed by the day they are due. Within a day, tasks without a time + * come first, then by due time and title, so the order does not depend on + * the order the server returned them in. + */ +export function groupTasksByDueDay(tasks: CalendarTask[] | undefined): Map { + const map = new Map(); + if (!tasks?.length) return map; + for (const task of tasks) { + const key = taskDueDayKey(task); + if (!key) continue; + const existing = map.get(key); + if (existing) existing.push(task); + else map.set(key, [task]); + } + for (const dayTasks of map.values()) { + dayTasks.sort((a, b) => { + const aTimed = a.showWithoutTime ? 0 : 1; + const bTimed = b.showWithoutTime ? 0 : 1; + if (aTimed !== bTimed) return aTimed - bTimed; + const byDue = (a.due ?? "").localeCompare(b.due ?? ""); + if (byDue !== 0) return byDue; + return (a.title ?? "").localeCompare(b.title ?? ""); + }); + } + return map; +} diff --git a/locales/ar/common.json b/locales/ar/common.json index 7ef9da399..b2e3eb238 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "تفعيل المهام", "enable_tasks_desc": "إظهار عرض للمهام في التقويم لإدارة قوائم المهام", "show_tasks_on_calendar": "إظهار المهام في التقويم", - "show_tasks_on_calendar_desc": "عرض شارات المهام في عروض التقويم اليومي والأسبوعي", + "show_tasks_on_calendar_desc": "عرض المهام في عروض التقويم الشهري والأسبوعي واليومي", "hover_preview": "معاينة عند التمرير فوق الحدث", "hover_preview_desc": "إظهار نافذة تفاصيل منبثقة عند التمرير فوق أحداث التقويم", "hover_preview_instant": "فوري", diff --git a/locales/ca/common.json b/locales/ca/common.json index d64418abe..32fb68c2e 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Activa les tasques", "enable_tasks_desc": "Mostra una vista de tasques al calendari per gestionar pendents", "show_tasks_on_calendar": "Mostra les tasques al calendari", - "show_tasks_on_calendar_desc": "Mostra etiquetes de tasca a les vistes de dia i setmana del calendari", + "show_tasks_on_calendar_desc": "Mostra les tasques a les vistes de mes, setmana i dia del calendari", "hover_preview": "Previsualització en passar el cursor", "hover_preview_desc": "Mostra una finestra emergent de detalls en passar el cursor per sobre dels esdeveniments", "hover_preview_instant": "Instantània", diff --git a/locales/cs/common.json b/locales/cs/common.json index 6c378f9b1..f811fcf9d 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Povolit úkoly", "enable_tasks_desc": "Zobrazovat zobrazení úkolů v kalendáři pro správu úkolů", "show_tasks_on_calendar": "Zobrazit úkoly v kalendáři", - "show_tasks_on_calendar_desc": "Zobrazovat značky úkolů v denním a týdenním zobrazení kalendáře", + "show_tasks_on_calendar_desc": "Zobrazovat úkoly v měsíčním, týdenním a denním zobrazení kalendáře", "hover_preview": "Náhled události po přejetí", "hover_preview_desc": "Zobrazovat vyskakovací okno s podrobnostmi při najetí myší na události v kalendáři", "hover_preview_instant": "Okamžitě", diff --git a/locales/da/common.json b/locales/da/common.json index 884dd2e34..e419a74a9 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Aktivér opgaver", "enable_tasks_desc": "Vis en opgavevisning i kalenderen til styring af gøremål", "show_tasks_on_calendar": "Vis opgaver på kalender", - "show_tasks_on_calendar_desc": "Vis opgave-chips i dags- og ugekalendervisningerne", + "show_tasks_on_calendar_desc": "Vis opgaver i måneds-, uge- og dagsvisningen", "hover_preview": "Hover-forhåndsvisning", "hover_preview_desc": "Vis en detalje-popover når du holder musen over kalenderbegivenheder", "hover_preview_instant": "Øjeblikkeligt", diff --git a/locales/de/common.json b/locales/de/common.json index 8bc9631d3..6d19bb964 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Aufgaben aktivieren", "enable_tasks_desc": "Eine Aufgabenansicht im Kalender zum Verwalten von To-dos anzeigen", "show_tasks_on_calendar": "Aufgaben im Kalender anzeigen", - "show_tasks_on_calendar_desc": "Aufgabenmarkierungen in der Tages- und Wochenansicht anzeigen", + "show_tasks_on_calendar_desc": "Aufgaben in der Monats-, Wochen- und Tagesansicht anzeigen", "hover_preview": "Ereignis-Hover-Vorschau", "hover_preview_desc": "Ein Detail-Popover anzeigen, wenn über Kalender-Ereignisse gehovert wird", "hover_preview_instant": "Sofort", diff --git a/locales/en/common.json b/locales/en/common.json index c558f5d1b..228f259c8 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Enable tasks", "enable_tasks_desc": "Show a tasks view in the calendar for managing to-dos", "show_tasks_on_calendar": "Show tasks on calendar", - "show_tasks_on_calendar_desc": "Display task chips on the day and week calendar views", + "show_tasks_on_calendar_desc": "Display tasks on the month, week and day calendar views", "hover_preview": "Event hover preview", "hover_preview_desc": "Show a detail popover when hovering over calendar events", "hover_preview_instant": "Instant", diff --git a/locales/es/common.json b/locales/es/common.json index 781b93725..a36d8db47 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Activar tareas", "enable_tasks_desc": "Mostrar una vista de tareas en el calendario para gestionar tareas pendientes", "show_tasks_on_calendar": "Mostrar tareas en el calendario", - "show_tasks_on_calendar_desc": "Mostrar etiquetas de tareas en las vistas diarias y semanales del calendario", + "show_tasks_on_calendar_desc": "Mostrar las tareas en las vistas mensual, semanal y diaria del calendario", "hover_preview": "Vista previa al pasar el ratón", "hover_preview_desc": "Mostrar un popover de detalles al pasar el ratón sobre eventos del calendario", "hover_preview_instant": "Instantáneo", diff --git a/locales/fa/common.json b/locales/fa/common.json index 057657d10..f43e9e98e 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "فعال کردن وظایف", "enable_tasks_desc": "نمایش نمای وظایف در تقویم برای مدیریت کارها", "show_tasks_on_calendar": "نمایش وظایف روی تقویم", - "show_tasks_on_calendar_desc": "نمایش برچسب وظایف در نماهای روز و هفته", + "show_tasks_on_calendar_desc": "نمایش وظایف در نماهای ماه، هفته و روز", "hover_preview": "پیش‌نمایش هاور رویداد", "hover_preview_desc": "نمایش popover جزئیات هنگام هاور روی رویدادهای تقویم", "hover_preview_instant": "بلافاصله", diff --git a/locales/fr/common.json b/locales/fr/common.json index ba35b2456..bd76bd1b0 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Activer les tâches", "enable_tasks_desc": "Afficher une vue des tâches dans le calendrier pour gérer les choses à faire", "show_tasks_on_calendar": "Afficher les tâches dans le calendrier", - "show_tasks_on_calendar_desc": "Afficher les pastilles des tâches dans les vues jour et semaine du calendrier", + "show_tasks_on_calendar_desc": "Afficher les tâches dans les vues mois, semaine et jour du calendrier", "hover_preview": "Aperçu au survol", "hover_preview_desc": "Afficher un popover de détails au survol des événements du calendrier", "hover_preview_instant": "Instantané", diff --git a/locales/he/common.json b/locales/he/common.json index d99170230..89549fd98 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2914,7 +2914,7 @@ "enable_tasks": "אפשר משימות", "enable_tasks_desc": "הצג תצוגת משימות בלוח השנה לניהול מטלות", "show_tasks_on_calendar": "הצג משימות בלוח השנה", - "show_tasks_on_calendar_desc": "הצג שבבי משימות בתצוגות יומן היום והשבוע", + "show_tasks_on_calendar_desc": "הצג משימות בתצוגות החודש, השבוע והיום של היומן", "hover_preview": "תצוגה מקדימה של ריחוף לאירוע", "hover_preview_desc": "הצג חלון קופץ לפרטי בעת ריחוף מעל אירועי לוח שנה", "hover_preview_instant": "מיידי", diff --git a/locales/hu/common.json b/locales/hu/common.json index 730b98bc9..91a40b89b 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Feladatok engedélyezése", "enable_tasks_desc": "Feladatnézet mutatása a naptárban a teendők kezeléséhez", "show_tasks_on_calendar": "Feladatok mutatása a naptárban", - "show_tasks_on_calendar_desc": "Feladat chip-ek mutatása a nap és hét naptár nézetekben", + "show_tasks_on_calendar_desc": "Feladatok mutatása a havi, heti és napi naptárnézetben", "hover_preview": "Esemény előnézet egérrel", "hover_preview_desc": "Részletes felugró ablak mutatása az események fölé húzva", "hover_preview_instant": "Azonnali", diff --git a/locales/it/common.json b/locales/it/common.json index dc68d2246..480560aeb 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Abilita attività", "enable_tasks_desc": "Mostra una vista attività nel calendario per gestire le cose da fare", "show_tasks_on_calendar": "Mostra attività nel calendario", - "show_tasks_on_calendar_desc": "Mostra gli indicatori delle attività nelle viste giornaliera e settimanale del calendario", + "show_tasks_on_calendar_desc": "Mostra le attività nelle viste mensile, settimanale e giornaliera del calendario", "hover_preview": "Anteprima al passaggio del mouse", "hover_preview_desc": "Mostra un popover dettagliato al passaggio del mouse sugli eventi del calendario", "hover_preview_instant": "Istantaneo", diff --git a/locales/ja/common.json b/locales/ja/common.json index 1ab168ef8..c6a2414e1 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "タスクを有効化", "enable_tasks_desc": "カレンダーにタスク表示を追加して、ToDoを管理します", "show_tasks_on_calendar": "カレンダーにタスクを表示", - "show_tasks_on_calendar_desc": "日表示と週表示にタスクチップを表示します", + "show_tasks_on_calendar_desc": "月表示・週表示・日表示にタスクを表示します", "hover_preview": "イベントホバープレビュー", "hover_preview_desc": "カレンダーイベントにマウスを合わせたときに詳細ポップオーバーを表示", "hover_preview_instant": "即時", diff --git a/locales/ko/common.json b/locales/ko/common.json index 010487de7..ea498c9af 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "할 일 사용", "enable_tasks_desc": "캘린더에 할 일을 관리할 수 있는 메뉴를 추가해요", "show_tasks_on_calendar": "캘린더에 할 일 표시", - "show_tasks_on_calendar_desc": "일간 및 주간 캘린더에 할 일 배지를 보여줘요", + "show_tasks_on_calendar_desc": "월간, 주간 및 일간 캘린더에 할 일을 보여줘요", "hover_preview": "마우스 호버 미리보기", "hover_preview_desc": "캘린더 일정 위에 마우스를 올리면 세부 정보 팝업을 띄워요", "hover_preview_instant": "즉시", diff --git a/locales/lv/common.json b/locales/lv/common.json index 52627b2da..b4e5be4a8 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2989,7 +2989,7 @@ "enable_tasks": "Iespējot uzdevumus", "enable_tasks_desc": "Rādīt uzdevumu sadaļu kalendārā", "show_tasks_on_calendar": "Rādīt uzdevumus kalendārā", - "show_tasks_on_calendar_desc": "Rādīt uzdevumu ierakstus dienas un nedēļas skatā", + "show_tasks_on_calendar_desc": "Rādīt uzdevumus mēneša, nedēļas un dienas skatā", "hover_preview": "Priekšskatījums uzvirzot", "hover_preview_desc": "Rādīt informāciju, uzvirzot kursoru uz pasākuma", "hover_preview_instant": "Uzreiz", diff --git a/locales/mn/common.json b/locales/mn/common.json index 21d5c4338..2d601ace2 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Даалгавруудыг идэвхжүүлэх", "enable_tasks_desc": "Хийх ажлуудыг удирдахын тулд календарьт ажлын харагдах байдлыг харуулах", "show_tasks_on_calendar": "Хуанли дээр даалгавруудыг харуулах", - "show_tasks_on_calendar_desc": "Даалгаврын чипийг өдөр, долоо хоногийн хуанлийн харагдац дээр харуулах", + "show_tasks_on_calendar_desc": "Даалгаврыг сар, долоо хоног, өдрийн хуанлийн харагдац дээр харуулах", "hover_preview": "Үйл явдлыг товшино уу", "hover_preview_desc": "Календарийн үйл явдлууд дээр хулганыг гүйлгэх үед дэлгэрэнгүй поповыг харуулах", "hover_preview_instant": "Шуурхай", diff --git a/locales/nb/common.json b/locales/nb/common.json index d24f1af94..e992aef07 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Aktiver oppgaver", "enable_tasks_desc": "Vis en oppgavevisning i kalenderen for å administrere gjøremål", "show_tasks_on_calendar": "Vis oppgaver i kalenderen", - "show_tasks_on_calendar_desc": "Vis oppgavemarkører i dags- og ukesvisningen", + "show_tasks_on_calendar_desc": "Vis oppgaver i måneds-, uke- og dagsvisningen", "hover_preview": "Forhåndsvis hendelser når du peker", "hover_preview_desc": "Vis et detaljvindu når du holder markøren over kalenderhendelser", "hover_preview_instant": "Øyeblikkelig", diff --git a/locales/nl/common.json b/locales/nl/common.json index 2a4cfedfc..a1aa32e42 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Taken inschakelen", "enable_tasks_desc": "Een takenweergave in de agenda tonen om to-do's te beheren", "show_tasks_on_calendar": "Taken in agenda tonen", - "show_tasks_on_calendar_desc": "Taaklabels tonen in dag- en weekweergave van de agenda", + "show_tasks_on_calendar_desc": "Taken tonen in de maand-, week- en dagweergave van de agenda", "hover_preview": "Voorvertoning bij hover", "hover_preview_desc": "Toon een detailpopover bij het hoveren over agenda-evenementen", "hover_preview_instant": "Direct", diff --git a/locales/pl/common.json b/locales/pl/common.json index 720be1eb1..095ea9c34 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Włącz zadania", "enable_tasks_desc": "Pokaż widok zadań w kalendarzu do zarządzania zadaniami", "show_tasks_on_calendar": "Pokaż zadania w kalendarzu", - "show_tasks_on_calendar_desc": "Wyświetlaj oznaczenia zadań w dziennym i tygodniowym widoku kalendarza", + "show_tasks_on_calendar_desc": "Wyświetlaj zadania w miesięcznym, tygodniowym i dziennym widoku kalendarza", "hover_preview": "Podgląd wydarzenia po najechaniu", "hover_preview_desc": "Pokaż wyskakujące okno szczegółów po najechaniu na wydarzenia w kalendarzu", "hover_preview_instant": "Natychmiast", diff --git a/locales/pt/common.json b/locales/pt/common.json index 7df6bc600..acd38dd8c 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Ativar tarefas", "enable_tasks_desc": "Mostrar uma visualização de tarefas no calendário para gerenciar pendências", "show_tasks_on_calendar": "Mostrar tarefas no calendário", - "show_tasks_on_calendar_desc": "Exibir marcadores de tarefas nas visualizações diária e semanal do calendário", + "show_tasks_on_calendar_desc": "Exibir tarefas nas visualizações mensal, semanal e diária do calendário", "hover_preview": "Pré-visualização ao passar o rato", "hover_preview_desc": "Mostrar um popover de detalhes ao passar o rato sobre eventos do calendário", "hover_preview_instant": "Instantâneo", diff --git a/locales/ro/common.json b/locales/ro/common.json index 679a7e9d4..78187a83f 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Activați sarcinile", "enable_tasks_desc": "Afișați o vizualizare a sarcinilor în calendar pentru gestionarea sarcinilor de îndeplinit", "show_tasks_on_calendar": "Afișați sarcinile în calendar", - "show_tasks_on_calendar_desc": "Afișați cartonașe cu sarcini în vizualizările calendarului zilnic și săptămânal", + "show_tasks_on_calendar_desc": "Afișați sarcinile în vizualizările lunare, săptămânale și zilnice ale calendarului", "hover_preview": "Previzualizare la trecerea cursorului peste elemente", "hover_preview_desc": "Afișați o fereastră pop-up cu detalii atunci când treceți cu mouse-ul peste evenimentele din calendar", "hover_preview_instant": "Instant", diff --git a/locales/ru/common.json b/locales/ru/common.json index 24a8ff361..d97793776 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Включить задачи", "enable_tasks_desc": "Показывать представление задач в календаре для управления делами", "show_tasks_on_calendar": "Показывать задачи в календаре", - "show_tasks_on_calendar_desc": "Отображать чипы задач в видах «день» и «неделя»", + "show_tasks_on_calendar_desc": "Отображать задачи в видах «месяц», «неделя» и «день»", "hover_preview": "Предпросмотр при наведении", "hover_preview_desc": "Показывать всплывающее окно с деталями при наведении на события календаря", "hover_preview_instant": "Мгновенно", diff --git a/locales/sk/common.json b/locales/sk/common.json index 9a1188f35..a6cff0f67 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Povoliť úlohy", "enable_tasks_desc": "Zobrazovať zobrazenie úloh v kalendári", "show_tasks_on_calendar": "Zobraziť úlohy v kalendári", - "show_tasks_on_calendar_desc": "Zobrazovať značky úloh v dennom a týždennom zobrazení kalendára", + "show_tasks_on_calendar_desc": "Zobrazovať úlohy v mesačnom, týždennom a dennom zobrazení kalendára", "hover_preview": "Náhľad udalosti po prejdení", "hover_preview_desc": "Zobrazovať vyskakovacie okno s podrobnosťami pri najetí myšou na udalosti v kalendári", "hover_preview_instant": "Okamžite", diff --git a/locales/tr/common.json b/locales/tr/common.json index bce533ba7..570d41e53 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Görevleri etkinleştir", "enable_tasks_desc": "Yapılacaklar listesini yönetmek için takvimde görev görünümü göster", "show_tasks_on_calendar": "Görevleri takvimde göster", - "show_tasks_on_calendar_desc": "Gün ve hafta takvim görünümlerinde görev parçacıklarını görüntüle", + "show_tasks_on_calendar_desc": "Görevleri ay, hafta ve gün takvim görünümlerinde görüntüle", "hover_preview": "Etkinlik üzerine gelme önizlemesi", "hover_preview_desc": "Takvim etkinliklerinin üzerine gelindiğinde ayrıntı açılır penceresi göster", "hover_preview_instant": "Anında", diff --git a/locales/uk/common.json b/locales/uk/common.json index f25f1d67f..f2f8a9da7 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "Увімкнути завдання", "enable_tasks_desc": "Показати перегляд завдань у календарі для керування справами", "show_tasks_on_calendar": "Показати завдання в календарі", - "show_tasks_on_calendar_desc": "Відображення фішок завдань у режимі перегляду календаря дня та тижня", + "show_tasks_on_calendar_desc": "Відображати завдання в режимах перегляду календаря місяця, тижня та дня", "hover_preview": "Попередній перегляд подій", "hover_preview_desc": "Показувати спливаюче вікно з деталями під час наведення курсора на події календаря", "hover_preview_instant": "Миттєво", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 0c05405e7..5c3687e77 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "啟用任務", "enable_tasks_desc": "在行事曆中顯示任務檢視以管理待辦事項", "show_tasks_on_calendar": "在行事曆上顯示任務", - "show_tasks_on_calendar_desc": "在行事曆的日/週檢視中顯示任務標籤", + "show_tasks_on_calendar_desc": "在行事曆的月/週/日檢視中顯示任務", "hover_preview": "活動游標暫留預覽", "hover_preview_desc": "游標移至行事曆活動上方時顯示詳細資料浮動視窗", "hover_preview_instant": "立即", diff --git a/locales/zh/common.json b/locales/zh/common.json index 6b2ccf8bc..c06541007 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2990,7 +2990,7 @@ "enable_tasks": "启用任务", "enable_tasks_desc": "在日历中显示任务视图以管理待办事项", "show_tasks_on_calendar": "在日历上显示任务", - "show_tasks_on_calendar_desc": "在日历的日/周视图中显示任务标签", + "show_tasks_on_calendar_desc": "在日历的月/周/日视图中显示任务", "hover_preview": "事件悬停预览", "hover_preview_desc": "将鼠标悬停在日历事件上时显示详细信息弹出窗口", "hover_preview_instant": "立即",