Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/calendar/0.1.2/ui.js

Large diffs are not rendered by default.

56 changes: 51 additions & 5 deletions integrations/calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ drive apps.
1. **Install.** From the catalog: entry `calendar` (experimental; published but disabled pending launch: the catalog entry carries the module and its integrity with `enabled: false`, so the Integrations page does not offer it yet; the lanes' dev-server serves it enabled (`DEV_SERVER_ENABLE_APPS`), which is how the e2e installs it). Once enabled it is listed under the
Integrations page's **Drive apps**. The host downloads
`apps/calendar/<version>/ui.js` (`app/build.mjs`'s bundle, minified,
104,561 bytes for 0.1.1) from GitHub Pages and refuses it unless it
107,798 bytes for 0.1.2) from GitHub Pages and refuses it unless it
matches the entry's integrity hash (see
[Publishing a drive app](../README.md#publishing-a-drive-app)). The e2e
installs it that way, from the committed module the lane's dev-server
Expand Down Expand Up @@ -104,15 +104,59 @@ day is exclusive, like Google's all-day end:
- timed event within one day: no End day (the property is removed if an
edit makes an overnight event fit in one day).

Day and End day are derived on import and on every local edit, and never
read back, so move an event by editing Start and End. Version 0.1.0 wrote
Day and End day are derived on import and on every local edit, and are not
re-derived when Start or End is edited in the host table, so move an event
with the app's Edit (or edit Day and End day too). Version 0.1.0 wrote
the shortnames `day` and `all-day`, no End day, and Google's description to
the core Description; the host view drew its all-day and multi-day events
on their first day only. A table first imported by 0.1.0 keeps those old
Properties (0.1.1 no longer writes them) and gets the new ones on its next
sync; installs of 0.1.0 were experimental, so nothing migrates the old
values beyond that re-import.

### Which days the Agenda and Week draw an event on

From 0.1.2 the app's own views draw every row on exactly the days the host
table's Calendar view draws it (Michiel's decision on
atomic-server#1803: the data keeps the host's format, and the app's views
follow the host, not Google's UI). `app/events.ts` imports the host's
`isAllDayOnDate` from `browser/lib/src/calendar-date.ts` and applies
`CalendarView.tsx`'s bucketing to the same columns, at the pin:

| Row | Host view and app views |
| ---------------------------------------------------------- | ----------------------------------------- |
| All day, End day after Day | Day up to, not including, End day |
| All day, End day equal to or before Day, or no date | nowhere |
| All day, no End day | Day only |
| Not all day (timed), any End day | Day only, End day ignored |
| No Day (or one that doesn't start with `YYYY-MM-DD`) | nowhere |
| Made with the host view's `+` (Day, End day, no Start/End) | Day (the app draws it untimed, read-only) |

Consequences, which differ from 0.1.1 and from Google's own UI:

- A timed event that runs past midnight, or over several days, is drawn on
its Day only: the Week's block stops at 24:00, and its label and
accessible name give the real end ("22:00 to 01:30 on Friday 25
September").
- The day and the clock times of a timed event are the ones in its stored
offset (the date Day holds), not in the viewer's zone. For a viewer whose
zone has the same offset as the event nothing changes. Otherwise, an event
stored as `23:30-04:00` shows at 23:30 on its own date, as the host does,
while the drawer still gives the time in the viewer's zone and, on a
second line, in the event's own offset.
- Day and End day decide, not Start and End: a row whose Day or End day was
edited in the host table moves in the app too.

Declared by unit tests (`app/events.test.ts`, one case per row of the table,
each checked against the host's `isAllDayOnDate`); the e2e checks the host
view's days for the fixture, not the app's views in another zone.

The Google side stays as Google has it: an all-day event whose `end.date`
equals its `start.date` (Google's UI shows one day) is read as that one
day, with End day the day after (#184), both by `project()` and by the
LocalThought lens in `devonian/google-calendar/lens/projection.ts`; a
write-back sends Google the exclusive end.

Each row also carries its binding, outside the table's columns: the Google
event id, the ETag last read, and the sync baseline. The baseline is JSON of
the five fields as both sides last agreed, keyed by field (`title`,
Expand Down Expand Up @@ -231,8 +275,10 @@ node --test integrations/localthought/mock-proxy.test.mjs
- **Unit, views** (`app/view.test.ts`, jsdom; `app/controller.test.ts`,
`app/events.test.ts`, `app/contrast.test.ts`): every screen of the design
against the fake store, the banner copy for each provider status, agenda
grouping and week packing (exclusive all-day ends, midnight crossings,
viewer zone), local edits stored as offset-qualified strings, and the
grouping and week packing on the host Calendar view's days (exclusive
End day, End day == Day, missing Day or End day, midnight crossings,
events stored in another offset), local edits stored as offset-qualified
strings, and the
event tints' contrast for Google's 24 classic calendar colours.
- **Unit** (`app/sync.test.ts`, `adapter.test.ts`): the whole drive-app path
against the stateful fixture in
Expand Down
7 changes: 3 additions & 4 deletions integrations/calendar/app/agenda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function dayStrip(
): HTMLElement {
const { doc } = ctx;
const monday = mondayOf(selected);
const busy = busyDays(events, monday, 7, ctx.zone);
const busy = busyDays(events, monday, 7);

return h(
doc,
Expand Down Expand Up @@ -55,10 +55,10 @@ function row(ctx: Ctx, segment: Segment): HTMLElement {
const { doc } = ctx;
const e = segment.event;
const time = segment.allDay
? [h(doc, 'span', {}, 'All day')]
? [h(doc, 'span', {}, segment.untimed ? 'No time' : 'All day')]
: [
h(doc, 'span', {}, hhmm(segment.startMin)),
h(doc, 'small', {}, hhmm(segment.endMin)),
h(doc, 'small', {}, hhmm(segment.until?.minutes ?? segment.endMin)),
];
const second = segment.dayOf
? `Day ${segment.dayOf.n} of ${segment.dayOf.total}`
Expand Down Expand Up @@ -109,7 +109,6 @@ export function agenda(
events,
from,
Math.max(1, daysBetween(from, addDays(mondayOf(from), 7))),
ctx.zone,
);

return h(
Expand Down
31 changes: 28 additions & 3 deletions integrations/calendar/app/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
addDays,
formatOffset,
hhmm,
isDate,
longDay,
offsetAt,
shortDay,
Expand Down Expand Up @@ -40,11 +41,25 @@ export function relativeDay(ctx: Ctx, date: string, long: string): string {
return long;
}

/** `10:00 to 11:00`, or `All day`, for one segment. */
/**
* `10:00 to 11:00`, `22:00 to 01:30 on Friday 25 September`, `All day`, or
* `No time set`, for one segment.
*/
export function spoken(segment: Segment): string {
if (segment.untimed) return 'No time set';
if (segment.allDay) return 'All day';

return `${hhmm(segment.startMin)} to ${hhmm(segment.endMin)}`;
return `${hhmm(segment.startMin)} to ${endClock(segment)}`;
}

/** The end as the views label it: its own clock, and its date when later. */
export function endClock(segment: Segment, short = false): string {
if (!segment.until) return hhmm(segment.endMin);
const time = hhmm(segment.until.minutes);

return short
? `${time} ${shortDay(segment.until.date)}`
: `${time} on ${longDay(segment.until.date)}`;
}

/**
Expand All @@ -66,9 +81,19 @@ export function accessibleName(segment: Segment): string {

/** When an event is, in the viewer's zone: a day line and a time line. */
export function when(
event: Projection,
event: Projection & { day?: unknown },
zone: string,
): { day: string; time?: string; own?: string } {
// A row made in the host table can have a Day but no Start or End.
const day =
typeof event.day === 'string' && isDate(event.day.slice(0, 10))
? longDay(event.day.slice(0, 10))
: 'No date set';
if (event.allDay && (!isDate(event.start) || !isDate(event.end)))
return { day, time: 'All day' };
if (!event.allDay && (!event.start || !event.end))
return { day, time: 'No time set' };

if (event.allDay) {
const last = addDays(event.end, -1);

Expand Down
Loading
Loading