Add comprehensive iCal properties support to event creation modal
Enhanced the create event modal to include all major iCalendar properties: - Event status (confirmed/tentative/cancelled) - Privacy classification (public/private/confidential) - Priority levels (0-9 numeric scale) - Organizer email field - Attendees list (comma-separated emails) - Categories (comma-separated tags) - Reminder options (none to 1 week before) - Recurrence patterns (none/daily/weekly/monthly/yearly) Updated backend to parse and handle all new fields, with proper enum conversion and comma-separated list parsing. Events now generate complete iCal data with STATUS, CLASS, PRIORITY, ORGANIZER, ATTENDEE, CATEGORIES, VALARM, and RRULE properties. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
Cargo.toml
19
Cargo.toml
@@ -7,7 +7,24 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
web-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"HtmlSelectElement",
|
||||
"HtmlInputElement",
|
||||
"HtmlTextAreaElement",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"InputEvent",
|
||||
"Element",
|
||||
"Document",
|
||||
"Window",
|
||||
"Location",
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
# HTTP client for CalDAV requests
|
||||
|
||||
@@ -415,6 +415,63 @@ pub async fn create_event(
|
||||
// Generate a unique UID for the event
|
||||
let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp());
|
||||
|
||||
// Parse status
|
||||
let status = match request.status.to_lowercase().as_str() {
|
||||
"tentative" => crate::calendar::EventStatus::Tentative,
|
||||
"cancelled" => crate::calendar::EventStatus::Cancelled,
|
||||
_ => crate::calendar::EventStatus::Confirmed,
|
||||
};
|
||||
|
||||
// Parse class
|
||||
let class = match request.class.to_lowercase().as_str() {
|
||||
"private" => crate::calendar::EventClass::Private,
|
||||
"confidential" => crate::calendar::EventClass::Confidential,
|
||||
_ => crate::calendar::EventClass::Public,
|
||||
};
|
||||
|
||||
// Parse attendees (comma-separated email list)
|
||||
let attendees: Vec<String> = if request.attendees.trim().is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
request.attendees
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Parse categories (comma-separated list)
|
||||
let categories: Vec<String> = if request.categories.trim().is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
request.categories
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Parse reminders (for now, just store as a simple reminder duration)
|
||||
let reminders: Vec<chrono::Duration> = match request.reminder.to_lowercase().as_str() {
|
||||
"15min" => vec![chrono::Duration::minutes(15)],
|
||||
"30min" => vec![chrono::Duration::minutes(30)],
|
||||
"1hour" => vec![chrono::Duration::hours(1)],
|
||||
"2hours" => vec![chrono::Duration::hours(2)],
|
||||
"1day" => vec![chrono::Duration::days(1)],
|
||||
"2days" => vec![chrono::Duration::days(2)],
|
||||
"1week" => vec![chrono::Duration::weeks(1)],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// Parse recurrence (basic implementation)
|
||||
let recurrence_rule = match request.recurrence.to_lowercase().as_str() {
|
||||
"daily" => Some("FREQ=DAILY".to_string()),
|
||||
"weekly" => Some("FREQ=WEEKLY".to_string()),
|
||||
"monthly" => Some("FREQ=MONTHLY".to_string()),
|
||||
"yearly" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Create the CalendarEvent struct
|
||||
let event = crate::calendar::CalendarEvent {
|
||||
uid,
|
||||
@@ -431,17 +488,21 @@ pub async fn create_event(
|
||||
} else {
|
||||
Some(request.location.clone())
|
||||
},
|
||||
status: crate::calendar::EventStatus::Confirmed,
|
||||
class: crate::calendar::EventClass::Public,
|
||||
priority: None,
|
||||
organizer: None,
|
||||
attendees: Vec::new(),
|
||||
categories: Vec::new(),
|
||||
status,
|
||||
class,
|
||||
priority: request.priority,
|
||||
organizer: if request.organizer.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(request.organizer.clone())
|
||||
},
|
||||
attendees,
|
||||
categories,
|
||||
created: Some(chrono::Utc::now()),
|
||||
last_modified: Some(chrono::Utc::now()),
|
||||
recurrence_rule: None,
|
||||
recurrence_rule,
|
||||
all_day: request.all_day,
|
||||
reminders: Vec::new(),
|
||||
reminders,
|
||||
etag: None,
|
||||
href: None,
|
||||
calendar_path: Some(calendar_path.clone()),
|
||||
|
||||
@@ -80,6 +80,14 @@ pub struct CreateEventRequest {
|
||||
pub end_time: String, // HH:MM format
|
||||
pub location: String,
|
||||
pub all_day: bool,
|
||||
pub status: String, // confirmed, tentative, cancelled
|
||||
pub class: String, // public, private, confidential
|
||||
pub priority: Option<u8>, // 0-9 priority level
|
||||
pub organizer: String, // organizer email
|
||||
pub attendees: String, // comma-separated attendee emails
|
||||
pub categories: String, // comma-separated categories
|
||||
pub reminder: String, // reminder type
|
||||
pub recurrence: String, // recurrence type
|
||||
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
||||
}
|
||||
|
||||
|
||||
42
src/app.rs
42
src/app.rs
@@ -2,7 +2,7 @@ use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use web_sys::MouseEvent;
|
||||
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler};
|
||||
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType};
|
||||
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
||||
use chrono::NaiveDate;
|
||||
|
||||
@@ -221,6 +221,38 @@ pub fn App() -> Html {
|
||||
let end_date = event_data.end_date.format("%Y-%m-%d").to_string();
|
||||
let end_time = event_data.end_time.format("%H:%M").to_string();
|
||||
|
||||
// Convert enums to strings for backend
|
||||
let status_str = match event_data.status {
|
||||
EventStatus::Tentative => "tentative",
|
||||
EventStatus::Cancelled => "cancelled",
|
||||
_ => "confirmed",
|
||||
}.to_string();
|
||||
|
||||
let class_str = match event_data.class {
|
||||
EventClass::Private => "private",
|
||||
EventClass::Confidential => "confidential",
|
||||
_ => "public",
|
||||
}.to_string();
|
||||
|
||||
let reminder_str = match event_data.reminder {
|
||||
ReminderType::Minutes15 => "15min",
|
||||
ReminderType::Minutes30 => "30min",
|
||||
ReminderType::Hour1 => "1hour",
|
||||
ReminderType::Hours2 => "2hours",
|
||||
ReminderType::Day1 => "1day",
|
||||
ReminderType::Days2 => "2days",
|
||||
ReminderType::Week1 => "1week",
|
||||
_ => "none",
|
||||
}.to_string();
|
||||
|
||||
let recurrence_str = match event_data.recurrence {
|
||||
RecurrenceType::Daily => "daily",
|
||||
RecurrenceType::Weekly => "weekly",
|
||||
RecurrenceType::Monthly => "monthly",
|
||||
RecurrenceType::Yearly => "yearly",
|
||||
_ => "none",
|
||||
}.to_string();
|
||||
|
||||
match calendar_service.create_event(
|
||||
&token,
|
||||
&password,
|
||||
@@ -232,6 +264,14 @@ pub fn App() -> Html {
|
||||
end_time,
|
||||
event_data.location,
|
||||
event_data.all_day,
|
||||
status_str,
|
||||
class_str,
|
||||
event_data.priority,
|
||||
event_data.organizer,
|
||||
event_data.attendees,
|
||||
event_data.categories,
|
||||
reminder_str,
|
||||
recurrence_str,
|
||||
None // Let backend use first available calendar
|
||||
).await {
|
||||
Ok(_) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::{HtmlInputElement, HtmlTextAreaElement};
|
||||
use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement};
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
@@ -10,6 +10,65 @@ pub struct CreateEventModalProps {
|
||||
pub on_create: Callback<EventCreationData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum EventStatus {
|
||||
Tentative,
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for EventStatus {
|
||||
fn default() -> Self {
|
||||
EventStatus::Confirmed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum EventClass {
|
||||
Public,
|
||||
Private,
|
||||
Confidential,
|
||||
}
|
||||
|
||||
impl Default for EventClass {
|
||||
fn default() -> Self {
|
||||
EventClass::Public
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum ReminderType {
|
||||
None,
|
||||
Minutes15,
|
||||
Minutes30,
|
||||
Hour1,
|
||||
Hours2,
|
||||
Day1,
|
||||
Days2,
|
||||
Week1,
|
||||
}
|
||||
|
||||
impl Default for ReminderType {
|
||||
fn default() -> Self {
|
||||
ReminderType::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum RecurrenceType {
|
||||
None,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Yearly,
|
||||
}
|
||||
|
||||
impl Default for RecurrenceType {
|
||||
fn default() -> Self {
|
||||
RecurrenceType::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct EventCreationData {
|
||||
pub title: String,
|
||||
@@ -20,6 +79,14 @@ pub struct EventCreationData {
|
||||
pub end_time: NaiveTime,
|
||||
pub location: String,
|
||||
pub all_day: bool,
|
||||
pub status: EventStatus,
|
||||
pub class: EventClass,
|
||||
pub priority: Option<u8>,
|
||||
pub organizer: String,
|
||||
pub attendees: String, // Comma-separated list
|
||||
pub categories: String, // Comma-separated list
|
||||
pub reminder: ReminderType,
|
||||
pub recurrence: RecurrenceType,
|
||||
}
|
||||
|
||||
impl Default for EventCreationData {
|
||||
@@ -37,6 +104,14 @@ impl Default for EventCreationData {
|
||||
end_time,
|
||||
location: String::new(),
|
||||
all_day: false,
|
||||
status: EventStatus::default(),
|
||||
class: EventClass::default(),
|
||||
priority: None,
|
||||
organizer: String::new(),
|
||||
attendees: String::new(),
|
||||
categories: String::new(),
|
||||
reminder: ReminderType::default(),
|
||||
recurrence: RecurrenceType::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +184,117 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_organizer_input = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.organizer = input.value();
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_attendees_input = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
if let Some(textarea) = e.target_dyn_into::<HtmlTextAreaElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.attendees = textarea.value();
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_categories_input = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.categories = input.value();
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_status_change = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.status = match select.value().as_str() {
|
||||
"tentative" => EventStatus::Tentative,
|
||||
"cancelled" => EventStatus::Cancelled,
|
||||
_ => EventStatus::Confirmed,
|
||||
};
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_class_change = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.class = match select.value().as_str() {
|
||||
"private" => EventClass::Private,
|
||||
"confidential" => EventClass::Confidential,
|
||||
_ => EventClass::Public,
|
||||
};
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_priority_input = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.priority = input.value().parse::<u8>().ok().filter(|&p| p <= 9);
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_reminder_change = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.reminder = match select.value().as_str() {
|
||||
"15min" => ReminderType::Minutes15,
|
||||
"30min" => ReminderType::Minutes30,
|
||||
"1hour" => ReminderType::Hour1,
|
||||
"2hours" => ReminderType::Hours2,
|
||||
"1day" => ReminderType::Day1,
|
||||
"2days" => ReminderType::Days2,
|
||||
"1week" => ReminderType::Week1,
|
||||
_ => ReminderType::None,
|
||||
};
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_recurrence_change = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||
let mut data = (*event_data).clone();
|
||||
data.recurrence = match select.value().as_str() {
|
||||
"daily" => RecurrenceType::Daily,
|
||||
"weekly" => RecurrenceType::Weekly,
|
||||
"monthly" => RecurrenceType::Monthly,
|
||||
"yearly" => RecurrenceType::Yearly,
|
||||
_ => RecurrenceType::None,
|
||||
};
|
||||
event_data.set(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_start_date_change = {
|
||||
let event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
@@ -302,6 +488,119 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
||||
placeholder="Enter event location"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="event-status">{"Status"}</label>
|
||||
<select
|
||||
id="event-status"
|
||||
class="form-input"
|
||||
onchange={on_status_change}
|
||||
>
|
||||
<option value="confirmed" selected={matches!(data.status, EventStatus::Confirmed)}>{"Confirmed"}</option>
|
||||
<option value="tentative" selected={matches!(data.status, EventStatus::Tentative)}>{"Tentative"}</option>
|
||||
<option value="cancelled" selected={matches!(data.status, EventStatus::Cancelled)}>{"Cancelled"}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-class">{"Privacy"}</label>
|
||||
<select
|
||||
id="event-class"
|
||||
class="form-input"
|
||||
onchange={on_class_change}
|
||||
>
|
||||
<option value="public" selected={matches!(data.class, EventClass::Public)}>{"Public"}</option>
|
||||
<option value="private" selected={matches!(data.class, EventClass::Private)}>{"Private"}</option>
|
||||
<option value="confidential" selected={matches!(data.class, EventClass::Confidential)}>{"Confidential"}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-priority">{"Priority (0-9, optional)"}</label>
|
||||
<input
|
||||
type="number"
|
||||
id="event-priority"
|
||||
class="form-input"
|
||||
value={data.priority.map(|p| p.to_string()).unwrap_or_default()}
|
||||
oninput={on_priority_input}
|
||||
placeholder="0-9 priority level"
|
||||
min="0"
|
||||
max="9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-organizer">{"Organizer Email"}</label>
|
||||
<input
|
||||
type="email"
|
||||
id="event-organizer"
|
||||
class="form-input"
|
||||
value={data.organizer.clone()}
|
||||
oninput={on_organizer_input}
|
||||
placeholder="organizer@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-attendees">{"Attendees (comma-separated emails)"}</label>
|
||||
<textarea
|
||||
id="event-attendees"
|
||||
class="form-input"
|
||||
value={data.attendees.clone()}
|
||||
oninput={on_attendees_input}
|
||||
placeholder="attendee1@example.com, attendee2@example.com"
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-categories">{"Categories (comma-separated)"}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="event-categories"
|
||||
class="form-input"
|
||||
value={data.categories.clone()}
|
||||
oninput={on_categories_input}
|
||||
placeholder="work, meeting, personal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="event-reminder">{"Reminder"}</label>
|
||||
<select
|
||||
id="event-reminder"
|
||||
class="form-input"
|
||||
onchange={on_reminder_change}
|
||||
>
|
||||
<option value="none" selected={matches!(data.reminder, ReminderType::None)}>{"None"}</option>
|
||||
<option value="15min" selected={matches!(data.reminder, ReminderType::Minutes15)}>{"15 minutes"}</option>
|
||||
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes"}</option>
|
||||
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour"}</option>
|
||||
<option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours"}</option>
|
||||
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day"}</option>
|
||||
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days"}</option>
|
||||
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week"}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event-recurrence">{"Recurrence"}</label>
|
||||
<select
|
||||
id="event-recurrence"
|
||||
class="form-input"
|
||||
onchange={on_recurrence_change}
|
||||
>
|
||||
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"None"}</option>
|
||||
<option value="daily" selected={matches!(data.recurrence, RecurrenceType::Daily)}>{"Daily"}</option>
|
||||
<option value="weekly" selected={matches!(data.recurrence, RecurrenceType::Weekly)}>{"Weekly"}</option>
|
||||
<option value="monthly" selected={matches!(data.recurrence, RecurrenceType::Monthly)}>{"Monthly"}</option>
|
||||
<option value="yearly" selected={matches!(data.recurrence, RecurrenceType::Yearly)}>{"Yearly"}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -17,7 +17,7 @@ pub use create_calendar_modal::CreateCalendarModal;
|
||||
pub use context_menu::ContextMenu;
|
||||
pub use event_context_menu::EventContextMenu;
|
||||
pub use calendar_context_menu::CalendarContextMenu;
|
||||
pub use create_event_modal::{CreateEventModal, EventCreationData};
|
||||
pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
|
||||
pub use sidebar::Sidebar;
|
||||
pub use calendar_list_item::CalendarListItem;
|
||||
pub use route_handler::RouteHandler;
|
||||
@@ -599,6 +599,14 @@ impl CalendarService {
|
||||
end_time: String,
|
||||
location: String,
|
||||
all_day: bool,
|
||||
status: String,
|
||||
class: String,
|
||||
priority: Option<u8>,
|
||||
organizer: String,
|
||||
attendees: String,
|
||||
categories: String,
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
calendar_path: Option<String>
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
@@ -616,6 +624,14 @@ impl CalendarService {
|
||||
"end_time": end_time,
|
||||
"location": location,
|
||||
"all_day": all_day,
|
||||
"status": status,
|
||||
"class": class,
|
||||
"priority": priority,
|
||||
"organizer": organizer,
|
||||
"attendees": attendees,
|
||||
"categories": categories,
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"calendar_path": calendar_path
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user