fix(ui/invite): make pending-approval admin guidance non-clickable (#6786)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies, and
humans onboard into companies via invite links.
> - Some invite types (`company_join` with
`requires_company_admin_approval`) require an admin to approve the join
request after the invitee submits it.
> - While the requester waits, the invitee sees the
`AwaitingJoinApprovalPanel` in `InviteLanding`, which describes where
the admin needs to go to approve the request.
> - That panel rendered the destination — "Company Settings → Access" —
as two clickable `<a href="/company/settings/access">` links, even
though the surrounding copy is plainly addressed to the admin ("Ask
**them** to visit ..."), not the requester.
> - First-time invitees naturally click the only underlined link on the
screen, are sent to `/company/settings/access`, hit the "No company
access" panel (they have no membership yet), and conclude the invite
flow is broken.
> - This PR removes the navigation by rendering both "Company Settings →
Access" references as plain styled text (`<p>` / `<span>`), so the
guidance stays visible but cannot be followed by the requester.
> - The benefit is that the post-submit invite experience matches the
copy's intent — guidance for the admin, not navigation for the
requester.

Fixes #6784.

## What Changed

- `ui/src/pages/InviteLanding.tsx` — In `AwaitingJoinApprovalPanel`,
replace the two `<a href={approvalUrl}>Company Settings → Access</a>`
elements with `<p>` and `<span>` containing the same text. Remove the
now-unused `approvalUrl` constant.
- `ui/src/pages/InviteLanding.test.tsx` — Update the existing "pending
approval page" test: it previously asserted two anchor tags pointing at
`/company/settings/access`; it now asserts **zero** anchors while the
text "Company Settings → Access" still appears twice (in the "Approval
page" box and inline in the "Ask them to visit ..." sentence). Renamed
the test description from "...linked access instructions" to
"...non-clickable access instructions" to reflect the contract.

## Verification

```
pnpm vitest run ui/src/pages/InviteLanding.test.tsx
```

Result: 8 / 8 pass, including the updated "shows the pending approval
page with the company icon and non-clickable access instructions" case.

Manual reproduction (master @ `242a2c2f`,
`deploymentMode=authenticated`, `bind=lan`, embedded Postgres):

1. As instance admin, generate a `company_join` invite that requires
admin approval.
2. In a fresh browser profile, open the invite link.
3. Fill **Create your account** and submit.
4. The "Request to join \<company\>" panel appears.
5. Hover the "Company Settings → Access" mentions — no underline, no
link cursor; clicking does nothing. The text is still readable and the
surrounding copy ("Ask them to visit ...") still conveys the instruction
to the requester.

Before / after screenshots: see issue #6784 — the "before" state lands
users on `/company/settings/access` which renders "No company access".
After this PR the guidance is informational only.

## Risks

Low. UI-only change confined to one function in `InviteLanding.tsx` plus
its matching test. No API contracts, routes, or data shapes are
modified. The removed `approvalUrl` constant was only referenced by the
two anchor elements.

## Model Used

- Anthropic Claude Opus 4.7 (`claude-opus-4-7`, 1M context, extended
thinking enabled).
- Tools: file editing, Bash, Playwright reproduction against a
self-hosted Paperclip instance running master @ `242a2c2f`, and the
Paperclip monorepo's own Vitest suite for verifying the test update.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (will attach in PR thread)
- [x] I have updated relevant documentation to reflect my changes (no
docs files needed updating)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
le-czs 2026-07-22 01:22:17 +08:00 committed by GitHub
parent 68ba7ccae6
commit c1f6698ec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 13 additions and 15 deletions

View File

@ -506,7 +506,7 @@ describe("InviteLandingPage", () => {
});
});
it("shows the pending approval page with the company icon and linked access instructions", async () => {
it("shows the pending approval page with the company icon and non-clickable access instructions", async () => {
acceptInviteMock.mockResolvedValue({
id: "join-1",
companyId: "company-1",
@ -553,14 +553,18 @@ describe("InviteLandingPage", () => {
expect(container.querySelector('img[alt="Acme Robotics logo"]')).not.toBeNull();
expect(container.textContent).not.toContain("http://localhost/company/settings/members");
const approvalLinks = Array.from(container.querySelectorAll("a")).filter(
// The "Company Settings → Members" guidance addresses the company admin,
// not the requester. It must render as plain text so the requester cannot
// navigate themselves to /company/settings/members — a route they have no
// permission to view, which renders a misleading "No company access"
// panel and makes the invite flow look broken. See #6784.
const approvalAnchors = Array.from(container.querySelectorAll("a")).filter(
(link) => link.textContent === "Company Settings → Members",
);
expect(approvalLinks).toHaveLength(2);
const expectedApprovalUrl = `${window.location.origin}/company/settings/members`;
for (const link of approvalLinks) {
expect(link.getAttribute("href")).toBe(expectedApprovalUrl);
}
expect(approvalAnchors).toHaveLength(0);
const approvalMentions =
container.textContent?.match(/Company Settings → Members/g) ?? [];
expect(approvalMentions).toHaveLength(2);
await act(async () => {
root.unmount();

View File

@ -160,7 +160,6 @@ function AwaitingJoinApprovalPanel({
claimApiKeyPath = null,
onboardingTextUrl = null,
}: AwaitingJoinApprovalPanelProps) {
const approvalUrl = `${window.location.origin}/company/settings/members`;
const approverLabel = invitedByUserName ?? "A company admin";
return (
@ -181,15 +180,10 @@ function AwaitingJoinApprovalPanel({
</p>
<div className="border border-zinc-800 p-3">
<p className="text-xs text-zinc-500 mb-1">Approval page</p>
<a
href={approvalUrl}
className="text-sm text-zinc-200 underline underline-offset-2 hover:text-zinc-100"
>
Company Settings → Members
</a>
<p className="text-sm text-zinc-200">Company Settings → Members</p>
</div>
<p className="text-sm text-zinc-400">
Ask them to visit <a href={approvalUrl} className="text-zinc-200 underline underline-offset-2 hover:text-zinc-100">Company Settings → Members</a> to approve your request.
Ask them to visit <span className="text-zinc-200">Company Settings → Members</span> to approve your request.
</p>
<p className="text-xs text-zinc-500">
Refresh this page after you've been approved — you'll be redirected automatically.