feat(server): send review request when team member count over quota limit (#11126)

This commit is contained in:
fengmk2
2025-03-25 06:51:26 +00:00
parent 36eb4991c9
commit 9bad6fa12d
3 changed files with 134 additions and 33 deletions

View File

@@ -0,0 +1,95 @@
import {
acceptInviteByInviteIdMutation,
createInviteLinkMutation,
WorkspaceInviteLinkExpireTime,
} from '@affine/graphql';
import { Mockers } from '../../mocks';
import { app, e2e } from '../test';
async function createTeamWorkspace(quantity = 10) {
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,
workspace,
};
}
e2e(
'should invite by link and send review request notification below quota limit',
async t => {
const { owner, workspace } = await createTeamWorkspace();
await app.login(owner);
const { createInviteLink } = await app.gql({
query: createInviteLinkMutation,
variables: {
workspaceId: workspace.id,
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
},
});
t.truthy(createInviteLink, 'failed to create invite link');
const link = createInviteLink.link;
const inviteId = link.split('/').pop()!;
// accept invite by link
await app.signup();
const result = await app.gql({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId: workspace.id,
inviteId,
},
});
t.truthy(result, 'failed to accept invite');
const notification = app.queue.last(
'notification.sendInvitationReviewRequest'
);
t.is(notification.payload.reviewerId, owner.id);
t.truthy(notification.payload.inviteId);
}
);
e2e(
'should invite by link and send review request notification over quota limit',
async t => {
const { owner, workspace } = await createTeamWorkspace(1);
await app.login(owner);
const { createInviteLink } = await app.gql({
query: createInviteLinkMutation,
variables: {
workspaceId: workspace.id,
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
},
});
t.truthy(createInviteLink, 'failed to create invite link');
const link = createInviteLink.link;
const inviteId = link.split('/').pop()!;
// accept invite by link
await app.signup();
const result = await app.gql({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId: workspace.id,
inviteId,
},
});
t.truthy(result, 'failed to accept invite');
const notification = app.queue.last(
'notification.sendInvitationReviewRequest'
);
t.is(notification.payload.reviewerId, owner.id);
t.truthy(notification.payload.inviteId);
}
);

View File

@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
import { Feature } from '../../models';
import { Feature, FeatureType } from '../../models';
import { Mocker } from './factory';
interface MockTeamWorkspaceInput {
@@ -46,6 +46,8 @@ export class MockTeamWorkspace extends Mocker<
featureId: feature.id,
reason: 'test',
activated: true,
name: Feature.TeamPlan,
type: FeatureType.Quota,
configs: {
memberLimit: quantity,
},

View File

@@ -610,38 +610,8 @@ export class WorkspaceResolver {
`workspace:inviteLink:${workspaceId}`
);
if (invite?.inviteId === inviteId) {
const seatAvailable = await this.quota.tryCheckSeat(workspaceId);
if (seatAvailable) {
const invite = await this.models.workspaceUser.set(
workspaceId,
user.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview
);
await this.workspaceService.sendReviewRequestNotification(invite.id);
return true;
} else {
const isTeam =
await this.workspaceService.isTeamWorkspace(workspaceId);
// only team workspace allow over limit
if (isTeam) {
await this.models.workspaceUser.set(
workspaceId,
user.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview
);
const memberCount =
await this.models.workspaceUser.count(workspaceId);
this.event.emit('workspace.members.updated', {
workspaceId,
count: memberCount,
});
return true;
} else {
throw new MemberQuotaExceeded();
}
}
await this.acceptInviteByLink(user, workspaceId);
return true;
}
}
@@ -650,6 +620,40 @@ export class WorkspaceResolver {
return true;
}
private async acceptInviteByLink(user: CurrentUser, workspaceId: string) {
const seatAvailable = await this.quota.tryCheckSeat(workspaceId);
if (seatAvailable) {
const role = await this.models.workspaceUser.set(
workspaceId,
user.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview
);
await this.workspaceService.sendReviewRequestNotification(role.id);
return;
}
const isTeam = await this.workspaceService.isTeamWorkspace(workspaceId);
// only team workspace allow over limit
if (isTeam) {
const role = await this.models.workspaceUser.set(
workspaceId,
user.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview
);
await this.workspaceService.sendReviewRequestNotification(role.id);
const memberCount = await this.models.workspaceUser.count(workspaceId);
this.event.emit('workspace.members.updated', {
workspaceId,
count: memberCount,
});
return;
}
throw new MemberQuotaExceeded();
}
@Mutation(() => Boolean)
async leaveWorkspace(
@CurrentUser() user: CurrentUser,