draftEvent.ts3.7 KBView on GitHub import type { ConferenceKey } from './conferencing';
import type { CalendarEvent } from '../types/calendar-types';
/** The composer fields that live in React state rather than on the draft itself. */
export interface ComposerFields {
summary: string;
location: string;
description: string;
colorId: string | null;
/** An RRULE, or the sentinel `'none'`. */
recurrenceRule: string;
conferenceKey=[redacted] | null;
}
/**
* Fold the composer's own fields — and optionally a new time range — onto the draft event.
*
* This exists because **the draft is the only thing that survives the composer**. Moving the
* event to a week that isn't on screen moves its block to a column the grid hasn't rendered;
* the block unmounts, the popover pinned to it goes with it, and the form comes back
* initialised from the draft. Send only the new times and it comes back holding only times —
* which is exactly how picking "next tuesday" silently cleared the title someone had typed.
*
* Guests already lived on the draft for the same reason (see `setGuests`); this is the rest.
*/
export function draftWithComposerFields(
draft: CalendarEvent,
fields: ComposerFields,
range?: { start: Date; end: Date },
): CalendarEvent {
return {
...draft,
// An empty box does not erase a title the draft arrived with (an agent proposal, a
// "schedule follow-up") — the form has simply not caught up to it yet.
summary: fields.summary || draft.summary,
location: fields.location || undefined,
description: fields.description || undefined,
colorId: fields.colorId || undefined,
recurrence: fields.recurrenceRule !== 'none' ? [fields.recurrenceRule] : undefined,
// `wantsMeetLink` is the flag every producer of a draft writes; `conferenceType` says
// WHICH. Both are read back when the form re-initialises.
wantsMeetLink: fields.conferenceKey != null || undefined,
conferenceType: fields.conferenceKey ?? undefined,
...(range
? {
start: { ...draft.start, dateTime: range.start.toISOString() },
end: { ...draft.end, dateTime: range.end.toISOString() },
}
: {}),
};
}
/**
* Does the composer hold anything the user actually put there?
*
* Times are not part of the answer: every draft is born with a start and an end, so they say
* nothing about whether the user meant to keep it. See `emptySlotAction` for what turns on it.
*/
export function hasComposerContent(fields: ComposerFields, guestCount: number): boolean {
return Boolean(
fields.summary.trim() ||
fields.location.trim() ||
fields.description.trim() ||
guestCount > 0 ||
fields.conferenceKey ||
fields.colorId ||
fields.recurrenceRule !== 'none',
);
}
/** What picking an empty slot should do to the calendar. */
export type EmptySlotAction = 'create' | 'move' | 'dismiss';
/**
* The three things picking an empty slot can mean, and how to tell them apart.
*
* With no draft open it is a new event. With one open it MOVES that draft — the grid stays
* live under the composer precisely so an event can be re-timed while it is being written.
*
* The exception is a draft with nothing in it: that click is the user abandoning it, not
* re-timing it, so it does one thing — take the empty block away. On screen the composer's
* own backdrop (see `EventComposerPopover`) usually catches that click first; this is the
* same answer for the case it cannot, when the draft sits in a week the grid has scrolled
* away from and has no block, and so no composer, to catch anything.
*/
export function emptySlotAction(args: {
hasDraft: boolean;
composerHasContent: boolean;
}): EmptySlotAction {
if (!args.hasDraft) return 'create';
if (!args.composerHasContent) return 'dismiss';
return 'move';
}