mirror of
https://github.com/supabase/supabase.git
synced 2026-06-12 00:01:19 +08:00
## What kind of change does this PR introduce? Feature that resolves DEPR-321. ## What is the current behavior? From @dshukertjr: > Users of free and pro orgs frequently struggle to figure out how to add teammates to a project. ## What is the new behavior? Project Settings → General now has a Project access section showing who can access the project (email + role), with a link to team management (_Manage members_ or _View team_ for limited users). Large member lists are truncated with +N more. | Permutations | | --- | | <img width="1462" height="544" alt="CleanShot 2026-03-06 at 17 00 00@2x" src="https://github.com/user-attachments/assets/68e5519f-3851-4ab8-a364-9fcb222fbcb7" /> | | <img width="1486" height="676" alt="CleanShot 2026-03-06 at 16 59 48@2x" src="https://github.com/user-attachments/assets/e1b85bb5-6fbd-46ec-9b13-15501defd030" /> | | <img width="1464" height="566" alt="CleanShot 2026-03-06 at 16 59 34@2x" src="https://github.com/user-attachments/assets/e9fdc188-cf79-4af9-8b3c-313e98256109" /> | | <img width="1438" height="654" alt="CleanShot 2026-03-06 at 17 11 25@2x-83D06149-E4AE-4AC0-98D9-FBBE10A58C8C" src="https://github.com/user-attachments/assets/8a6e1aa2-76bb-486e-999a-1df4f88c3692" /> | ## Additional context Behaviour is based on user role visibility, not a special plan-only toggle. Team+ orgs are more likely to hit limited/project-scoped cases; free/pro are usually org-wide access.
199 lines
5.1 KiB
TypeScript
199 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { summarizeProjectAccess } from './General.utils'
|
|
|
|
const roles = {
|
|
org_scoped_roles: [
|
|
{
|
|
id: 1,
|
|
name: 'Owner',
|
|
description: null,
|
|
base_role_id: 1,
|
|
projects: [],
|
|
},
|
|
],
|
|
project_scoped_roles: [
|
|
{
|
|
id: 2,
|
|
name: 'Developer',
|
|
description: null,
|
|
base_role_id: 3,
|
|
projects: [{ name: 'Project A', ref: 'ref-a' }],
|
|
},
|
|
],
|
|
} as any
|
|
|
|
describe('summarizeProjectAccess', () => {
|
|
it('includes org-scoped members for every project', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: 'owner-id',
|
|
username: 'Owner User',
|
|
primary_email: 'owner@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
],
|
|
roles,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: false,
|
|
})
|
|
|
|
expect(summary.projectMemberCount).toBe(1)
|
|
expect(summary.projectMembers[0].email).toBe('owner@example.com')
|
|
expect(summary.projectMembers[0].role).toBe('Owner')
|
|
expect(summary.hasOrganizationWideAccess).toBe(true)
|
|
})
|
|
|
|
it('filters project-scoped members to the selected project', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: 'dev-visible',
|
|
username: 'Dev Visible',
|
|
primary_email: 'visible@example.com',
|
|
role_ids: [2],
|
|
} as any,
|
|
{
|
|
gotrue_id: 'dev-hidden',
|
|
username: 'Dev Hidden',
|
|
primary_email: 'hidden@example.com',
|
|
role_ids: [3],
|
|
} as any,
|
|
],
|
|
roles: {
|
|
...roles,
|
|
project_scoped_roles: [
|
|
{
|
|
...roles.project_scoped_roles[0],
|
|
projects: [{ name: 'Project A', ref: 'ref-a' }],
|
|
},
|
|
{
|
|
...roles.project_scoped_roles[0],
|
|
id: 3,
|
|
projects: [{ name: 'Project B', ref: 'ref-b' }],
|
|
},
|
|
],
|
|
} as any,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: false,
|
|
})
|
|
|
|
expect(summary.projectMemberCount).toBe(1)
|
|
expect(summary.projectMembers[0].email).toBe('visible@example.com')
|
|
})
|
|
|
|
it('excludes invited members', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: 'member-id',
|
|
username: 'Member',
|
|
primary_email: 'member@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
{
|
|
gotrue_id: 'invite-id',
|
|
username: 'invite',
|
|
primary_email: 'invite@example.com',
|
|
role_ids: [1],
|
|
invited_id: 123,
|
|
} as any,
|
|
],
|
|
roles,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: false,
|
|
})
|
|
|
|
expect(summary.organizationMemberCount).toBe(1)
|
|
expect(summary.projectMemberCount).toBe(1)
|
|
})
|
|
|
|
it('does not show org comparison in limited-visibility mode', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: 'member-id',
|
|
username: 'Member',
|
|
primary_email: 'member@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
],
|
|
roles,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: true,
|
|
})
|
|
|
|
expect(summary.shouldShowOrgComparison).toBe(false)
|
|
expect(summary.hasOrganizationWideAccess).toBe(false)
|
|
})
|
|
|
|
it('caps visible members and tracks hidden count', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: '1',
|
|
username: 'A',
|
|
primary_email: 'a@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
{
|
|
gotrue_id: '2',
|
|
username: 'B',
|
|
primary_email: 'b@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
],
|
|
roles,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: false,
|
|
maxVisibleMembers: 1,
|
|
})
|
|
|
|
expect(summary.projectMemberCount).toBe(2)
|
|
expect(summary.visibleMembers).toHaveLength(1)
|
|
expect(summary.hiddenMembersCount).toBe(1)
|
|
})
|
|
|
|
it('places current user at the top of project members', () => {
|
|
const summary = summarizeProjectAccess({
|
|
organizationMembers: [
|
|
{
|
|
gotrue_id: 'alpha-id',
|
|
username: 'Alpha',
|
|
primary_email: 'alpha@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
{
|
|
gotrue_id: 'current-user-id',
|
|
username: 'Current User',
|
|
primary_email: 'zeta@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
{
|
|
gotrue_id: 'beta-id',
|
|
username: 'Beta',
|
|
primary_email: 'beta@example.com',
|
|
role_ids: [1],
|
|
} as any,
|
|
],
|
|
roles,
|
|
projectRef: 'ref-a',
|
|
hasLimitedVisibility: false,
|
|
currentUserId: 'current-user-id',
|
|
maxVisibleMembers: 2,
|
|
})
|
|
|
|
expect(summary.projectMembers.map((member) => member.id)).toEqual([
|
|
'current-user-id',
|
|
'alpha-id',
|
|
'beta-id',
|
|
])
|
|
expect(summary.visibleMembers.map((member) => member.id)).toEqual([
|
|
'current-user-id',
|
|
'alpha-id',
|
|
])
|
|
expect(summary.hiddenMembersCount).toBe(1)
|
|
})
|
|
})
|