feat(edit): make event attendees editable
Attendees were read-only (shown on the detail screen since v0.6) but the
form couldn't write them — the last read-only gap in the event model.
Add a Guests section to the create/edit form:
- Inline grouped list: each guest is a tonal card (avatar, name/email, a
tappable Required/Optional role chip, remove); the trailing card is an
inline email field — type an address, press Done, it commits. No dialog,
matching the app's inline-field input idiom. Manual adds are email-only
(names come from sync); the InlineTextField gains IME-action support.
- Reminders restyled to the same grouped-list pattern (shared
GroupedItemCard / AddActionCard), replacing the single-card blob.
Persistence (CalendarDataSource): new guests are written as plain
RELATIONSHIP_ATTENDEE / STATUS_INVITED rows — no fabricated organizer.
On edit, a dirty-checked reconcileAttendees diffs by email: drops removed
guests, inserts new ones, updates only the required/optional flag on kept
rows (preserving response status). Organizer, resources and no-email rows
are never touched. toEditForm carries only editable guests, so attendees
now ride in the edit snapshot and an external guest change trips the
conflict check.
Per the settled invitation decision: Calendula has no INTERNET and never
sends an invitation — it only writes the rows; the backend decides
delivery. The section shows honest, calendar-aware copy ("your account
may email guests when it syncs" on synced calendars, "no one is notified"
on local).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -211,12 +211,55 @@ class EventFormTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changes the form cannot write do not fake a conflict`() {
|
||||
fun `a reminder-method change the form cannot write does not fake a conflict`() {
|
||||
// The form carries reminder minutes only, not the method — so a method
|
||||
// flip the form can't express must not read as an external change.
|
||||
val loaded = detail(reminders = listOf(Reminder(10, ReminderMethod.Alert)))
|
||||
.toEditSnapshot(0L, 3_600_000L, berlin)
|
||||
val fresh = detail(reminders = listOf(Reminder(10, ReminderMethod.Email)))
|
||||
.toEditSnapshot(0L, 3_600_000L, berlin)
|
||||
assertThat(fresh).isEqualTo(loaded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an external guest change now trips the conflict check`() {
|
||||
// Attendees are writable now, so they ride in the snapshot: adding a
|
||||
// guest externally is a real conflict, not invisible.
|
||||
val loaded = detail().toEditSnapshot(0L, 3_600_000L, berlin)
|
||||
val fresh = detail(
|
||||
attendees = listOf(Attendee("Ada", "ada@example.org", AttendeeStatus.Accepted)),
|
||||
attendees = listOf(Attendee("Ada", "ada@example.org", AttendeeStatus.NeedsAction)),
|
||||
).toEditSnapshot(0L, 3_600_000L, berlin)
|
||||
assertThat(fresh).isEqualTo(loaded)
|
||||
assertThat(fresh).isNotEqualTo(loaded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toEditForm carries editable guests with name email and optional role`() {
|
||||
val prefilled = detail(
|
||||
attendees = listOf(
|
||||
Attendee("Ada", "ada@example.org", AttendeeStatus.Accepted, type = AttendeeType.Required),
|
||||
Attendee("", "bob@example.org", AttendeeStatus.NeedsAction, type = AttendeeType.Optional),
|
||||
),
|
||||
).toEditForm(beginMillis = 0L, endMillis = 3_600_000L, zone = berlin)
|
||||
assertThat(prefilled.attendees).containsExactly(
|
||||
EventAttendee(email = "ada@example.org", name = "Ada", optional = false),
|
||||
EventAttendee(email = "bob@example.org", name = "", optional = true),
|
||||
).inOrder()
|
||||
assertThat(prefilled.populatedFields()).contains(EventFormField.Attendees)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toEditForm drops the organizer, resources and guests without an email`() {
|
||||
val prefilled = detail(
|
||||
attendees = listOf(
|
||||
Attendee("Org", "org@example.org", AttendeeStatus.Accepted, AttendeeRelationship.Organizer),
|
||||
Attendee("Room A", "room@example.org", AttendeeStatus.Accepted, type = AttendeeType.Resource),
|
||||
Attendee("No address", null, AttendeeStatus.NeedsAction),
|
||||
Attendee("Ada", "ada@example.org", AttendeeStatus.NeedsAction),
|
||||
),
|
||||
).toEditForm(beginMillis = 0L, endMillis = 3_600_000L, zone = berlin)
|
||||
assertThat(prefilled.attendees).containsExactly(
|
||||
EventAttendee(email = "ada@example.org", name = "Ada", optional = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user