mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: ask which occurrences a dragged or resized series event moves, and offer Undo after a move
This commit is contained in:
@@ -339,3 +339,39 @@ describe('CalendarApp read-only events', () => {
|
||||
expect(screen.getAllByRole('button', { name: /Standup/ })[0]).toHaveAttribute('draggable', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
function dropOnMonthDay(day: RegExp, event: CalendarEvent) {
|
||||
const cell = screen.getAllByRole('gridcell').find((c) => day.test(c.getAttribute('aria-label') ?? ''));
|
||||
if (!cell) throw new Error(`no cell ${day}`);
|
||||
const json = JSON.stringify({ type: 'calendar-event', eventId: event.id, originalStart: event.start, duration: event.duration });
|
||||
fireEvent.drop(cell, { dataTransfer: { types: ['application/x-calendar-event'], getData: () => json } });
|
||||
}
|
||||
|
||||
describe('CalendarApp dragging', () => {
|
||||
it('asks which occurrences a drag of a series moves', async () => {
|
||||
setUp([weekly], 'month');
|
||||
renderApp();
|
||||
dropOnMonthDay(/October 14, 2026/, weekly);
|
||||
const scope = await screen.findByRole('dialog', { name: 'edit_title' });
|
||||
expect(store.updateEvent).not.toHaveBeenCalled();
|
||||
fireEvent.click(within(scope).getByRole('button', { name: 'cancel' }));
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'edit_title' })).toBeNull());
|
||||
expect(store.updateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('moves one event at once and can move it back', async () => {
|
||||
setUp([makeEvent()], 'month');
|
||||
renderApp();
|
||||
dropOnMonthDay(/October 14, 2026/, makeEvent());
|
||||
await waitFor(() => expect(store.updateEvent).toHaveBeenCalledTimes(1));
|
||||
expect(store.updateEvent.mock.calls[0][2]).toMatchObject({ start: '2026-10-14T10:00:00' });
|
||||
const moved = await waitFor(() => {
|
||||
const found = useToastStore.getState().toasts.find((t) => t.title === 'notifications.event_moved');
|
||||
expect(found).toBeTruthy();
|
||||
return found!;
|
||||
});
|
||||
moved.action!.onClick();
|
||||
await waitFor(() => expect(store.updateEvent).toHaveBeenCalledTimes(2));
|
||||
expect(store.updateEvent.mock.calls[1][2]).toMatchObject({ start: '2026-10-12T10:00:00', duration: 'PT1H' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ import { deleteOccurrence, saveOccurrenceEdit, type EventEditorState } from "@/l
|
||||
import { useContactNameResolver } from "@/hooks/use-contact-name-resolver";
|
||||
import { getClientByLocalAccountId } from "@/stores/client-registry";
|
||||
import { useCalendarErrorMessage } from "@/hooks/use-calendar-error-message";
|
||||
import { getEventStartDate } from "@/lib/calendar-utils";
|
||||
import { getEventDisplayEndDate, getEventEndDate, getEventStartDate } from "@/lib/calendar-utils";
|
||||
import { displayNow, getEffectiveTimeZone } from "@/lib/timezone";
|
||||
import { canCreateEventsIn, getEventEditability, type EditabilityContext } from "@/lib/calendar-editability";
|
||||
import { QuickCreatePopover, type QuickCreateDraft } from "@/components/calendar/quick-create-popover";
|
||||
@@ -73,7 +73,8 @@ import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
||||
import { SchedulingDeniedError } from "@/lib/jmap/scheduling-error";
|
||||
import { CreateCalendarModal } from "@/components/calendar/create-calendar-modal";
|
||||
import { CalendarSidebarHeader } from "@/components/calendar/calendar-sidebar-header";
|
||||
import { getUserParticipantId, collectUserCalendarAddresses, userOrganizes } from "@/lib/calendar-participants";
|
||||
import { getUserParticipantId, collectUserCalendarAddresses, userOrganizes, getParticipantList } from "@/lib/calendar-participants";
|
||||
import type { MoveEventHandler } from "@/hooks/use-time-grid-interactions";
|
||||
import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { sharedCalendarColorKey, pickUnusedCalendarColor } from "@/lib/shared-calendar-colors";
|
||||
import { debug } from "@/lib/debug";
|
||||
@@ -1303,6 +1304,53 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
}
|
||||
}, [client, pendingScopeAction, updateEvent, deleteEvent, createEvent, findMasterEvent, currentUserEmails, resolveContactName, refetchCurrentRange, submitRsvp, describeError, t]);
|
||||
|
||||
// A drag or resize. A series asks which occurrences it covers, as the
|
||||
// editor does; one event moves at once and can be moved back.
|
||||
const handleMoveEvent = useCallback<MoveEventHandler>(async (event, updates, sendScheduling, kind) => {
|
||||
if (!client) { toast.error(t("notifications.event_move_error")); return; }
|
||||
if (isRecurringEvent(event)) {
|
||||
const moved = { ...event, ...updates };
|
||||
const allDay = !!event.showWithoutTime;
|
||||
// The attendees stay as they are; the save works out the rest from
|
||||
// where the occurrence went, as for a time changed in the editor.
|
||||
const attendees = getParticipantList(event, { resolveName: resolveContactName })
|
||||
.filter((p) => !p.isOrganizer && p.email)
|
||||
.map((p) => ({ name: p.name, email: p.email.trim() }));
|
||||
setPendingScopeAction({
|
||||
type: "edit",
|
||||
event,
|
||||
updates,
|
||||
sendScheduling,
|
||||
editor: {
|
||||
initialAttendees: attendees,
|
||||
attendees,
|
||||
when: { allDay, start: getEventStartDate(moved), end: allDay ? getEventDisplayEndDate(moved) : getEventEndDate(moved) },
|
||||
ruleChanged: false,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// false: a plugin stopped it and said why.
|
||||
if (!(await updateEvent(client, event.id, updates, sendScheduling))) return;
|
||||
} catch (error) {
|
||||
toast.error(describeError(error, t(kind === "resize" ? "notifications.event_resize_error" : "notifications.event_move_error")));
|
||||
return;
|
||||
}
|
||||
const before: Partial<CalendarEvent> = { start: event.start, duration: event.duration };
|
||||
if (updates.timeZone !== undefined) before.timeZone = event.timeZone;
|
||||
toast.success(t(kind === "resize" ? "notifications.event_updated" : "notifications.event_moved"), {
|
||||
action: {
|
||||
label: t("notifications.undo"),
|
||||
onClick: () => {
|
||||
void updateEvent(client, event.id, before, sendScheduling).catch((error: unknown) => {
|
||||
toast.error(describeError(error, t("notifications.event_move_error")));
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [client, updateEvent, resolveContactName, describeError, t]);
|
||||
|
||||
const handleRsvp = useCallback(async (eventId: string, participantId: string, status: CalendarParticipant['participationStatus']) => {
|
||||
if (!client) return;
|
||||
// An answer on one occurrence of a series asks whether it covers just
|
||||
@@ -1665,6 +1713,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
onSelectTask={openEditTaskModal}
|
||||
currentUserEmails={currentUserEmails}
|
||||
canEditEvent={canEditEvent}
|
||||
onMoveEvent={handleMoveEvent}
|
||||
/>
|
||||
);
|
||||
case "week":
|
||||
@@ -1691,6 +1740,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
onSelectTask={openEditTaskModal}
|
||||
currentUserEmails={currentUserEmails}
|
||||
canEditEvent={canEditEvent}
|
||||
onMoveEvent={handleMoveEvent}
|
||||
/>
|
||||
);
|
||||
case "day":
|
||||
@@ -1714,6 +1764,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
onSelectTask={openEditTaskModal}
|
||||
currentUserEmails={currentUserEmails}
|
||||
canEditEvent={canEditEvent}
|
||||
onMoveEvent={handleMoveEvent}
|
||||
/>
|
||||
);
|
||||
case "agenda":
|
||||
|
||||
@@ -15,7 +15,7 @@ import { displayNow, isDisplayToday } from "@/lib/timezone";
|
||||
import { groupTasksByDueDay } from "@/lib/calendar-tasks";
|
||||
import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types";
|
||||
import { useDisplayHours } from "@/hooks/use-display-hours";
|
||||
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
||||
import { useTimeGridInteractions, type MoveEventHandler } from "@/hooks/use-time-grid-interactions";
|
||||
import { useScrollWindow, getScrollStart, setScrollStart, scrollToStart } from "@/hooks/use-scroll-window";
|
||||
import { dayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window";
|
||||
import type { PendingEventPreview } from "./event-modal";
|
||||
@@ -46,6 +46,11 @@ interface CalendarDayViewProps extends ScrollWindowViewProps {
|
||||
currentUserEmails?: string[];
|
||||
/** Whether the user may change an event: only those can be dragged or resized. */
|
||||
canEditEvent?: (event: CalendarEvent) => boolean;
|
||||
/**
|
||||
* Save a drag or resize. The page asks which occurrences a change to a
|
||||
* series covers; without it the change goes straight to the store.
|
||||
*/
|
||||
onMoveEvent?: MoveEventHandler;
|
||||
}
|
||||
|
||||
const HOUR_HEIGHT = 64;
|
||||
@@ -78,6 +83,7 @@ export function CalendarDayView({
|
||||
onSelectTask,
|
||||
currentUserEmails,
|
||||
canEditEvent = () => true,
|
||||
onMoveEvent,
|
||||
}: CalendarDayViewProps) {
|
||||
const t = useTranslations("calendar");
|
||||
const describeError = useCalendarErrorMessage();
|
||||
@@ -280,6 +286,7 @@ export function CalendarDayView({
|
||||
describeError,
|
||||
currentUserEmails,
|
||||
isMobile,
|
||||
onMoveEvent,
|
||||
});
|
||||
|
||||
const formatHour = (h: number): string => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useScrollWindow } from "@/hooks/use-scroll-window";
|
||||
import { dayKey, parseDayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window";
|
||||
import { useCalendarErrorMessage } from "@/hooks/use-calendar-error-message";
|
||||
import { PluginDayLabel } from "./plugin-day-label";
|
||||
import type { MoveEventHandler } from "@/hooks/use-time-grid-interactions";
|
||||
|
||||
interface CalendarMonthViewProps extends ScrollWindowViewProps {
|
||||
selectedDate: Date;
|
||||
@@ -48,6 +49,11 @@ interface CalendarMonthViewProps extends ScrollWindowViewProps {
|
||||
currentUserEmails?: string[];
|
||||
/** Whether the user may change an event: only those can be dragged or resized. */
|
||||
canEditEvent?: (event: CalendarEvent) => boolean;
|
||||
/**
|
||||
* Save a drag or resize. The page asks which occurrences a change to a
|
||||
* series covers; without it the change goes straight to the store.
|
||||
*/
|
||||
onMoveEvent?: MoveEventHandler;
|
||||
}
|
||||
|
||||
/** Fraction of the viewport height at which the "current month" is sampled. */
|
||||
@@ -80,6 +86,7 @@ export function CalendarMonthView({
|
||||
onSelectTask,
|
||||
currentUserEmails,
|
||||
canEditEvent = () => true,
|
||||
onMoveEvent,
|
||||
}: CalendarMonthViewProps) {
|
||||
const t = useTranslations("calendar");
|
||||
const describeError = useCalendarErrorMessage();
|
||||
@@ -290,11 +297,12 @@ export function CalendarMonthView({
|
||||
const client = useAuthStore.getState().client;
|
||||
if (!client) return;
|
||||
const moved = movedEventUpdates(event, { start: newStartISO }, currentUserEmails ?? []);
|
||||
await useCalendarStore.getState().updateEvent(client, data.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
if (event && onMoveEvent) await onMoveEvent(event, moved.updates, moved.sendSchedulingMessages, "move");
|
||||
else await useCalendarStore.getState().updateEvent(client, data.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
} catch (error) {
|
||||
toast.error(describeError(error, t("notifications.event_move_error")));
|
||||
}
|
||||
}, [t, describeError, currentUserEmails]);
|
||||
}, [t, describeError, currentUserEmails, onMoveEvent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 overflow-hidden" role="grid" aria-label={formatFullDate(selectedDate)}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { displayNow, isDisplayToday } from "@/lib/timezone";
|
||||
import type { CalendarEvent, Calendar, CalendarTask } from "@/lib/jmap/types";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useDisplayHours } from "@/hooks/use-display-hours";
|
||||
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
||||
import { useTimeGridInteractions, type MoveEventHandler } from "@/hooks/use-time-grid-interactions";
|
||||
import { useScrollWindow, getScrollStart, setScrollStart, scrollToStart } from "@/hooks/use-scroll-window";
|
||||
import { dayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window";
|
||||
import type { PendingEventPreview } from "./event-modal";
|
||||
@@ -53,6 +53,11 @@ interface CalendarWeekViewProps extends ScrollWindowViewProps {
|
||||
currentUserEmails?: string[];
|
||||
/** Whether the user may change an event: only those can be dragged or resized. */
|
||||
canEditEvent?: (event: CalendarEvent) => boolean;
|
||||
/**
|
||||
* Save a drag or resize. The page asks which occurrences a change to a
|
||||
* series covers; without it the change goes straight to the store.
|
||||
*/
|
||||
onMoveEvent?: MoveEventHandler;
|
||||
}
|
||||
|
||||
const HOUR_HEIGHT = 60;
|
||||
@@ -89,6 +94,7 @@ export function CalendarWeekView({
|
||||
onSelectTask,
|
||||
currentUserEmails,
|
||||
canEditEvent = () => true,
|
||||
onMoveEvent,
|
||||
}: CalendarWeekViewProps) {
|
||||
const t = useTranslations("calendar");
|
||||
const describeError = useCalendarErrorMessage();
|
||||
@@ -373,6 +379,7 @@ export function CalendarWeekView({
|
||||
describeError,
|
||||
currentUserEmails,
|
||||
isMobile,
|
||||
onMoveEvent,
|
||||
});
|
||||
|
||||
const formatHour = (h: number): string => {
|
||||
|
||||
@@ -7,7 +7,15 @@ import { debug } from "@/lib/debug";
|
||||
import { formatIsoInTimeZone, getEventStartDate } from "@/lib/calendar-utils";
|
||||
import { fromDisplayDate, getEffectiveTimeZone } from "@/lib/timezone";
|
||||
import { movedEventUpdates } from "@/lib/calendar-participants";
|
||||
import type { Calendar } from "@/lib/jmap/types";
|
||||
import type { Calendar, CalendarEvent } from "@/lib/jmap/types";
|
||||
|
||||
/** Saves a dragged or resized event; `kind` says which, for the message. */
|
||||
export type MoveEventHandler = (
|
||||
event: CalendarEvent,
|
||||
updates: Partial<CalendarEvent>,
|
||||
sendSchedulingMessages: boolean | undefined,
|
||||
kind: "move" | "resize",
|
||||
) => Promise<unknown> | void;
|
||||
|
||||
interface DragCreateState {
|
||||
dayKey: string;
|
||||
@@ -59,6 +67,8 @@ interface UseTimeGridInteractionsOptions {
|
||||
*/
|
||||
gridStartMinutes?: number;
|
||||
gridEndMinutes?: number;
|
||||
/** Save a drop or resize (the page asks the scope of a series); else the store directly. */
|
||||
onMoveEvent?: MoveEventHandler;
|
||||
}
|
||||
|
||||
/** `date` moved by `minutes` on the local clock, not by elapsed time. */
|
||||
@@ -78,6 +88,7 @@ export function useTimeGridInteractions({
|
||||
isMobile,
|
||||
gridStartMinutes = 0,
|
||||
gridEndMinutes = 1440,
|
||||
onMoveEvent,
|
||||
}: UseTimeGridInteractionsOptions) {
|
||||
const snapToMinutes = useCallback((clientY: number, containerTop: number): number => {
|
||||
const raw = gridStartMinutes + ((clientY - containerTop) / hourHeight) * 60;
|
||||
@@ -275,14 +286,15 @@ export function useTimeGridInteractions({
|
||||
const updates: { start?: string; duration: string } = { duration: dur };
|
||||
if (start) updates.start = start;
|
||||
const moved = movedEventUpdates(event, updates, currentUserEmails ?? []);
|
||||
await useCalendarStore.getState().updateEvent(client, resize.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
if (event && onMoveEvent) await onMoveEvent(event, moved.updates, moved.sendSchedulingMessages, "resize");
|
||||
else await useCalendarStore.getState().updateEvent(client, resize.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
} catch (error) {
|
||||
debug.error("Failed to resize event:", resize.eventId, error);
|
||||
toast.error(describeError ? describeError(error, errorMessages.resize) : errorMessages.resize);
|
||||
} finally {
|
||||
setResizeVisual(null);
|
||||
}
|
||||
}, [computeResize, errorMessages.resize, describeError, currentUserEmails]);
|
||||
}, [computeResize, errorMessages.resize, describeError, currentUserEmails, onMoveEvent]);
|
||||
|
||||
// --- Click / Double-click / Quick-create ---
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -410,11 +422,12 @@ export function useTimeGridInteractions({
|
||||
return;
|
||||
}
|
||||
const moved = movedEventUpdates(event, { start: newStartISO }, currentUserEmails ?? []);
|
||||
await useCalendarStore.getState().updateEvent(client, data.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
if (event && onMoveEvent) await onMoveEvent(event, moved.updates, moved.sendSchedulingMessages, "move");
|
||||
else await useCalendarStore.getState().updateEvent(client, data.eventId, moved.updates, moved.sendSchedulingMessages);
|
||||
} catch (error) {
|
||||
toast.error(describeError ? describeError(error, errorMessages.move) : errorMessages.move);
|
||||
}
|
||||
}, [snapDragMinutes, errorMessages.move, describeError, currentUserEmails]);
|
||||
}, [snapDragMinutes, errorMessages.move, describeError, currentUserEmails, onMoveEvent]);
|
||||
|
||||
return {
|
||||
dragCreate,
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "تم إنشاء الحدث",
|
||||
"event_updated": "تم تحديث الحدث",
|
||||
"event_moved": "تم نقل الحدث",
|
||||
"undo": "تراجع",
|
||||
"event_deleted": "تم حذف الحدث",
|
||||
"event_delete_error": "تعذّر حذف الحدث",
|
||||
"task_deleted": "تم حذف المهمة",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Esdeveniment creat",
|
||||
"event_updated": "Esdeveniment actualitzat",
|
||||
"event_moved": "Esdeveniment mogut",
|
||||
"undo": "Desfés",
|
||||
"event_deleted": "Esdeveniment suprimit",
|
||||
"event_delete_error": "No s'ha pogut suprimir l'esdeveniment",
|
||||
"task_deleted": "Tasca suprimida",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Událost byla vytvořena",
|
||||
"event_updated": "Událost byla aktualizována",
|
||||
"event_moved": "Událost byla přesunuta",
|
||||
"undo": "Zpět",
|
||||
"event_deleted": "Událost byla odstraněna",
|
||||
"event_delete_error": "Událost se nepodařilo odstranit",
|
||||
"task_deleted": "Úkol byl odstraněn",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Begivenhed oprettet",
|
||||
"event_updated": "Begivenhed opdateret",
|
||||
"event_moved": "Begivenhed flyttet",
|
||||
"undo": "Fortryd",
|
||||
"event_deleted": "Begivenhed slettet",
|
||||
"event_delete_error": "Begivenheden kunne ikke slettes",
|
||||
"task_deleted": "Opgave slettet",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Termin erstellt",
|
||||
"event_updated": "Termin aktualisiert",
|
||||
"event_moved": "Termin verschoben",
|
||||
"undo": "Rückgängig",
|
||||
"event_deleted": "Termin gelöscht",
|
||||
"event_delete_error": "Termin konnte nicht gelöscht werden",
|
||||
"task_deleted": "Aufgabe gelöscht",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Event created",
|
||||
"event_updated": "Event updated",
|
||||
"event_moved": "Event moved",
|
||||
"undo": "Undo",
|
||||
"event_deleted": "Event deleted",
|
||||
"event_delete_error": "Couldn't delete event",
|
||||
"task_deleted": "Task deleted",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Evento creado",
|
||||
"event_updated": "Evento actualizado",
|
||||
"event_moved": "Evento movido",
|
||||
"undo": "Deshacer",
|
||||
"event_deleted": "Evento eliminado",
|
||||
"event_delete_error": "No se pudo eliminar el evento",
|
||||
"task_deleted": "Tarea eliminada",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Sündmus loodud",
|
||||
"event_updated": "Sündmus uuendatud",
|
||||
"event_moved": "Sündmus liigutatud",
|
||||
"undo": "Võta tagasi",
|
||||
"event_deleted": "Sündmus kustutatud",
|
||||
"event_delete_error": "Sündmust ei saanud kustutada",
|
||||
"task_deleted": "Ülesanne kustutatud",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "رویداد ایجاد شد",
|
||||
"event_updated": "رویداد بهروزرسانی شد",
|
||||
"event_moved": "رویداد منتقل شد",
|
||||
"undo": "بازگردانی",
|
||||
"event_deleted": "رویداد حذف شد",
|
||||
"event_delete_error": "حذف رویداد ممکن نشد",
|
||||
"task_deleted": "وظیفه حذف شد",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Événement créé",
|
||||
"event_updated": "Événement mis à jour",
|
||||
"event_moved": "Événement déplacé",
|
||||
"undo": "Annuler",
|
||||
"event_deleted": "Événement supprimé",
|
||||
"event_delete_error": "Impossible de supprimer l'événement",
|
||||
"task_deleted": "Tâche supprimée",
|
||||
|
||||
@@ -3436,6 +3436,8 @@
|
||||
"notifications": {
|
||||
"event_created": "האירוע נוצר",
|
||||
"event_updated": "האירוע עודכן",
|
||||
"event_moved": "האירוע הועבר",
|
||||
"undo": "בטל",
|
||||
"event_deleted": "האירוע נמחק",
|
||||
"event_delete_error": "לא ניתן היה למחוק את האירוע",
|
||||
"task_deleted": "המשימה נמחקה",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Esemény létrehozva",
|
||||
"event_updated": "Esemény frissítve",
|
||||
"event_moved": "Esemény áthelyezve",
|
||||
"undo": "Visszavonás",
|
||||
"event_deleted": "Esemény törölve",
|
||||
"event_delete_error": "Nem sikerült törölni az eseményt",
|
||||
"task_deleted": "Feladat törölve",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Evento creato",
|
||||
"event_updated": "Evento aggiornato",
|
||||
"event_moved": "Evento spostato",
|
||||
"undo": "Annulla",
|
||||
"event_deleted": "Evento eliminato",
|
||||
"event_delete_error": "Impossibile eliminare l'evento",
|
||||
"task_deleted": "Attività eliminata",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "予定を作成しました",
|
||||
"event_updated": "予定を更新しました",
|
||||
"event_moved": "予定を移動しました",
|
||||
"undo": "元に戻す",
|
||||
"event_deleted": "予定を削除しました",
|
||||
"event_delete_error": "予定を削除できませんでした",
|
||||
"task_deleted": "タスクを削除しました",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "일정이 만들어졌어요",
|
||||
"event_updated": "일정이 업데이트되었어요",
|
||||
"event_moved": "일정을 이동했어요",
|
||||
"undo": "실행 취소",
|
||||
"event_deleted": "일정이 삭제되었어요",
|
||||
"event_delete_error": "일정을 삭제하지 못했습니다",
|
||||
"task_deleted": "할 일이 삭제되었습니다",
|
||||
|
||||
@@ -3517,6 +3517,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Pasākums izveidots",
|
||||
"event_updated": "Pasākums atjaunināts",
|
||||
"event_moved": "Pasākums pārvietots",
|
||||
"undo": "Atsaukt",
|
||||
"event_deleted": "Pasākums izdzēsts",
|
||||
"event_delete_error": "Neizdevās dzēst notikumu",
|
||||
"task_deleted": "Uzdevums izdzēsts",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Үйл явдал үүсгэсэн",
|
||||
"event_updated": "Үйл явдал шинэчлэгдсэн",
|
||||
"event_moved": "Үйл явдлыг зөөсөн",
|
||||
"undo": "Цуцлах",
|
||||
"event_deleted": "Үйл явдал устгагдсан",
|
||||
"event_delete_error": "Арга хэмжээг устгаж чадсангүй",
|
||||
"task_deleted": "Даалгавар устгагдсан",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Hendelsen er opprettet",
|
||||
"event_updated": "Hendelsen er oppdatert",
|
||||
"event_moved": "Hendelsen er flyttet",
|
||||
"undo": "Angre",
|
||||
"event_deleted": "Hendelsen er slettet",
|
||||
"event_delete_error": "Kunne ikke slette hendelsen",
|
||||
"task_deleted": "Oppgaven er slettet",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Evenement aangemaakt",
|
||||
"event_updated": "Evenement bijgewerkt",
|
||||
"event_moved": "Evenement verplaatst",
|
||||
"undo": "Ongedaan maken",
|
||||
"event_deleted": "Evenement verwijderd",
|
||||
"event_delete_error": "Kan afspraak niet verwijderen",
|
||||
"task_deleted": "Taak verwijderd",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Wydarzenie utworzono",
|
||||
"event_updated": "Wydarzenie zaktualizowano",
|
||||
"event_moved": "Wydarzenie przeniesiono",
|
||||
"undo": "Cofnij",
|
||||
"event_deleted": "Wydarzenie usunięto",
|
||||
"event_delete_error": "Nie udało się usunąć wydarzenia",
|
||||
"task_deleted": "Zadanie usunięto",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Evento criado",
|
||||
"event_updated": "Evento atualizado",
|
||||
"event_moved": "Evento movido",
|
||||
"undo": "Desfazer",
|
||||
"event_deleted": "Evento excluído",
|
||||
"event_delete_error": "Não foi possível excluir o evento",
|
||||
"task_deleted": "Tarefa excluída",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Eveniment creat",
|
||||
"event_updated": "Eveniment actualizat",
|
||||
"event_moved": "Eveniment mutat",
|
||||
"undo": "Anulează",
|
||||
"event_deleted": "Eveniment șters",
|
||||
"event_delete_error": "Evenimentul nu a putut fi șters",
|
||||
"task_deleted": "Sarcină ștearsă",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Событие создано",
|
||||
"event_updated": "Событие обновлено",
|
||||
"event_moved": "Событие перемещено",
|
||||
"undo": "Отменить",
|
||||
"event_deleted": "Событие удалено",
|
||||
"event_delete_error": "Не удалось удалить событие",
|
||||
"task_deleted": "Задача удалена",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Udalosť bola vytvorená",
|
||||
"event_updated": "Udalosť bola aktualizovaná",
|
||||
"event_moved": "Udalosť bola presunutá",
|
||||
"undo": "Späť",
|
||||
"event_deleted": "Udalosť bola odstránená",
|
||||
"event_delete_error": "Udalosť sa nepodarilo odstrániť",
|
||||
"task_deleted": "Úloha bola odstránená",
|
||||
|
||||
@@ -2858,6 +2858,8 @@
|
||||
"notifications": {
|
||||
"event_created": "สร้างกิจกรรมแล้ว",
|
||||
"event_updated": "อัปเดตกิจกรรมแล้ว",
|
||||
"event_moved": "ย้ายกิจกรรมแล้ว",
|
||||
"undo": "เลิกทำ",
|
||||
"event_deleted": "ลบกิจกรรมแล้ว",
|
||||
"event_delete_error": "ไม่สามารถลบกิจกรรมได้",
|
||||
"task_deleted": "ลบงานแล้ว",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Etkinlik oluşturuldu",
|
||||
"event_updated": "Etkinlik güncellendi",
|
||||
"event_moved": "Etkinlik taşındı",
|
||||
"undo": "Geri al",
|
||||
"event_deleted": "Etkinlik silindi",
|
||||
"event_delete_error": "Etkinlik silinemedi",
|
||||
"task_deleted": "Görev silindi",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "Подія створена",
|
||||
"event_updated": "Подію оновлено",
|
||||
"event_moved": "Подію переміщено",
|
||||
"undo": "Скасувати",
|
||||
"event_deleted": "Подію видалено",
|
||||
"event_delete_error": "Не вдалося видалити подію",
|
||||
"task_deleted": "Завдання видалено",
|
||||
|
||||
@@ -3544,6 +3544,8 @@
|
||||
"notifications": {
|
||||
"event_created": "活動已建立",
|
||||
"event_updated": "活動已更新",
|
||||
"event_moved": "活動已移動",
|
||||
"undo": "復原",
|
||||
"event_deleted": "活動已刪除",
|
||||
"event_delete_error": "無法刪除活動",
|
||||
"task_deleted": "任務已刪除",
|
||||
|
||||
@@ -3518,6 +3518,8 @@
|
||||
"notifications": {
|
||||
"event_created": "活动已创建",
|
||||
"event_updated": "活动已更新",
|
||||
"event_moved": "活动已移动",
|
||||
"undo": "撤销",
|
||||
"event_deleted": "活动已删除",
|
||||
"event_delete_error": "无法删除事件",
|
||||
"task_deleted": "任务已删除",
|
||||
|
||||
Reference in New Issue
Block a user