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.3/ui.js

Large diffs are not rendered by default.

84 changes: 75 additions & 9 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,
107,798 bytes for 0.1.2) from GitHub Pages and refuses it unless it
110,466 bytes for 0.1.3) 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 All @@ -37,8 +37,10 @@ drive apps.
days by width, with a sidebar from 900px). An event opens in a drawer;
Edit changes exactly the five mapped fields and saves to the row only
("Saved here · not sent to Google yet"). "Open in Google Calendar" asks
the host to open the event's Google page. Rows edited in the host's table
show up the same way after the next sync.
the host to open the event's Google page. Rows edited outside the app
(the host's table, its Calendar view, another device) show up the same
way when the app next opens or syncs; see
[Edits made outside the app](#edits-made-outside-the-app-compare-on-open).
6. **Review and send.** "Review N changes" lists each changed field
(before → after), with Discard per event. Nothing is sent until you press
"Send N changes"; each row then reports Sent, Changed in Google (a `412`,
Expand Down Expand Up @@ -104,9 +106,10 @@ 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 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
Day and End day are derived on import and on every edit made in the app.
Edited in the host, they are read back into Start and End (see
[Edits made outside the app](#edits-made-outside-the-app-compare-on-open)).
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
Expand Down Expand Up @@ -163,6 +166,61 @@ the five fields as both sides last agreed, keyed by field (`title`,
`description`, …), not by column, so the renamed columns leave it as it
was. It is what lets a refresh tell a local edit from a Google edit.

## Edits made outside the app (compare on open)

Per #177 (Q4–Q7) and #192. The bookkeeping lives on each row, as provider
extras: `google-event-id`, `google-etag` and `sync-baseline`. The host
gives the app no change events yet (#177 H6), so the app compares instead:
every time it opens, and on "Sync now", it reads Google and compares each
synced row with its baseline. Any difference is a local change, however it
was made: the host's table, the host's Calendar view, another view, another
device. It goes into the same "Review N changes" list as an edit made in the
app, and nothing is sent until you press Send. A send is a `PATCH` with
`If-Match`, and the baseline advances only once Google confirms it.

The rows keep the host's format, so the comparison reads them back through
the lens (`hostValue` in `app/sync.ts`):

| Edited in the host | Sent to Google (after review) |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Name, Notes, Location | `summary`, `description`, `location` |
| Start, End (Day and End day left as they were) | `start`, `end` as typed; Day and End day are derived again |
| Day or End day, all-day event | `start.date` = Day, `end.date` = End day (exclusive), so Google shows the days the host view shows |
| End day cleared, all-day event | one day: `end.date` = the day after Day (the host view shows Day only) |
| Day, timed event | same clock time and offset on the new Day; End moves by the same number of days |
| End day, timed event | End on that date at its own time (Day's date when cleared) |
| All day turned on | all-day from Day to End day, or Day alone when there is no End day |
| All day turned off, Start and End still dates | **not sent**, listed: there are no times to send |
| End day on or before Day, all-day event | **not sent**, listed: the host view draws it nowhere (end == start is not read as one day here, unlike Google's) |
| Day or End day not a date | **not sent**, listed |
| Start or End and Day or End day, disagreeing | **not sent**, listed: the app can't tell which one you meant |
| A column the app doesn't map (one you added) | **never sent**; listed as "Kept here only" with how many synced events fill it |

Only Day moved, with End day left as it was, changes the length of an
all-day event, since that is what the host view then shows. A row that is
listed as not sent is held back entirely: it is neither sent nor rewritten
by the sync (a Google edit to it waits too), until it is fixed in the table
or with the app's Edit. The core Description (0.1.0's column) counts as a
column the app doesn't map: Notes is what is sent.

- **Conflicts.** A field changed both here (in any view) and in Google since
the baseline is a conflict, handled as any other (_Conflicts_ above):
neither side is overwritten until you choose per field. A Day edit counts
as a Start edit. A Google change to a different field lands in the row,
and the local change is still reviewed.
- **Deleted rows are not noticed.** A synced row deleted in the host takes
its baseline with it, so there is nothing left to compare: the next sync
imports the Google event again as a new row, and the event stays in
Google. Noticing a local deletion needs the host's per-table change list
with tombstones (#177 H6), which doesn't exist at the pin. Declared
limitation.
- **Only while the app is open.** Edits made while it is closed are found
the next time it opens; nothing is sent in the background.

Declared by unit tests (`app/compare.test.ts`: one case per row of the
table, a both-sides conflict, a deletion, a hand-added row and an unmapped
column) and one e2e step (below). Not live-verified.

## Scope and policies

Declared, not live-verified (see _Verification_):
Expand All @@ -189,7 +247,9 @@ Declared, not live-verified (see _Verification_):
- A local value that can't be sent (an empty title, an interval that isn't
valid) is held back and listed. Neither side changes.
- Rows made in the table (no Google event id) are counted, never sent:
creating events is not supported.
creating events is not supported. This is the app's answer to #177 Q6
(hand-added rows are local only unless published explicitly); there is
no "Publish to Google Calendar" yet, so for now they stay local.
- **Conditional writes.** Each approved edit is one `PATCH` of only the
changed fields, with `If-Match` set to the ETag that same preview read. A
`412` marks only that event "Changed in Google since this preview; not
Expand Down Expand Up @@ -291,7 +351,8 @@ node --test integrations/localthought/mock-proxy.test.mjs
and removed again for a timed event across midnight); recurring and
cancelled skips; refresh; review; `If-Match` on send; `412`; both-changed
conflicts; a lost response followed by a reconnect; cancellation after
import; local-only and invalid rows. `app/build.test.ts` checks that the
import; local-only and invalid rows. `app/compare.test.ts`: edits made
outside the app, found on open (see the table above). `app/build.test.ts` checks that the
bundle is one ES module with no storage, `fetch` or credential of its own.
- **Host e2e** (`e2e/calendar.spec.ts`, lane `calendar`, tier `e2e`): the
same path in the real plugin frame on the pinned host, with the mock
Expand All @@ -303,7 +364,12 @@ node --test integrations/localthought/mock-proxy.test.mjs
(`POST /fixture/google-calendar/…`). It then sends a reviewed edit
(checking the fixture received that `If-Match`), sends into a `412`, and
loses a `PATCH` response (Playwright lets the request reach the mock, then
aborts the response), syncs again, and checks that the preview agrees. A
aborts the response), syncs again, and checks that the preview agrees.
Then (#192) it sets the one-day all-day event's End day a day later as
the signed-in user (a commit, as a table edit is; not through the table's
cells), reloads the page so the app opens again, checks that the review
lists End, sends it, and checks that the fixture received a `PATCH` of
only `end.date`, with `If-Match`. A
later test follows Month into the host table, adds its Calendar view, and
checks that the fixture's three-day all-day event is drawn on the 10th,
11th and 12th (not the 9th or 13th), the one-day all-day event on its day
Expand Down
Loading
Loading