feat(desktop): invite members from every people picker

Merged
SA
1mo ago
+337-462

Summary

  • Every people picker in the desktop app now ends with a pinned Invite member row. On a free plan it opens the Pro upsell on Team Collaboration; on a paid plan it opens the invite dialog.
  • The "This will affect your billing" confirm step is gone. People know by now that we charge per seat.
  • Fixes a shared-dialog bug that this depends on: a dialog opened on top of another dialog closed the one underneath it.

Screenshots of every scenario: https://app.superset.sh/page/invite-from-people-pickers-dqjhc7

Why / Context

Assigning work to someone who isn't in the org yet meant leaving what you were doing to go to Settings → Organization. This adds a quick path from the place where you notice they're missing.

How It Works

  • useInviteMember() returns undefined when your role can't invite anyone, so the row doesn't render. Otherwise it returns a handler that calls gateFeature(INVITE_MEMBERS, …): free → paywall, paid → open the dialog.
  • InviteMemberDialog moved from the members settings folder to routes/_authenticated/components/. It's now mounted once in the authenticated layout and driven by stores/invite-member-dialog.ts. It reads the org, org name and role itself, so callers pass nothing. The Settings → Organization button goes through the same hook.
  • Surfaces:
    • Project lead: list row, detail sidebar, and the New project chip. ProjectLeadPicker takes onInvite, threaded from the screens alongside people, which keeps it presentational for Storybook.
    • Task assignee:
      • AssigneeMenuItems calls the hook itself. It only mounts while a menu is open, so the per-row table cell and context menu stay lazy.
      • AssigneeProperty and AssigneeCell were line-for-line copies of AssigneeMenuItems and now use it.
      • CreateTaskAssigneePicker gets the row as well.
    • Settings: Teams "Add member" (its "Invite people..." link to settings now opens the dialog) and Hosts "Add member".
  • The invite row sits outside each menu's scroll area. Otherwise the max-h-64 lists clipped it at four members.
  • useOrganizationRole() is new; useIsOrganizationOwner is now built on it.

Dialog stacking (packages/ui)

Dialog defaults to modal={false}, so focus or a click inside a dialog opened on top counted as "outside" for the dialog below and closed it. That wiped the Create task and New project drafts. DialogContent now ignores outside interactions that land inside another [data-slot="dialog-content"]. The same fix covers the paywall and alert() opened over any dialog.

Focus after a menu closes

A ContextMenu hands focus back to its trigger as it closes, which dismissed the freshly opened invite dialog. The dialog now catches focus-outside and moves focus back to the email field.

Manual QA Checklist

Driven over CDP in the dev app. Pro = Superset org; free = Satya Test Org.

  • Projects list lead cell → invite dialog opens with the email field focused
  • Project detail lead → dialog
  • New project → lead chip → dialog stacks over New project; closing it leaves the draft
  • Tasks table assignee cell → dialog
  • Right-click a task → Assignee submenu (keyboard) → dialog stays open with the email field focused
  • Task detail assignee → dialog
  • Create task → assignee → dialog stacks over it; the typed title survives Cancel; Escape closes only the top dialog
  • Teams "Add member" → dialog
  • Hosts "Add member" → dialog
  • Settings → Organization button → dialog, no billing confirm
  • Free org → Create task → assignee → Invite member → paywall on Team Collaboration, stacked; Cancel returns to the draft
  • No pointer-events: none left on <body> after closing; no new console errors
  • Actually sending an invitation from a picker (the send path itself is unchanged)

Testing

  • bun run lint, bun run typecheck (whole repo), bun run check:i18n, bunx sherif: all pass
  • bun run test: everything passes except 7 deriveSandboxCredentials tests in @superset/trpc. Those fail locally because this worktree's .env has an empty SANDBOX_GATE_SECRET, and they pass with a placeholder secret. The PR doesn't touch trpc or shared.

Design Decisions

  • @mentions and filters left out: you can't filter by or mention someone who isn't in the org yet.
  • Fix stacking in the shared DialogContent rather than in each host dialog: every stacked dialog, including the paywall and alert(), had the same bug.

Known Limitations

  • Inviting someone doesn't assign them; they become pickable after they accept.
  • settings/members/components/MembersSettings/MembersSettings.tsx isn't routed anywhere (members live under Organization settings). It's edited here only so it still typechecks.

Summary by cubic

Every people picker in the desktop app now ends with a pinned Invite member row, so you can invite someone from the place you notice they're missing instead of leaving for Settings → Organization. Free plans get the Pro upsell; paid plans open the invite dialog directly, and the per-seat billing confirmation step is removed.

Bug Fixes

  • A dialog opened on top of another no longer closes the one underneath it, which previously wiped Create task and New project drafts.
  • The invite dialog keeps focus on the email field when a closing menu hands focus back to its trigger.

Refactors

  • The invite dialog is now mounted once in the authenticated layout and driven by a shared store; callers use the new useInviteMember() hook, which returns undefined when the user's role can't invite.
  • Task sidebar and table cell assignee pickers now reuse a shared AssigneeMenuItems component instead of maintaining duplicate copies.

Summary by CodeRabbit

  • New Features

    • Added “Invite member” actions to assignee, project lead, team, and host member pickers when invitations are available.
    • Invitation access now reflects your organization role, and the invitation dialog can be opened from these menus.
    • Pending invitations are shown in organization and member settings.
  • Bug Fixes

    • Improved dialog focus behavior when opening invitations from a menu.