Skip to content

Documents: invite people to a document by email (#56) - #169

Merged
Adron merged 2 commits into
mainfrom
issue-56-document-invites
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-56-document-invites

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stacked on #168 (issue #55). Base is issue-55-list-invites, so review that one
first — this PR's own diff is just the four files below.

Hosts the invite panel built in #168 over /api/documents/{id}/invites, satisfying the
acceptance criterion that documents share one invite component with the list flow rather
than duplicating it
. The sharing model is identical and the invite payloads turned out
byte-identical, so EmailInvite, InvitePanelViewModel and Views/InvitePanel are reused
unchanged — this PR adds only a service partial, a target adapter, one factory line and one
view line.

Branch / file placement

Services/InterlinedApiClient.Documents.cs is in unmerged PR #139, so the service goes
into a new domain partial — Services/InterlinedApiClient.DocumentInvites.cs.
Partial-per-domain is this repo's stated convention, and .Documents.cs is not touched at
all. DocumentsViewModel.cs (do-not-touch) is untouched too, and there is no code-behind
change: the panel takes its target declaratively.

DocumentsView.xaml takes one functional line (plus a comment), inserted as the first
child of the existing Share card's StackPanel — no new Grid.RowDefinition, no row
renumbering:

<local:InvitePanel Kind="Document" TargetId="{Binding SelectedDocument.Id}" Margin="0,0,0,12"/>

Placing it at the top of that card is also the product-correct ordering: per
/help/documents the Share window leads with "Invite people" and offers Make public /
share links as the secondary option below.

Live verification (2026-09-16)

Probed with a bearer sync-token against a throwaway document I created and then
deleted — never the account's existing documents.

GET    /api/documents/{id}/invites          -> 200 {"invites":[]}
POST   /api/documents/{id}/invites          -> 201 {"email":"…","role":"manager",
                                                     "expiresAt":"2026-09-23T00:00:00.000Z",
                                                     "url":"https://interlinedlist.com/documents/invite/<token>"}
GET    /api/documents/{id}/invites          -> 200 {"invites":[{"email":"…","role":"manager",
                                                     "expiresAt":"2026-09-23T00:00:00.000Z",
                                                     "accepted":false,"createdAt":"…",
                                                     "token":"<token>"}]}
DELETE /api/documents/{id}/invites/{token}  -> 200 {"revoked":true}
GET    /api/documents/{id}/invites          -> 200 {"invites":[]}

Both role (probed with manager) and expiresAt (probed with a 7-day expiry) round-trip,
even though the published OpenAPI request schema for this route lists only email +
expiresAt. The payload is identical to the list route's in every field, which is what
justifies the shared wire type.

The one real difference is the ownership envelope: GET /api/documents/{id} nests the
resource under "document" where GET /api/lists/{id} uses "data", so
GetDocumentOwnerUserIdAsync reads the other key. Both payloads carry userId (confirmed
live), which is how ownership is resolved without modifying DocumentSummary — a
Models/* file I must not touch, and one that doesn't model that field.

What I deliberately did not verify

  • No invite was sent to any real address. Every probe used
    claude-probe@il-probe.invalid — .invalid is an IANA-reserved TLD that can never
    resolve, so the server's fire-and-forget invite email cannot reach a person. Each invite
    was revoked immediately after its shape was captured.
  • The claim/accept side is unverified and not implemented (GET/POST /api/documents/invite/{token}). Claiming is session-cookie-only per the API docs —
    a bearer-token native client structurally can't do it — and it needs a second verified
    account. That's the rest of epic Epic: Sharing, invites and collaboration depth #54.
  • An undocumented notify: false on the invite create is accepted (201) but not
    echoed
    , so I couldn't confirm it suppresses the email and did not expose a tick for
    it. The web's "Email this person" tick belongs to the named-person (collaborator) flow,
    where notify is documented — for an email invite the email is the delivery
    mechanism. Wiring notify into the existing AddDocumentCollaboratorAsync would mean
    editing the contended .Documents.cs, so it's left for a follow-up.

Cleanup confirmed

The throwaway document ZZ claude-probe invites 56 was deleted, and a closing
GET /api/documents returns exactly the five pre-existing documents (a-root-doc,
another-root, Bespoke_Template!!!, Social Media Campaign, Untitled). The throwaway
list from #168 was deleted too — GET /api/lists is back to just New list.

Behaviour inherited from the shared panel

Unchanged from #168, and all of it applies to documents automatically:

  • Owner-only — only the true owner can invite, re-role or remove; even an Admin
    (manager) collaborator cannot manage sharing.
    A non-owner sees a plain notice, and
    the server's 404-for-non-owner (existence is never leaked) is swallowed rather than
    shown as an error.
  • Subscriber-gated, locked not hidden — a free account sees the form visible and
    disabled with an amber Upgrade prompt. Gate is customerStatus != "free". The invitee
    never pays.
  • Revoke is not subscriber-gated — a lapsed owner can always shut off access they
    granted, so Revoke stays live while the send form is locked.
  • Roles labelled Read-only / Edit / Admin over watcher / collaborator / manager;
    expiry Never / 7 days / 30 days; pending rows show address · role · Pending-or-Accepted ·
    expiry; Copy link rebuilds the landing URL from the token (the listing returns no url).

What still needs wiring

Nothing for this issue — the card is live in the document Share panel. Remaining epic
#54 work, untouched here: the claim/resolve side (/api/{lists|documents}/invite/{token},
cookie-session-only), GET /api/lists/shared/{token}/data, and a notify toggle on the
existing per-person collaborator/watcher add.

Notes for review

  • Builds clean in both -c Debug and -c Release (-r win-x64, 0 warnings), as does
    InterlinedList.Sync.Core.
  • Strata tokens only; 4px card corners, 3px rows, 4pt spacing; no blocking native dialogs.

Closes #56

🤖 Generated with Claude Code

Hosts the invite panel built for #55 over /api/documents/{id}/invites, so both
domains share one control instead of duplicating the flow — the sharing model
is identical and the invite payloads are byte-identical, so EmailInvite and
InvitePanel are reused as-is.

Service lives in a new domain partial, Services/InterlinedApiClient.DocumentInvites.cs,
rather than in .Documents.cs — partial-per-domain is this repo's convention and
it keeps the contended file untouched.

Shapes verified live 2026-09-16 against a throwaway document, then cleaned up:

  GET    /api/documents/{id}/invites          -> 200 {"invites":[{email,role,
                                                       expiresAt,accepted,createdAt,token}]}
  POST   /api/documents/{id}/invites          -> 201 {email,role,expiresAt,url}
  DELETE /api/documents/{id}/invites/{token}  -> 200 {"revoked":true}

Role and expiresAt both round-trip (probed with role "manager" and a 7-day
expiry). The one real difference from the list side is the ownership envelope:
documents nest the payload under "document" where lists use "data", so
GetDocumentOwnerUserIdAsync reads the other key.

The panel leads the Share card rather than sitting under the share links,
matching the web's "Share window leads with Invite people; Make public is
secondary" ordering. Hosting it costs DocumentsView.xaml one line, and the
InviteTargets factory one.

Closes #56

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Adron
Adron merged commit 7914937 into main Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sharing: invite people to a document by email

1 participant