Merge pull request #8 from suitenumerique/visio-button
Add video conference link generation
This commit is contained in:
@@ -50,6 +50,7 @@ class EventDetails:
|
|||||||
summary: str
|
summary: str
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
location: Optional[str]
|
location: Optional[str]
|
||||||
|
url: Optional[str]
|
||||||
dtstart: datetime
|
dtstart: datetime
|
||||||
dtend: Optional[datetime]
|
dtend: Optional[datetime]
|
||||||
organizer_email: str
|
organizer_email: str
|
||||||
@@ -95,17 +96,10 @@ class ICalendarParser:
|
|||||||
# Handle multi-line values (lines starting with space/tab are continuations)
|
# Handle multi-line values (lines starting with space/tab are continuations)
|
||||||
icalendar = re.sub(r"\r?\n[ \t]", "", icalendar)
|
icalendar = re.sub(r"\r?\n[ \t]", "", icalendar)
|
||||||
|
|
||||||
pattern = rf"^{property_name}[;:](.+)$"
|
pattern = rf"^{property_name}(;[^:]*)?:(.+)$"
|
||||||
match = re.search(pattern, icalendar, re.MULTILINE | re.IGNORECASE)
|
match = re.search(pattern, icalendar, re.MULTILINE | re.IGNORECASE)
|
||||||
if match:
|
if match:
|
||||||
value = match.group(1)
|
return match.group(2).strip()
|
||||||
# Remove parameters (everything before the last colon if there are parameters)
|
|
||||||
if ";" in property_name or ":" not in value:
|
|
||||||
return value.strip()
|
|
||||||
# Handle properties with parameters like ORGANIZER;CN=Name:mailto:email
|
|
||||||
if ":" in value:
|
|
||||||
return value.split(":")[-1].strip()
|
|
||||||
return value.strip()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -205,6 +199,7 @@ class ICalendarParser:
|
|||||||
summary = cls.extract_property(vevent_block, "SUMMARY") or "(Sans titre)"
|
summary = cls.extract_property(vevent_block, "SUMMARY") or "(Sans titre)"
|
||||||
description = cls.extract_property(vevent_block, "DESCRIPTION")
|
description = cls.extract_property(vevent_block, "DESCRIPTION")
|
||||||
location = cls.extract_property(vevent_block, "LOCATION")
|
location = cls.extract_property(vevent_block, "LOCATION")
|
||||||
|
url = cls.extract_property(vevent_block, "URL")
|
||||||
|
|
||||||
# Parse dates with timezone support - from VEVENT block only
|
# Parse dates with timezone support - from VEVENT block only
|
||||||
dtstart_raw, dtstart_params = cls.extract_property_with_params(
|
dtstart_raw, dtstart_params = cls.extract_property_with_params(
|
||||||
@@ -265,6 +260,7 @@ class ICalendarParser:
|
|||||||
summary=summary,
|
summary=summary,
|
||||||
description=description,
|
description=description,
|
||||||
location=location,
|
location=location,
|
||||||
|
url=url,
|
||||||
dtstart=dtstart,
|
dtstart=dtstart,
|
||||||
dtend=dtend,
|
dtend=dtend,
|
||||||
organizer_email=organizer_email,
|
organizer_email=organizer_email,
|
||||||
|
|||||||
@@ -119,6 +119,12 @@
|
|||||||
<td>{{ event.location }}</td>
|
<td>{{ event.location }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if event.url %}
|
||||||
|
<tr>
|
||||||
|
<td>Visio</td>
|
||||||
|
<td><a href="{{ event.url }}" style="color: #0066cc;">{{ event.url }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Organisateur</td>
|
<td>Organisateur</td>
|
||||||
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Détails de l'événement
|
|||||||
Titre : {{ event.summary }}
|
Titre : {{ event.summary }}
|
||||||
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
||||||
{% if event.location %}Lieu : {{ event.location }}
|
{% if event.location %}Lieu : {{ event.location }}
|
||||||
|
{% endif %}{% if event.url %}Visio : {{ event.url }}
|
||||||
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
||||||
|
|
||||||
{% if event.description %}
|
{% if event.description %}
|
||||||
|
|||||||
@@ -114,6 +114,12 @@
|
|||||||
<td>{{ event.location }}</td>
|
<td>{{ event.location }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if event.url %}
|
||||||
|
<tr>
|
||||||
|
<td>Visio</td>
|
||||||
|
<td>{{ event.url }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Organisateur</td>
|
<td>Organisateur</td>
|
||||||
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Détails de l'événement annulé
|
|||||||
Titre : {{ event.summary }}
|
Titre : {{ event.summary }}
|
||||||
Était prévu le : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
Était prévu le : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
||||||
{% if event.location %}Lieu : {{ event.location }}
|
{% if event.location %}Lieu : {{ event.location }}
|
||||||
|
{% endif %}{% if event.url %}Visio : {{ event.url }}
|
||||||
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
||||||
|
|
||||||
Cet événement a été annulé. Vous pouvez le supprimer de votre calendrier en ouvrant le fichier .ics en pièce jointe.
|
Cet événement a été annulé. Vous pouvez le supprimer de votre calendrier en ouvrant le fichier .ics en pièce jointe.
|
||||||
|
|||||||
@@ -101,6 +101,12 @@
|
|||||||
<td>{{ event.location }}</td>
|
<td>{{ event.location }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if event.url %}
|
||||||
|
<tr>
|
||||||
|
<td>Visio</td>
|
||||||
|
<td><a href="{{ event.url }}" style="color: #28a745;">{{ event.url }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Participant</td>
|
<td>Participant</td>
|
||||||
<td>{{ attendee_display }} <{{ event.attendee_email }}></td>
|
<td>{{ attendee_display }} <{{ event.attendee_email }}></td>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Détails de l'événement
|
|||||||
Titre : {{ event.summary }}
|
Titre : {{ event.summary }}
|
||||||
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
||||||
{% if event.location %}Lieu : {{ event.location }}
|
{% if event.location %}Lieu : {{ event.location }}
|
||||||
|
{% endif %}{% if event.url %}Visio : {{ event.url }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
La réponse du participant a été enregistrée dans le fichier .ics en pièce jointe.
|
La réponse du participant a été enregistrée dans le fichier .ics en pièce jointe.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -124,6 +124,12 @@
|
|||||||
<td>{{ event.location }}</td>
|
<td>{{ event.location }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if event.url %}
|
||||||
|
<tr>
|
||||||
|
<td>Visio</td>
|
||||||
|
<td><a href="{{ event.url }}" style="color: #e65100;">{{ event.url }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Organisateur</td>
|
<td>Organisateur</td>
|
||||||
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
<td>{{ organizer_display }} <{{ event.organizer_email }}></td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Détails de l'événement mis à jour
|
|||||||
Titre : {{ event.summary }}
|
Titre : {{ event.summary }}
|
||||||
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
Quand : {{ start_date }} {{ time_str }}{% if start_date != end_date %} - {{ end_date }}{% endif %}
|
||||||
{% if event.location %}Lieu : {{ event.location }}
|
{% if event.location %}Lieu : {{ event.location }}
|
||||||
|
{% endif %}{% if event.url %}Visio : {{ event.url }}
|
||||||
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
{% endif %}Organisateur : {{ organizer_display }} <{{ event.organizer_email }}>
|
||||||
|
|
||||||
{% if event.description %}
|
{% if event.description %}
|
||||||
|
|||||||
100
src/backend/core/tests/test_calendar_invitation_service.py
Normal file
100
src/backend/core/tests/test_calendar_invitation_service.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""Tests for ICalendarParser and email template rendering."""
|
||||||
|
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.services.calendar_invitation_service import (
|
||||||
|
CalendarInvitationService,
|
||||||
|
ICalendarParser,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sample ICS with URL property
|
||||||
|
ICS_WITH_URL = """BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-123
|
||||||
|
DTSTART:20260210T140000Z
|
||||||
|
DTEND:20260210T150000Z
|
||||||
|
SUMMARY:Réunion d'équipe
|
||||||
|
DESCRIPTION:Point hebdomadaire
|
||||||
|
LOCATION:Salle 301
|
||||||
|
URL:https://visio.numerique.gouv.fr/abc-defg-hij
|
||||||
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||||
|
ATTENDEE;CN=Bob;RSVP=TRUE:mailto:bob@example.com
|
||||||
|
SEQUENCE:0
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR"""
|
||||||
|
|
||||||
|
# Sample ICS without URL property
|
||||||
|
ICS_WITHOUT_URL = """BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-456
|
||||||
|
DTSTART:20260210T140000Z
|
||||||
|
DTEND:20260210T150000Z
|
||||||
|
SUMMARY:Simple meeting
|
||||||
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||||
|
ATTENDEE;CN=Bob;RSVP=TRUE:mailto:bob@example.com
|
||||||
|
SEQUENCE:0
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestICalendarParserUrl:
|
||||||
|
"""Tests for URL property extraction in ICalendarParser."""
|
||||||
|
|
||||||
|
def test_parse_extracts_url_when_present(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITH_URL, "bob@example.com")
|
||||||
|
assert event is not None
|
||||||
|
assert event.url == "https://visio.numerique.gouv.fr/abc-defg-hij"
|
||||||
|
|
||||||
|
def test_parse_url_is_none_when_absent(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITHOUT_URL, "bob@example.com")
|
||||||
|
assert event is not None
|
||||||
|
assert event.url is None
|
||||||
|
|
||||||
|
def test_parse_preserves_other_fields_with_url(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITH_URL, "bob@example.com")
|
||||||
|
assert event is not None
|
||||||
|
assert event.summary == "Réunion d'équipe"
|
||||||
|
assert event.description == "Point hebdomadaire"
|
||||||
|
assert event.location == "Salle 301"
|
||||||
|
assert event.organizer_email == "alice@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestEmailTemplateVisioUrl:
|
||||||
|
"""Tests for visio URL rendering in email templates."""
|
||||||
|
|
||||||
|
def _build_context(self, event):
|
||||||
|
service = CalendarInvitationService()
|
||||||
|
return service._build_template_context(event, "REQUEST")
|
||||||
|
|
||||||
|
def test_invitation_html_contains_visio_link(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITH_URL, "bob@example.com")
|
||||||
|
context = self._build_context(event)
|
||||||
|
html = render_to_string("emails/calendar_invitation.html", context)
|
||||||
|
assert "https://visio.numerique.gouv.fr/abc-defg-hij" in html
|
||||||
|
assert "Visio" in html
|
||||||
|
|
||||||
|
def test_invitation_txt_contains_visio_link(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITH_URL, "bob@example.com")
|
||||||
|
context = self._build_context(event)
|
||||||
|
txt = render_to_string("emails/calendar_invitation.txt", context)
|
||||||
|
assert "https://visio.numerique.gouv.fr/abc-defg-hij" in txt
|
||||||
|
assert "Visio" in txt
|
||||||
|
|
||||||
|
def test_invitation_html_no_visio_when_absent(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITHOUT_URL, "bob@example.com")
|
||||||
|
context = self._build_context(event)
|
||||||
|
html = render_to_string("emails/calendar_invitation.html", context)
|
||||||
|
assert "Visio" not in html
|
||||||
|
|
||||||
|
def test_invitation_txt_no_visio_when_absent(self):
|
||||||
|
event = ICalendarParser.parse(ICS_WITHOUT_URL, "bob@example.com")
|
||||||
|
context = self._build_context(event)
|
||||||
|
txt = render_to_string("emails/calendar_invitation.txt", context)
|
||||||
|
assert "Visio" not in txt
|
||||||
@@ -1 +1,2 @@
|
|||||||
NEXT_PUBLIC_API_ORIGIN=
|
NEXT_PUBLIC_API_ORIGIN=
|
||||||
|
NEXT_PUBLIC_VISIO_BASE_URL=https://visio.suite.anct.gouv.fr
|
||||||
@@ -1 +1,2 @@
|
|||||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8921
|
NEXT_PUBLIC_API_ORIGIN=http://localhost:8921
|
||||||
|
NEXT_PUBLIC_VISIO_BASE_URL=https://visio.suite.anct.gouv.fr
|
||||||
|
|||||||
@@ -119,13 +119,19 @@ export const EventModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visioBaseUrl = process.env.NEXT_PUBLIC_VISIO_BASE_URL;
|
||||||
|
|
||||||
const pills = useMemo(
|
const pills = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
...(visioBaseUrl
|
||||||
id: "videoConference" as const,
|
? [
|
||||||
icon: "videocam",
|
{
|
||||||
label: t("calendar.event.sections.addVideoConference"),
|
id: "videoConference" as const,
|
||||||
},
|
icon: "videocam",
|
||||||
|
label: t("calendar.event.sections.addVideoConference"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: "location" as const,
|
id: "location" as const,
|
||||||
icon: "place",
|
icon: "place",
|
||||||
@@ -147,7 +153,7 @@ export const EventModal = ({
|
|||||||
label: t("calendar.event.attendees"),
|
label: t("calendar.event.attendees"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[t, visioBaseUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||||
import { SectionRow } from "./SectionRow";
|
import { SectionRow } from "./SectionRow";
|
||||||
|
import { generateVisioRoomId } from "./generateVisioRoomId";
|
||||||
|
|
||||||
interface VideoConferenceSectionProps {
|
interface VideoConferenceSectionProps {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -19,14 +19,12 @@ export const VideoConferenceSection = ({
|
|||||||
onToggle,
|
onToggle,
|
||||||
}: VideoConferenceSectionProps) => {
|
}: VideoConferenceSectionProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
|
||||||
|
|
||||||
const handleCreateVisio = () => {
|
const handleCreateVisio = () => {
|
||||||
// Inert for now - will integrate with La Suite API in the future
|
const baseUrl = process.env.NEXT_PUBLIC_VISIO_BASE_URL;
|
||||||
setIsCreating(true);
|
if (!baseUrl) return;
|
||||||
setTimeout(() => {
|
const roomId = generateVisioRoomId();
|
||||||
setIsCreating(false);
|
onChange(`${baseUrl}/${roomId}`);
|
||||||
}, 500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
@@ -42,15 +40,35 @@ export const VideoConferenceSection = ({
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
>
|
>
|
||||||
<Button
|
{url ? (
|
||||||
size="small"
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
color="neutral"
|
<a
|
||||||
variant="tertiary"
|
href={url}
|
||||||
onClick={handleCreateVisio}
|
target="_blank"
|
||||||
disabled={isCreating}
|
rel="noopener noreferrer"
|
||||||
>
|
style={{ wordBreak: "break-all" }}
|
||||||
{t("calendar.event.sections.createVisio")}
|
>
|
||||||
</Button>
|
{url}
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="neutral"
|
||||||
|
variant="tertiary"
|
||||||
|
icon={<span className="material-icons">close</span>}
|
||||||
|
onClick={handleRemove}
|
||||||
|
aria-label={t("calendar.event.sections.removeVisio")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="neutral"
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={handleCreateVisio}
|
||||||
|
>
|
||||||
|
{t("calendar.event.sections.createVisio")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</SectionRow>
|
</SectionRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { generateVisioRoomId } from "../generateVisioRoomId";
|
||||||
|
|
||||||
|
describe("generateVisioRoomId", () => {
|
||||||
|
it("returns a string matching the xxx-xxxx-xxx format", () => {
|
||||||
|
const id = generateVisioRoomId();
|
||||||
|
expect(id).toMatch(/^[a-z]{3}-[a-z]{4}-[a-z]{3}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates different IDs on subsequent calls", () => {
|
||||||
|
const ids = new Set(
|
||||||
|
Array.from({ length: 20 }, () => generateVisioRoomId()),
|
||||||
|
);
|
||||||
|
expect(ids.size).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has exactly 12 characters (3 + 1 + 4 + 1 + 3)", () => {
|
||||||
|
const id = generateVisioRoomId();
|
||||||
|
expect(id.length).toBe(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const randomLetters = (n: number): string =>
|
||||||
|
Array.from({ length: n }, () =>
|
||||||
|
String.fromCharCode(97 + Math.floor(Math.random() * 26)),
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
export const generateVisioRoomId = (): string =>
|
||||||
|
`${randomLetters(3)}-${randomLetters(4)}-${randomLetters(3)}`;
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
"addLocation": "Add location",
|
"addLocation": "Add location",
|
||||||
"addVideoConference": "Visio",
|
"addVideoConference": "Visio",
|
||||||
"createVisio": "Add video conference",
|
"createVisio": "Add video conference",
|
||||||
|
"removeVisio": "Remove video conference",
|
||||||
"videoLink": "Video conference link",
|
"videoLink": "Video conference link",
|
||||||
"addAttendees": "Add participants",
|
"addAttendees": "Add participants",
|
||||||
"addDescription": "Add description",
|
"addDescription": "Add description",
|
||||||
@@ -747,6 +748,7 @@
|
|||||||
"addLocation": "Ajouter un lieu",
|
"addLocation": "Ajouter un lieu",
|
||||||
"addVideoConference": "Visio",
|
"addVideoConference": "Visio",
|
||||||
"createVisio": "Ajouter une visioconférence",
|
"createVisio": "Ajouter une visioconférence",
|
||||||
|
"removeVisio": "Supprimer la visioconférence",
|
||||||
"videoLink": "Lien de visioconférence",
|
"videoLink": "Lien de visioconférence",
|
||||||
"addAttendees": "Ajouter des participants",
|
"addAttendees": "Ajouter des participants",
|
||||||
"addDescription": "Ajouter une description",
|
"addDescription": "Ajouter une description",
|
||||||
@@ -1104,6 +1106,7 @@
|
|||||||
"addLocation": "Locatie toevoegen",
|
"addLocation": "Locatie toevoegen",
|
||||||
"addVideoConference": "Visio",
|
"addVideoConference": "Visio",
|
||||||
"createVisio": "Add video conference",
|
"createVisio": "Add video conference",
|
||||||
|
"removeVisio": "Videoconferentie verwijderen",
|
||||||
"videoLink": "Videoconferentie link",
|
"videoLink": "Videoconferentie link",
|
||||||
"addAttendees": "Deelnemers toevoegen",
|
"addAttendees": "Deelnemers toevoegen",
|
||||||
"addDescription": "Beschrijving toevoegen",
|
"addDescription": "Beschrijving toevoegen",
|
||||||
|
|||||||
Reference in New Issue
Block a user