feat: show tasks with due dates in the calendar month view #1107

This commit is contained in:
Linus Rath
2026-09-26 20:47:13 +02:00
parent 7b25d3a95b
commit 078e9f9676
35 changed files with 551 additions and 116 deletions
@@ -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> = {}): 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<typeof CalendarMonthView>;
function renderView(overrides: Partial<Props> = {}) {
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(<CalendarMonthView {...props} />) };
}
function chipSlot(taskId: string): HTMLElement {
const chip = document.querySelector<HTMLElement>(`[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<HTMLElement>('[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<HTMLElement>('[data-calendar-task="t1"]');
expect(ring?.tagName).toBe('SPAN');
expect(ring).toHaveClass('border');
expect(screen.queryByText('Task t1')).toBeNull();
});
});
+69 -16
View File
@@ -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<import("@/lib/jmap/types").CalendarTask>) => {
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 && (
<div
role="dialog"
aria-modal="true"
aria-label={editTask ? t("tasks.edit") : t("tasks.create")}
className={isPaneScoped ? "absolute inset-0 z-40 flex flex-col bg-background" : "fixed inset-0 z-50 flex flex-col bg-background"}
>
<TaskModal
key={editTask?.id ?? 'new-task'}
task={editTask}
calendars={displayCalendars}
onSave={handleSaveTask}
onDelete={handleDeleteTask}
onClose={() => { setShowTaskModal(false); setEditTask(null); }}
isMobile={true}
/>
</div>
)}
{showImportModal && client && (
<ICalImportModal
calendars={displayCalendars}
+10 -16
View File
@@ -3,13 +3,14 @@
import { useMemo, useEffect, useLayoutEffect, useRef, useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter";
import { format, isSameDay, parseISO, eachDayOfInterval, differenceInCalendarDays } from "date-fns";
import { 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 { formatSnapTime, getEventDayBounds, getPrimaryCalendarId, isTimedEventFullDayOnDate, layoutOverlappingEvents } from "@/lib/calendar-utils";
import { displayNow, isDisplayToday } from "@/lib/timezone";
import { groupTasksByDueDay } from "@/lib/calendar-tasks";
import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types";
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
import { useScrollWindow, getScrollStart, setScrollStart, scrollToStart } from "@/hooks/use-scroll-window";
@@ -31,6 +32,7 @@ interface CalendarDayViewProps extends ScrollWindowViewProps {
pendingPreview?: PendingEventPreview | null;
tasks?: CalendarTask[];
onToggleTaskComplete?: (task: CalendarTask) => 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<string, CalendarTask[]>();
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 (
<div
key={task.id}
data-calendar-task={task.id}
onClick={() => 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}` }}
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onToggleTaskComplete?.(task); }}
className={cn(
"flex-shrink-0 w-3.5 h-3.5 rounded-full border flex items-center justify-center",
@@ -366,6 +359,7 @@ export function CalendarDayView({
? "bg-success border-success text-success-foreground"
: "border-muted-foreground/40 hover:border-primary"
)}
aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")}
>
{isCompleted && <Check className="h-2.5 w-2.5" />}
</button>
+70 -7
View File
@@ -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<HTMLDivElement>(null);
const topSentinelRef = useRef<HTMLDivElement>(null);
@@ -258,10 +281,10 @@ export function CalendarMonthView({
onScroll={handleScroll}
>
<div ref={topSentinelRef} data-testid="month-top-sentinel" className="h-px flex-shrink-0" />
{weekSegments.map(({ week, segments, rowCount }) => (
{weekSegments.map(({ week, segments, rowCount, dayEventRows, dayTasks, contentRows }) => (
<div key={dayKey(week[0])} data-week={dayKey(week[0])} className="relative flex-shrink-0 border-b border-border" role="row" style={{
minHeight: showChips
? Math.max(rowMinHeight, overlayTop + 4 + rowCount * rowHeight + 8)
? Math.max(rowMinHeight, overlayTop + 4 + contentRows * rowHeight + 8)
: rowMinHeight,
}}>
<div className="grid grid-cols-7 h-full">
@@ -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 (
<span
key={`task-${task.id}`}
data-calendar-task={task.id}
className={cn("w-1.5 h-1.5 rounded-full border", task.progress === "completed" && "opacity-40")}
style={{ borderColor: (calId && calendarMap.get(calId)?.color) || "#3b82f6" }}
/>
);
})}
{dayEvents.length + dayTaskList.length > 3 && (
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40" />
)}
{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({
})}
</div>
)}
{showChips && dayTasks.some((dayTaskList) => dayTaskList.length > 0) && (
<div className="absolute inset-x-0 pointer-events-none" style={{ top: overlayTop }}>
{dayTasks.map((dayTaskList, dayIndex) => dayTaskList.map((task, taskIndex) => {
const calId = Object.keys(task.calendarIds).find((id) => calendarMap.has(id));
return (
<div
key={`task-${task.id}`}
className="absolute px-0.5 pointer-events-auto"
style={{
left: `calc(${(dayIndex / 7) * 100}% + 1px)`,
width: `calc(${(1 / 7) * 100}% - 2px)`,
top: (dayEventRows[dayIndex] + taskIndex) * rowHeight,
height: chipHeight,
}}
>
<CalendarTaskChip
task={task}
calendar={calId ? calendarMap.get(calId) : undefined}
onToggleComplete={onToggleTaskComplete}
onSelect={onSelectTask}
/>
</div>
);
}))}
</div>
)}
</div>
))}
<div ref={bottomSentinelRef} data-testid="month-bottom-sentinel" className="h-px flex-shrink-0" />
+25 -50
View File
@@ -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<string, CalendarTask[]>();
const map = new Map<string, CalendarTask[]>();
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 (
<div
key={`task-${task.id}`}
className="absolute px-0.5 pointer-events-auto"
style={{
left: `calc(${(dayIndex / colCount) * 100}% + 1px)`,
width: `calc(${(1 / colCount) * 100}% - 2px)`,
top: taskIndex * 24,
height: 20,
}}
>
<div
className="h-full rounded text-[10px] leading-[20px] font-medium px-1.5 truncate flex items-center gap-1 cursor-pointer hover:opacity-80"
style={{ backgroundColor: `${color}20`, borderLeft: `3px solid ${color}` }}
onClick={() => onToggleTaskComplete?.(task)}
>
<span className={cn(
"w-2.5 h-2.5 rounded-full border flex-shrink-0 flex items-center justify-center",
isCompleted ? "bg-success border-success" : "border-current"
)}>
{isCompleted && <Check className="h-2 w-2 text-white" />}
</span>
<span className={cn("truncate", isCompleted && "line-through text-muted-foreground")}>
{task.title}
</span>
</div>
</div>
);
});
return dayTasks.map((task, taskIndex) => (
<div
key={`task-${task.id}`}
className="absolute px-0.5 pointer-events-auto"
style={{
left: `calc(${(dayIndex / colCount) * 100}% + 1px)`,
width: `calc(${(1 / colCount) * 100}% - 2px)`,
top: taskIndex * 24,
height: 20,
}}
>
<CalendarTaskChip
task={task}
calendar={calendars.find(c => task.calendarIds[c.id])}
onToggleComplete={onToggleTaskComplete}
onSelect={onSelectTask}
/>
</div>
));
})}
</div>
)}
+61
View File
@@ -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 (
<div
data-calendar-task={task.id}
className={cn("h-full rounded text-[10px] font-medium flex items-center overflow-hidden", className)}
style={{ backgroundColor: `${color}20`, borderInlineStart: `3px solid ${color}` }}
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onToggleComplete?.(task); }}
className="group h-full ps-1 pe-0.5 flex items-center flex-shrink-0"
aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")}
>
<span
className={cn(
"w-2.5 h-2.5 rounded-full border flex items-center justify-center",
isCompleted ? "bg-success border-success text-success-foreground" : "group-hover:bg-background",
)}
style={isCompleted ? undefined : { borderColor: color }}
>
{isCompleted && <Check className="h-2 w-2" />}
</span>
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onSelect?.(task); }}
className={cn(
"h-full flex-1 min-w-0 truncate text-start pe-1 hover:opacity-80",
isCompleted && "line-through text-muted-foreground",
)}
title={title}
>
{title}
</button>
</div>
);
}
+86
View File
@@ -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> = {}): 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);
});
});
+48
View File
@@ -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<string, CalendarTask[]> {
const map = new Map<string, CalendarTask[]>();
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;
}
+1 -1
View File
@@ -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": "فوري",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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ě",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "بلافاصله",
+1 -1
View File
@@ -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é",
+1 -1
View File
@@ -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": "מיידי",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "即時",
+1 -1
View File
@@ -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": "즉시",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "Шуурхай",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "Мгновенно",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "Миттєво",
+1 -1
View File
@@ -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": "立即",
+1 -1
View File
@@ -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": "立即",