diff --git a/__test__/features/Calendars/CalendarAPI.test.tsx b/__test__/features/Calendars/CalendarAPI.test.tsx index 9c3a901..47b1b0d 100644 --- a/__test__/features/Calendars/CalendarAPI.test.tsx +++ b/__test__/features/Calendars/CalendarAPI.test.tsx @@ -2,6 +2,7 @@ import { addSharedCalendar, + exportCalendar, getCalendar, getCalendars, getSecretLink, @@ -149,6 +150,20 @@ describe("Calendar API", () => { ); }); + it("get export data ", async () => { + const calLink = "/calendars/calId.json"; + (api.get as jest.Mock).mockReturnValue({ + text: jest.fn().mockResolvedValue("data"), + }); + const data = await exportCalendar(calLink); + + expect(api.get).toHaveBeenCalledWith(`dav${calLink}?export`, { + headers: { + Accept: "application/calendar", + }, + }); + }); + it("When adding a sharedCal with #default as a name a new name is sent to the back", async () => { const mockApiPost = jest.spyOn(api, "post"); diff --git a/src/components/Calendar/AccessTab.tsx b/src/components/Calendar/AccessTab.tsx index ce9e054..96c86a8 100644 --- a/src/components/Calendar/AccessTab.tsx +++ b/src/components/Calendar/AccessTab.tsx @@ -1,4 +1,5 @@ import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; import { Box, IconButton, @@ -6,13 +7,19 @@ import { Button, Typography, InputAdornment, + Backdrop, + CircularProgress, } from "@mui/material"; import { useState, useEffect } from "react"; -import { getSecretLink } from "../../features/Calendars/CalendarApi"; +import { + exportCalendar, + getSecretLink, +} from "../../features/Calendars/CalendarApi"; import { Calendars } from "../../features/Calendars/CalendarTypes"; import { FieldWithLabel } from "../Event/components/FieldWithLabel"; import { SnackbarAlert } from "../Loading/SnackBarAlert"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; +import { ErrorSnackbar } from "../Error/ErrorSnackbar"; export function AccessTab({ calendar }: { calendar: Calendars }) { const { t } = useI18n(); @@ -45,6 +52,35 @@ export function AccessTab({ calendar }: { calendar: Calendars }) { setSecretLink(newSecret.secretLink); }; + const [exportLoading, setExportLoading] = useState(false); + const [exportError, setExportError] = useState(""); + + const handleExport = async () => { + try { + setExportLoading(true); + const exportedData = await exportCalendar( + calendar.link.replace(".json", "") + ); + const blob = new Blob([exportedData], { + type: "text/calendar", + }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = `${calendar.id.split("/")[1]}.ics`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (e) { + setExportError((e as Error).message); + setExportLoading(false); + } finally { + setExportLoading(false); + } + }; + return ( <> @@ -84,24 +120,49 @@ export function AccessTab({ calendar }: { calendar: Calendars }) { ), }} /> - + + {t("calendar.secretUrlDesc")} + - - {t("calendar.secretUrlDesc")} - + + + {t("calendar.exportDesc")} + + + + + ); } diff --git a/src/features/Calendars/CalendarApi.ts b/src/features/Calendars/CalendarApi.ts index 4519227..1655f6e 100644 --- a/src/features/Calendars/CalendarApi.ts +++ b/src/features/Calendars/CalendarApi.ts @@ -139,3 +139,14 @@ export async function getSecretLink(calLink: string, reset: boolean) { .json(); return response as { secretLink: string }; } + +export async function exportCalendar(calLink: string) { + const response = await api + .get(`dav${calLink}?export`, { + headers: { + Accept: "application/calendar", + }, + }) + .text(); + return response; +} diff --git a/src/locales/en.json b/src/locales/en.json index 740c4c5..90eae1a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -7,6 +7,8 @@ "caldav_access": "CalDAV access", "secretUrl": "Secret URL", "secretUrlDesc": "The secret address can be used to view all calendar events even without access permissions. You can reset your current secret address and get a new one.", + "exportCalendar": "Export calendar", + "exportDesc": "Export your calendar to iCalendar format file (.ics) which you can import to other applications.", "import_file_description": "Import events from an ICS file to one of your calendars.", "ics_feed_url": "ICS feed URL", "import_to": "Import to", @@ -31,7 +33,9 @@ "create": "Create", "import": "Import", "add": "Add", - "reset": "Reset" + "reset": "Reset", + "export": "Export", + "exporting": "Exporting..." }, "common": { "cancel": "Cancel", diff --git a/src/locales/fr.json b/src/locales/fr.json index 9cca5c5..1a8fcaf 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -7,6 +7,8 @@ "caldav_access": "Accès CalDAV", "secretUrl": "URL secrète", "secretUrlDesc": "L'adresse secrète peut être utilisée pour visualiser tous vos calendiers même sans permissions. Vous pouvez la réinitialiser pour en obtenir une nouvelle.", + "exportCalendar": "Exporter le calendrier", + "exportDesc": "Exportez votre calendrier au format iCalendar (.ics) que vous pouvez utiliser pour l'importer dans d'autres applications.", "import_file_description": "Importer des événements depuis un fichier ICS vers l'un de vos calendriers.", "ics_feed_url": "URL du flux ICS", "import_to": "Importer vers", @@ -31,7 +33,9 @@ "create": "Créer", "import": "Importer", "add": "Ajouter", - "reset": "Réinitialiser" + "reset": "Réinitialiser", + "export": "Exporter", + "exporting": "En cours d'exportation..." }, "common": { "cancel": "Annuler",