mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## What kind of change does this PR introduce? Bug fix and design-system documentation update. ## What is the current behavior? Invite acceptance failures only appear in a transient toast. ## What is the new behavior? Invite failures remain visible beside the actions. The design-system guidance now distinguishes field, action, state, and toast feedback. | Before | After | | --- | --- | | <img width="759" height="619" alt="Join Organization Supabase" src="https://github.com/user-attachments/assets/ed8e974c-5da3-477a-81da-628d3f847131" /> | <img width="741" height="768" alt="Join Organization Supabase" src="https://github.com/user-attachments/assets/4c3f6bcd-4ed9-40b2-8280-e8c8a44ecbd6" /> | ## To test With local Studio running at `http://localhost:8082`: 1. Open `apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts`. 2. At line 37, immediately inside `getOrganizationInviteStatus`, add: ```tsx return 'ready' ``` This deliberately bypasses invite lookup and account checks for the visual test. 3. Open `apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx`. 4. At line 30, change: ```tsx const [joinError, setJoinError] = useState<string>() ``` to: ```tsx const [joinError, setJoinError] = useState<string>('Invite token can only be accepted via an SSO account') ``` 5. Open `http://localhost:8082/join?token=test&slug=test` while signed in. 6. Confirm the card says **Join an organization** and shows the error below **Decline**, separated from the actions by a divider. 7. Revert both temporary edits before committing anything. ## Additional context First PR in a five-PR stack. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a new connect interstitial example showcasing an inline action-error state with clear retry guidance. - **Bug Fixes** - Invitation acceptance failures now show inline destructive feedback under “Accept invite,” keeping the button enabled for retry (and removing prior toast-based failure behavior). - Updated the invalid-invitation title to “Invalid invitation.” - Changed the “Decline” link destination to `/organizations`. - **Documentation** - Expanded Sonner toast “When to use” guidance. - Refined form and connect interstitial action-feedback patterns (inline vs toast usage). - **Tests** - Updated and added coverage for the inline error rendering and “Invalid invitation” text. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
|
|
import {
|
|
getOrganizationInviteContent,
|
|
getOrganizationInviteStatus,
|
|
type OrganizationInviteStatus,
|
|
} from './OrganizationInvite.utils'
|
|
import type { OrganizationInviteByToken } from '@/data/organization-members/organization-invitation-token-query'
|
|
import type { ResponseError } from '@/types'
|
|
|
|
const READY_INVITE: OrganizationInviteByToken = {
|
|
authorized_user: true,
|
|
email_match: true,
|
|
expired_token: false,
|
|
invite_id: 42,
|
|
organization_name: 'Acme Corp',
|
|
sso_mismatch: false,
|
|
token_does_not_exist: false,
|
|
}
|
|
|
|
const responseError = (message: string, code = 500) => ({ message, code }) as ResponseError
|
|
type StatusOverrides = Partial<Parameters<typeof getOrganizationInviteStatus>[0]>
|
|
|
|
const getStatus = (overrides: StatusOverrides = {}) =>
|
|
getOrganizationInviteStatus({
|
|
data: READY_INVITE,
|
|
error: null,
|
|
isErrorInvitation: false,
|
|
isLoadingInvitation: false,
|
|
isLoadingProfile: false,
|
|
isLoggedIn: true,
|
|
isRouterReady: true,
|
|
isSuccessInvitation: true,
|
|
profileExists: true,
|
|
...overrides,
|
|
})
|
|
|
|
describe('OrganizationInvite utils', () => {
|
|
test.each<[string, OrganizationInviteStatus, StatusOverrides]>([
|
|
['signed out when there is no current user', 'signed-out', { isLoggedIn: false }],
|
|
['loading while the profile is loading', 'loading', { isLoadingProfile: true }],
|
|
['loading while the invite is loading', 'loading', { isLoadingInvitation: true }],
|
|
[
|
|
'no longer valid for accepted or declined invites',
|
|
'no-longer-valid',
|
|
{
|
|
data: undefined,
|
|
error: responseError('Failed to retrieve organization', 401),
|
|
isErrorInvitation: true,
|
|
isSuccessInvitation: false,
|
|
},
|
|
],
|
|
[
|
|
'invalid when the API returns a missing token response',
|
|
'invalid',
|
|
{
|
|
data: { ...READY_INVITE, token_does_not_exist: true },
|
|
},
|
|
],
|
|
[
|
|
'invalid when the invite lookup 404s',
|
|
'invalid',
|
|
{
|
|
data: undefined,
|
|
error: responseError('Not Found', 404),
|
|
isErrorInvitation: true,
|
|
isSuccessInvitation: false,
|
|
},
|
|
],
|
|
[
|
|
'error for other API failures',
|
|
'error',
|
|
{
|
|
data: undefined,
|
|
error: responseError('Failed to retrieve token', 500),
|
|
isErrorInvitation: true,
|
|
isSuccessInvitation: false,
|
|
},
|
|
],
|
|
[
|
|
'expired when the token has expired',
|
|
'expired',
|
|
{
|
|
data: { ...READY_INVITE, expired_token: true },
|
|
},
|
|
],
|
|
[
|
|
'wrong account when the invite email does not match',
|
|
'wrong-account',
|
|
{
|
|
data: { ...READY_INVITE, email_match: false },
|
|
},
|
|
],
|
|
['ready when the invite can be accepted', 'ready', {}],
|
|
])('returns %s', (_name, expected, overrides) => {
|
|
expect(getStatus(overrides)).toBe(expected)
|
|
})
|
|
|
|
test.each<[OrganizationInviteStatus, string, string | undefined]>([
|
|
['signed-out', 'View invitation', 'Sign in or create an account to view this invitation'],
|
|
['ready', 'Join Acme Corp', 'You have been invited to join this Supabase organization'],
|
|
['wrong-account', 'Wrong account', undefined],
|
|
['expired', 'Invite expired', undefined],
|
|
['invalid', 'Invalid invitation', undefined],
|
|
['no-longer-valid', 'Invite no longer available', undefined],
|
|
['error', 'Unable to load invitation', undefined],
|
|
])('returns content for %s', (status, title, description) => {
|
|
expect(
|
|
getOrganizationInviteContent({
|
|
data: READY_INVITE,
|
|
isSignUpEnabled: true,
|
|
status,
|
|
})
|
|
).toEqual({ title, ...(description ? { description } : {}) })
|
|
})
|
|
|
|
test('omits sign-up copy when sign-up is disabled', () => {
|
|
expect(
|
|
getOrganizationInviteContent({
|
|
data: READY_INVITE,
|
|
isSignUpEnabled: false,
|
|
status: 'signed-out',
|
|
}).description
|
|
).toBe('Sign in to view this invitation')
|
|
})
|
|
})
|