Skip to content

feat(frontend)!: invite forms share one state language, and a send says what happened per address - #1677

Open
flipvh wants to merge 1 commit into
mainfrom
feat/invite-forms
Open

flipvh wants to merge 1 commit into
mainfrom
feat/invite-forms

Conversation

@flipvh

@flipvh flipvh commented Nov 1, 2026

Copy link
Copy Markdown
Member

Rebuilds the invite forms so one state language runs through chips, lists, results and pending invitations, and nothing an inviter enters disappears without a word. Mocks: https://claude.ai/artifact/MtEXikEpQ64EF1Rbnbz55r (private; concept B).

What changes

  • Modes: chooser tiles open one form with a segmented control (Email, People, Paste a list). InviteUsers owns the form, so a mode only swaps its field. The crumb, the "Click here" bulk link, the alert and the help tooltip go. Onboarding gets all modes.
  • States: every address reads as a session tile does: a dot, a colored word and a detail line. StatusTile and StatusDot share ExpandableTile's head.
  • Paste box: each line gets a dot, and anything unusual a word. Text with no address is named instead of dropped.
  • Invite button: no count. It says Invite, Remind, Invite and remind, Save (draft) or Nothing to send, and a summary line under the field carries the counts.
  • Result: a plain send still closes with a toast. Anything else gets a screen listing each address, what was not sent first.
  • Role: a segmented control with a line saying what the role may do. Its texts are new keys, role_description.<role>.
  • Pending invitations: tiles with link status, which replace the table in the sheet. The API gains linkExpiresAt.

Bugs fixed

  • A paste dropped lines without an address, with no message.
  • A system invite could reach a People mode that rendered nothing.
  • A decline that still holds counted as "Remind 1" and could end in a toast with an empty title.
  • A paste over 50 showed "Invite 60" and failed on submit. Batches now run, and a batch failing midway keeps what went out.
  • Jane Doe <jane@x.com> and quoted CSV parse the same way in every mode, through one parser.
  • Inviting on a draft no longer promises an email to an address already invited.
  • A failed check no longer shows a member as a green "New".
  • The check cache clears after an invite or a withdrawal, and resend refreshes its tile.
  • Focus stays in the field after removing a chip or row, and moves to the result summary after a send.
  • "Reject" is "Decline" everywhere.

Review notes

  • No Declined group in pending: rejected-invitations.test.ts forbids listing a declined invitation as pending, and any member may read that list.
  • linkExpiresAt is uniform: last email plus the token's lifetime, for every row, because pending-invitations.test.ts forbids telling an account from a new address. Someone with an account can still answer in the app after "Link expired".
  • select-emails.tsx moved from common/form-fields to user, since only invites use it. See the alignment.

Known limits

  • On a draft, re-inviting someone only to raise a held role now reads "Nothing to send". The API still supports it.
  • A reminder to an address mailed within the day says "gets the email again", though the backend holds that mail back. The form cannot tell beforehand.
  • "N pending" in the members bar stayed 0 on a stack with no CDC worker. This PR does not touch how the bar gets that count, and it has not been checked on a full stack.
  • The role control has not been tried with many roles on a phone.

Verification

  • pnpm check, pnpm unused, 30 backend invitation and security tests.
  • Frontend suite: 973 passed, 1 skipped. One of six repeat runs lost 44 files to a Storybook browser that failed to connect in time on a busy machine.
  • The first play in invite-users.stories.tsx timed out at the default one second in some full parallel runs and never alone. With a five second wait it passed every run that reached it, so the send was slow to start, not stuck.
  • Driven in a browser on a throwaway database before the rebase onto current main:
    • all three modes, avatar chips, and a paste with bad lines
    • fixing a near-miss in place, and focus after a remove
    • a send with the mode control locked, 60 addresses in two batches, and a batch failing midway
    • reminders, recent and old declines, a draft, and a failed check
    • the system invite, resend, light theme and phone width
  • The resend check set the row's last email in the database first, because the backend holds back a second mail within a day. It proves the tile refreshes, not that a real resend moves the stamp.
  • Not driven: the real onboarding flow (only its play) and a real phone keyboard.

Size

Kind Files Added Removed Net
Source 30 +1305 -818 +487
Docs 1 +36 0 +36
Tests 3 +109 -82 +27
JSON 2 +60 -38 +22
Generated 3 +8 -1 +7
Stories 3 +263 -310 -47
Total 42 +1781 -1249 +532
  • Source: the result screen, the pending tiles, the state views and the parser; +381 code, +63 comments, +43 blank.
  • Docs: the alignment for apps with their own roles or imports of the moved files.
  • Tests: select-emails.test.ts moved and checks a table-driven rule, that the parser accepts exactly what a submit accepts, plus a row per paste shape. base-ui-wiring.test.tsx pins how the role control names its options.
  • JSON: the state, group, summary and role texts.
  • Generated: linkExpiresAt in the SDK.

🤖 Generated with Claude Code

…ys what happened per address

Modes become a segmented control over one form, addresses read as session tiles do, and a paste names what it could not read. A send with something to explain gets a result screen, and pending invitations become tiles with link expiry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant