fix(server): should direct allocate seat if workspace is not team (#12469)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added the ability for workspace owners to approve members under review, with different approval processes for team and non-team workspaces.
- **Bug Fixes**
  - Improved accuracy of workspace seat quota calculations for member management.
- **Tests**
  - Enhanced test coverage and consistency for workspace member actions, including approval and revocation scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
forehalo
2025-05-23 04:25:53 +00:00
parent f38b8fef4d
commit 7978a2545f
3 changed files with 82 additions and 68 deletions

View File

@@ -1,5 +1,6 @@
import {
acceptInviteByInviteIdMutation,
approveWorkspaceTeamMemberMutation,
createInviteLinkMutation,
getInviteInfoQuery,
getMembersByWorkspaceIdQuery,
@@ -15,16 +16,11 @@ import { Models } from '../../../models';
import { Mockers } from '../../mocks';
import { app, e2e } from '../test';
async function createTeamWorkspace(quantity = 10) {
async function createWorkspace() {
const owner = await app.create(Mockers.User);
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
await app.create(Mockers.TeamWorkspace, {
id: workspace.id,
quantity,
});
return {
owner,
@@ -33,13 +29,10 @@ async function createTeamWorkspace(quantity = 10) {
}
e2e('should invite a user', async t => {
const u2 = await app.signup();
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const { owner, workspace } = await createWorkspace();
const u2 = await app.create(Mockers.User);
await app.login(owner);
const result = await app.gql({
query: inviteByEmailsMutation,
variables: {
@@ -47,6 +40,7 @@ e2e('should invite a user', async t => {
workspaceId: workspace.id,
},
});
t.truthy(result, 'failed to invite user');
// add invitation notification job
const invitationNotification = await app.queue.waitFor(
@@ -68,7 +62,7 @@ e2e('should invite a user', async t => {
t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending);
// u2 accept invite
await app.switchUser(u2);
await app.login(u2);
await app.gql({
query: acceptInviteByInviteIdMutation,
variables: {
@@ -88,18 +82,14 @@ e2e('should invite a user', async t => {
});
e2e('should leave a workspace', async t => {
const u2 = await app.signup();
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const { owner, workspace } = await createWorkspace();
const u2 = await app.create(Mockers.User);
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: u2.id,
});
await app.switchUser(u2.id);
await app.login(u2);
const { leaveWorkspace } = await app.gql({
query: leaveWorkspaceMutation,
variables: {
@@ -137,19 +127,42 @@ e2e('should revoke a user', async t => {
t.true(revokeMember, 'failed to revoke user');
});
e2e('should revoke a user on under review', async t => {
const user = await app.signup();
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
e2e('should approve a user on under review', async t => {
const { owner, workspace } = await createWorkspace();
const user = await app.create(Mockers.User);
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: user.id,
status: WorkspaceMemberStatus.UnderReview,
});
await app.login(owner);
const { approveMember } = await app.gql({
query: approveWorkspaceTeamMemberMutation,
variables: {
workspaceId: workspace.id,
userId: user.id,
},
});
t.true(approveMember, 'failed to approve member');
t.is(
(await app.get(Models).workspaceUser.get(workspace.id, user.id))?.status,
WorkspaceMemberStatus.Accepted
);
});
e2e('should revoke a user on under review', async t => {
const { owner, workspace } = await createWorkspace();
const user = await app.create(Mockers.User);
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: user.id,
status: WorkspaceMemberStatus.UnderReview,
});
await app.login(owner);
const { revokeMember } = await app.gql({
query: revokeMemberPermissionMutation,
variables: {
@@ -174,13 +187,10 @@ e2e('should revoke a user on under review', async t => {
});
e2e('should create user if not exist', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const { owner, workspace } = await createWorkspace();
const email = faker.internet.email();
await app.login(owner);
await app.gql({
query: inviteByEmailsMutation,
variables: {
@@ -194,13 +204,10 @@ e2e('should create user if not exist', async t => {
});
e2e('should support pagination for member', async t => {
const u1 = await app.signup();
const u2 = await app.signup();
const owner = await app.signup();
const { owner, workspace } = await createWorkspace();
const u1 = await app.create(Mockers.User);
const u2 = await app.create(Mockers.User);
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: u1.id,
@@ -210,6 +217,7 @@ e2e('should support pagination for member', async t => {
userId: u2.id,
});
await app.login(owner);
let result = await app.gql({
query: getMembersByWorkspaceIdQuery,
variables: {
@@ -245,14 +253,11 @@ e2e('should support pagination for member', async t => {
});
e2e('should limit member count correctly', async t => {
const owner = await app.signup();
const { owner, workspace } = await createWorkspace();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
await Promise.all(
Array.from({ length: 10 }).map(async () => {
const user = await app.signup();
const user = await app.create(Mockers.User);
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: user.id,
@@ -260,7 +265,7 @@ e2e('should limit member count correctly', async t => {
})
);
await app.switchUser(owner);
await app.login(owner);
const result = await app.gql({
query: getMembersByWorkspaceIdQuery,
variables: {
@@ -274,11 +279,7 @@ e2e('should limit member count correctly', async t => {
});
e2e('should get invite link info with status', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const { owner, workspace } = await createWorkspace();
await app.login(owner);
const { createInviteLink } = await app.gql({
@@ -335,13 +336,9 @@ e2e('should get invite link info with status', async t => {
e2e(
'should accept invitation by link directly if status is pending',
async t => {
const owner = await app.create(Mockers.User);
const { owner, workspace } = await createWorkspace();
const member = await app.create(Mockers.User);
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
await app.login(owner);
// create a pending invitation
const invite = await app.gql({
@@ -387,7 +384,7 @@ e2e(
e2e(
'should invite by link and send review request notification below quota limit',
async t => {
const { owner, workspace } = await createTeamWorkspace();
const { owner, workspace } = await createWorkspace();
await app.login(owner);
const { createInviteLink } = await app.gql({
@@ -422,7 +419,11 @@ e2e(
e2e(
'should invite by link and send review request notification over quota limit',
async t => {
const { owner, workspace } = await createTeamWorkspace(1);
const { owner, workspace } = await createWorkspace();
await app.create(Mockers.TeamWorkspace, {
id: workspace.id,
quantity: 3,
});
await app.login(owner);
const { createInviteLink } = await app.gql({
@@ -457,10 +458,7 @@ e2e(
e2e(
'should search members by name and email support case insensitive',
async t => {
const owner = await app.create(Mockers.User);
const workspace = await app.create(Mockers.Workspace, {
owner: { id: owner.id },
});
const { owner, workspace } = await createWorkspace();
const user1 = await app.create(Mockers.User, {
name: faker.internet.displayName({ firstName: 'Lucy' }),
});

View File

@@ -173,7 +173,8 @@ export class QuotaService {
async getWorkspaceSeatQuota(workspaceId: string) {
const quota = await this.getWorkspaceQuota(workspaceId);
const memberCount = await this.models.workspaceUser.count(workspaceId);
const memberCount =
await this.models.workspaceUser.chargedCount(workspaceId);
return {
memberCount,

View File

@@ -343,22 +343,37 @@ export class WorkspaceMemberResolver {
.workspace(workspaceId)
.assert('Workspace.Users.Manage');
const isTeam = await this.models.workspace.isTeamWorkspace(workspaceId);
const role = await this.models.workspaceUser.get(workspaceId, userId);
if (role) {
if (role.status === WorkspaceMemberStatus.UnderReview) {
await this.models.workspaceUser.setStatus(
workspaceId,
userId,
WorkspaceMemberStatus.AllocatingSeat,
{
inviterId: me.id,
if (isTeam) {
await this.models.workspaceUser.setStatus(
workspaceId,
userId,
WorkspaceMemberStatus.AllocatingSeat,
{
inviterId: me.id,
}
);
} else {
const quota = await this.quota.getWorkspaceSeatQuota(workspaceId);
if (quota.memberCount >= quota.memberLimit) {
throw new NoMoreSeat({ spaceId: workspaceId });
} else {
await this.models.workspaceUser.setStatus(
workspaceId,
userId,
WorkspaceMemberStatus.Accepted
);
}
);
}
this.event.emit('workspace.members.updated', {
workspaceId,
});
await this.workspaceService.sendReviewApprovedNotification(
role.id,
me.id