Skip to content

5536 feature view accept pending invitations - #122

Draft
a-effort wants to merge 13 commits into
mainfrom
5536-feature-view-accept-pending-invitations
Draft

5536 feature view accept pending invitations#122
a-effort wants to merge 13 commits into
mainfrom
5536-feature-view-accept-pending-invitations

Conversation

@a-effort

@a-effort a-effort commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Adds the invitee-facing UI for IBM/mcp-context-forge#5536. A count chip in the Settings > Teams toolbar opens a dialog listing every pending invitation, each with Decline and Join team. Resolving one confirms in place and decrements the count behind the dialog. Once nothing is left to act on, the dialog holds the confirmation for 4 seconds and closes itself, cancelled by pointer entry.

teams.invite.mp4

Structure is one provider, one dialog, N triggers. PendingInvitationsProvider mounts in AppShell and owns the data; triggers are thin consumers of usePendingInvitations(). The fetch is gated on a mounted consumer, so pages with no trigger issue no request. MVP ships one trigger.

Blocked on IBM/mcp-context-forge#6010 and not mergeable until it lands. Two of the three routes do not exist: nothing lists the invitations addressed to the caller, and there is no decline route. Without the list there is no token, so even the working accept route is unreachable.

Resolutions are committed after the server confirms rather than optimistically, since both actions are irreversible and the dialog stays open. The inviter renders as an email address, because TeamInvitationResponse carries no name field. Roles render lowercase to match the rest of the app.

e2e/invitations.spec.ts has one test per visual state, stubbed through the api-mock fixture, and becomes the integration test under E2E_REAL_API=true once e2e/seed/seed.ts can seed an invitation. It has not been run yet, so that needs doing before this leaves draft. Nothing in CI covers appearance either, so it is worth a pass through both themes.

Two follow-ups, neither in scope. IBM/mcp-context-forge#6796 covers the untranslated role picker. And the Settings toolbar only renders once a user has a team, so someone invited to their first team never sees the chip; the last test in the spec documents it and nothing tracks it yet.

The teams table and the members dialog each hardcoded bg-yellow-500.
The invitations dialog needs the same square, so give it a token named
for the domain rather than shipping a third copy.

Value is #ffd200 from the design, not yellow-500's #eab308, so the
existing squares change shade slightly. No .dark override, for the
reason the other icon-bg tokens already state.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Transport for the invitee's own invitations, for #5536.

Two of the three routes do not exist yet and are the subject of #6010,
which also leaves the inbox path undecided. Both unknowns are confined
to this module, so settling them is a one-line change here.

TeamInvitation re-exports the generated TeamInvitationResponse rather
than restating it, so regenerating the client turns any drift between
what this assumed and what the backend ships into a compile error.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Its own bundle rather than keys in teams.json, since the components
consuming it sit outside components/teams and a later surface
elsewhere in the app should not reach into the teams bundle.

The invitation sentence is one message with named placeholders so
translators control word order, and the count uses ICU plural.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
One invitation as a sentence plus either its two actions or the
outcome that replaced them.

Accepted and declined are deliberately asymmetric, per node
5966:29865: accepted gets a check and full contrast, declined is muted
with no icon. Roles map to display names with a fallback to the raw
string, so an unrecognised role stays readable.

Renders a div rather than an li, so a surface showing one invitation
inline is not forced into list markup.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Presentational: it takes the list and callbacks and owns no fetching.

Once nothing is left to act on it holds the confirmation for 4000ms
and closes itself, reusing sonner's default duration so the app has
one dwell rather than two. Pointer entry cancels the timer outright
rather than pausing it, since a timer that resumes on pointer exit
closes at an unpredictable moment. Radix's usual pause-while-focused
signal is unusable because the dialog traps focus.

Scroll is scoped to the list rather than the dialog. DialogContent's
own overflow puts the close button inside the scroll container, so
past roughly seven invitations the X scrolls out of reach.

Resolving a row unmounts the focused button, so focus moves to the
next actionable row or to the close button, which is what the new
data-slot on the primitive is for.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Resolutions are committed after the server confirms, not
optimistically: both actions are irreversible and the dialog stays
open, so a rollback would visibly flip a row back.

Named Data to keep it distinct from the context consumer of the same
idea. An accidental second instance holds its own resolutions, which
is the divergent count the provider exists to prevent, so the two
should not be one import typo apart.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
One provider, one dialog, N triggers. Two mounted copies of a
self-contained component would hold separate resolutions, so resolving
in one would leave the other's count stale with nothing logged.

Mounted in AppShell, which an app-wide fetch would not justify on its
own. The fetch is gated on a mounted consumer instead, so a page with
no trigger issues no request. That gating is also what lets this land
ahead of #6010 with no feature flag: nothing calls the missing routes
until a trigger is rendered.

Focus return is handled here rather than by a DialogTrigger, which
would force every trigger into one dialog's subtree. The element that
opened the dialog is commonly gone by close, since resolving the last
invitation unmounts every trigger.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
The Settings > Teams trigger, hidden at zero pending.

Not yet rendered anywhere. Mounting it registers the consumer that
starts the fetch, so the Teams page wiring waits on #6010.

Takes the height and padding of the Create team button it will sit
beside rather than the design's standalone 36px chip.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Measured against the Figma frame: 8px padding on all four sides with a
content-driven height, 4px corners, and an explicit foreground on the
label, which was inheriting its colour.

The icon square is 17.5px rather than the drawn 20px. At 20px the chip
stands 38px tall, and the Settings tab row reserves 32px for its
toolbar contents, so the drawn size pushes the whole row taller.

The three outline buttons opt out of the variant's shadow-xs, which
reads as an unwanted shadow in light mode. Removing it at the variant
belongs to #35, so these join the
dead overrides that PR already sweeps up.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
The step this feature could not land earlier. Mounting the chip is what
registers a consumer, which is what enables the fetch, so this is the
only change that calls the routes IBM/mcp-context-forge#6010 has yet to
add. Everything before it merges inert.

Teams reacts to an accepted invitation through a counter in context
rather than a callback prop, so a later surface elsewhere in the app
can react independently of whichever one rendered the trigger.

e2e/invitations.spec.ts stubs the three routes through the api-mock
fixture, one test per visual state. Under E2E_REAL_API=true it becomes
the integration test, once e2e/seed/seed.ts can seed a pending
invitation for the test user.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort a-effort linked an issue Sep 11, 2026 that may be closed by this pull request
Cut the build-time narrative: which routes did not exist yet, which
alternatives were weighed, why a structure was chosen over another.
That belongs in the PR and the issues, and goes stale in the code.

What is left says what a reader needs to work with it: mount one hook
instance, the row is not an <li>, the dwell never resumes, focus return
is manual because the opener is usually gone.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Corners at 12px. The sentence and the action row lose their 16px
inset, so both sit flush with the header against the dialog's own
padding.

Roles render lowercase, matching teams.json, with the English article
inside the value: "an owner", "a member". pt-BR and es-ES carry no
article, since "como proprietário" is right and "como um proprietário"
is not.

The resolved row keeps the height of the buttons it replaces, so
accepting or declining no longer shifts the invitation below it, and
the accepted check uses the success token.

The list no longer scrolls. scrollbar-gutter-stable goes on
DialogContent, which is a scroll container through the primitive; the
property does not inherit, so it needs its own.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
The chip renders when invitations are pending and not otherwise, and
the teams list refetches when acceptedCount increments.

The refetch is what puts a newly joined team in the table, and it had
no coverage at all.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
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.

[FEATURE]: View & accept pending invitations

1 participant