EventEditor puts an event's parts in a form: its title, whole days or hours, where, its color, how it repeats, its reminders and notes. Save waits for a title and an end after the start and hands on an EventDraft; Cancel and Delete show when the owner handles them. A new draft from the owner starts the editor over. RecurrenceEditor sets a rule after RFC 5545: daily, weekly on its days, monthly or yearly, every so many, until a day or a count. A line reads the rule back with the next days it falls on. ReminderPicker keeps reminders from a fixed list, soonest first, and Add brings the nearest one not yet set. AvailabilityPicker counts the open slots of each day and lists the chosen day's, around what is busy, inside working hours and after now. FormField filled its column with a percent width, which taffy 0.9 leaves at content width when a block measures the column first and the width comes out the same: at 280px the editor's fields shrank to their labels. It now fills with min_w_full, as new roots do; older roots move over in T44.
141 lines
5.1 KiB
Rust
141 lines
5.1 KiB
Rust
use ely_gpui_component::{
|
|
calendar::{AvailabilityPicker, EventDraft, EventEditor, Frequency, Recurrence, When},
|
|
theme::{ActiveTheme, Radius},
|
|
};
|
|
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
|
use jiff::{ToSpan, civil::Weekday};
|
|
|
|
use super::{here, plan, quiet};
|
|
use crate::{
|
|
probe::probe,
|
|
ui::{change, keep, section, set},
|
|
};
|
|
|
|
pub fn editor(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let (zone, _) = here();
|
|
let events = plan(window, cx);
|
|
let review = events
|
|
.read(cx)
|
|
.iter()
|
|
.find(|event| event.key.as_ref() == "review")
|
|
.cloned();
|
|
let said = keep("calendar-editor-said", || None::<SharedString>, window, cx);
|
|
let shown = said.read(cx).clone();
|
|
let theme = cx.theme();
|
|
let body = match review {
|
|
Some(review) => {
|
|
let (saving, cancelling, deleting) = (said.clone(), said.clone(), said);
|
|
let (kept, dropped) = (events.clone(), events);
|
|
let draft = EventDraft {
|
|
repeat: Some(Recurrence {
|
|
weekdays: vec![Weekday::Monday, Weekday::Thursday],
|
|
..Recurrence::new(Frequency::Weekly)
|
|
}),
|
|
reminders: vec![10, 1440],
|
|
..EventDraft::of(&review)
|
|
};
|
|
EventEditor::new("calendar-editor", draft)
|
|
.zone(zone)
|
|
.on_save(move |draft, _, cx| {
|
|
let draft = draft.clone();
|
|
change(&kept, cx, |events| {
|
|
let event = events
|
|
.iter_mut()
|
|
.find(|event| event.key.as_ref() == "review")
|
|
.expect("the review is listed");
|
|
event.title = draft.title.clone();
|
|
event.when = draft.when;
|
|
event.hue = draft.hue;
|
|
event.place = (!draft.place.is_empty()).then(|| draft.place.clone());
|
|
});
|
|
set(&saving, Some(format!("Saved {}.", draft.title).into()), cx)
|
|
})
|
|
.on_cancel(move |_, cx| set(&cancelling, Some("Left as it was.".into()), cx))
|
|
.on_delete(move |_, cx| {
|
|
change(&dropped, cx, |events| {
|
|
events.retain(|event| event.key.as_ref() != "review")
|
|
});
|
|
set(&deleting, Some("Deleted the review.".into()), cx)
|
|
})
|
|
.into_any_element()
|
|
}
|
|
None => quiet("The review was deleted.".into(), cx).into_any_element(),
|
|
};
|
|
section(
|
|
"EventEditor / RecurrenceEditor / ReminderPicker",
|
|
"An event's parts in a form: its title, whole days or hours, where, its color, how it repeats with the next days it falls on, its reminders and notes. Save waits for a title and an end after the start; saved, every view on the page shows the change.",
|
|
cx,
|
|
)
|
|
.child(probe(
|
|
"calendar-editor",
|
|
div()
|
|
.w(px(480.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(
|
|
div()
|
|
.p_5()
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
.rounded(theme.radius(Radius::Lg))
|
|
.child(body),
|
|
)
|
|
.children(shown.map(|shown| quiet(shown, cx))),
|
|
))
|
|
}
|
|
|
|
pub fn availability(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let (zone, today) = here();
|
|
let busy: Vec<_> = plan(window, cx)
|
|
.read(cx)
|
|
.iter()
|
|
.filter_map(|event| match event.when {
|
|
When::Timed { start, end } => Some((start, end)),
|
|
When::AllDay { .. } => None,
|
|
})
|
|
.collect();
|
|
let days: Vec<_> = (1..=9)
|
|
.map(|offset| {
|
|
today
|
|
.checked_add((offset as i64).days())
|
|
.expect("a day ahead")
|
|
})
|
|
.filter(|day| !matches!(day.weekday(), Weekday::Saturday | Weekday::Sunday))
|
|
.take(5)
|
|
.collect();
|
|
let picked = keep("calendar-slot", || None::<jiff::Timestamp>, window, cx);
|
|
let chosen = *picked.read(cx);
|
|
let said = chosen.map(|start| {
|
|
let when = start
|
|
.to_zoned(zone.clone())
|
|
.strftime("%A at %H:%M")
|
|
.to_string();
|
|
SharedString::from(format!("Meet {when}."))
|
|
});
|
|
let picker = AvailabilityPicker::new("calendar-slots", days)
|
|
.zone(zone)
|
|
.busy(busy)
|
|
.length(45)
|
|
.on_pick(move |start, _, cx| set(&picked, Some(start), cx));
|
|
let picker = match chosen {
|
|
Some(start) => picker.selected(start),
|
|
None => picker,
|
|
};
|
|
section(
|
|
"AvailabilityPicker",
|
|
"Times to meet over the working days ahead: each day with its count of open slots, then the slots of the one chosen. What the page's events take is closed.",
|
|
cx,
|
|
)
|
|
.child(probe(
|
|
"calendar-slots",
|
|
div()
|
|
.w(px(560.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(picker)
|
|
.children(said.map(|said| quiet(said, cx))),
|
|
))
|
|
}
|