feat(core): impl team workspace (#8920)
AF-1738 AF-1735 AF-1731 AF-1721 AF-1717 AF-1736 AF-1727 AF-1719 AF-1877 UI for team workspaces : - add upgrade to team & successful upgrade page ( `/upgrade-to-team` & `/upgrade-success/team`) - update team plans on pricing page ( settings —> pricing plans ) - update reaching the usage/member limit modal - update invite member modal - update member CRUD options
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type {
|
||||
Permission,
|
||||
WorkspaceInviteLinkExpireTime,
|
||||
} from '@affine/graphql';
|
||||
import type { WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
backoffRetry,
|
||||
@@ -7,11 +11,10 @@ import {
|
||||
Entity,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
mapInto,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { exhaustMap } from 'rxjs';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../../cloud';
|
||||
import type { WorkspacePermissionStore } from '../stores/permission';
|
||||
@@ -20,6 +23,8 @@ const logger = new DebugLogger('affine:workspace-permission');
|
||||
|
||||
export class WorkspacePermission extends Entity {
|
||||
isOwner$ = new LiveData<boolean | null>(null);
|
||||
isAdmin$ = new LiveData<boolean | null>(null);
|
||||
isTeam$ = new LiveData<boolean | null>(null);
|
||||
isLoading$ = new LiveData(false);
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
@@ -34,12 +39,18 @@ export class WorkspacePermission extends Entity {
|
||||
exhaustMap(() => {
|
||||
return fromPromise(async signal => {
|
||||
if (this.workspaceService.workspace.flavour !== 'local') {
|
||||
return await this.store.fetchIsOwner(
|
||||
const info = await this.store.fetchWorkspaceInfo(
|
||||
this.workspaceService.workspace.id,
|
||||
signal
|
||||
);
|
||||
|
||||
return {
|
||||
isOwner: info.isOwner,
|
||||
isAdmin: info.isAdmin,
|
||||
isTeam: info.workspace.team,
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
return { isOwner: true, isAdmin: false, isTeam: false };
|
||||
}
|
||||
}).pipe(
|
||||
backoffRetry({
|
||||
@@ -49,7 +60,12 @@ export class WorkspacePermission extends Entity {
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
mapInto(this.isOwner$),
|
||||
mergeMap(({ isOwner, isAdmin, isTeam }) => {
|
||||
this.isAdmin$.next(isAdmin);
|
||||
this.isOwner$.next(isOwner);
|
||||
this.isTeam$.next(isTeam);
|
||||
return EMPTY;
|
||||
}),
|
||||
catchErrorInto(this.error$, error => {
|
||||
logger.error('Failed to fetch isOwner', error);
|
||||
}),
|
||||
@@ -58,4 +74,89 @@ export class WorkspacePermission extends Entity {
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
async inviteMember(
|
||||
email: string,
|
||||
permission: Permission,
|
||||
sendInviteMail?: boolean
|
||||
) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to invite members');
|
||||
}
|
||||
return await this.store.inviteMember(
|
||||
this.workspaceService.workspace.id,
|
||||
email,
|
||||
permission,
|
||||
sendInviteMail
|
||||
);
|
||||
}
|
||||
|
||||
async inviteMembers(emails: string[], sendInviteMail?: boolean) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to invite members');
|
||||
}
|
||||
return await this.store.inviteBatch(
|
||||
this.workspaceService.workspace.id,
|
||||
emails,
|
||||
sendInviteMail
|
||||
);
|
||||
}
|
||||
|
||||
async generateInviteLink(expireTime: WorkspaceInviteLinkExpireTime) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to generate invite link');
|
||||
}
|
||||
return await this.store.generateInviteLink(
|
||||
this.workspaceService.workspace.id,
|
||||
expireTime
|
||||
);
|
||||
}
|
||||
|
||||
async revokeInviteLink() {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to revoke invite link');
|
||||
}
|
||||
return await this.store.revokeInviteLink(
|
||||
this.workspaceService.workspace.id
|
||||
);
|
||||
}
|
||||
|
||||
async revokeMember(userId: string) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to revoke members');
|
||||
}
|
||||
return await this.store.revokeMemberPermission(
|
||||
this.workspaceService.workspace.id,
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
async acceptInvite(inviteId: string, sendAcceptMail?: boolean) {
|
||||
return await this.store.acceptInvite(
|
||||
this.workspaceService.workspace.id,
|
||||
inviteId,
|
||||
sendAcceptMail
|
||||
);
|
||||
}
|
||||
|
||||
async approveMember(userId: string) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to accept invite');
|
||||
}
|
||||
return await this.store.approveMember(
|
||||
this.workspaceService.workspace.id,
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
async adjustMemberPermission(userId: string, permission: Permission) {
|
||||
if (!this.isAdmin$.value) {
|
||||
throw new Error('User has no permission to adjust member permissions');
|
||||
}
|
||||
return await this.store.adjustMemberPermission(
|
||||
this.workspaceService.workspace.id,
|
||||
userId,
|
||||
permission
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { getIsOwnerQuery, leaveWorkspaceMutation } from '@affine/graphql';
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
approveWorkspaceTeamMemberMutation,
|
||||
getWorkspaceInfoQuery,
|
||||
grantWorkspaceTeamMemberMutation,
|
||||
inviteByEmailMutation,
|
||||
inviteByEmailsMutation,
|
||||
inviteLinkMutation,
|
||||
leaveWorkspaceMutation,
|
||||
type Permission,
|
||||
revokeInviteLinkMutation,
|
||||
revokeMemberPermissionMutation,
|
||||
type WorkspaceInviteLinkExpireTime,
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
export class WorkspacePermissionStore extends Store {
|
||||
@@ -7,19 +20,161 @@ export class WorkspacePermissionStore extends Store {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetchIsOwner(workspaceId: string, signal?: AbortSignal) {
|
||||
async fetchWorkspaceInfo(workspaceId: string, signal?: AbortSignal) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const isOwner = await this.workspaceServerService.server.gql({
|
||||
query: getIsOwnerQuery,
|
||||
const info = await this.workspaceServerService.server.gql({
|
||||
query: getWorkspaceInfoQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return isOwner.isOwner;
|
||||
return info;
|
||||
}
|
||||
|
||||
async inviteMember(
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
permission: Permission,
|
||||
sendInviteMail = false
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const invite = await this.workspaceServerService.server.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
email,
|
||||
permission,
|
||||
sendInviteMail,
|
||||
},
|
||||
});
|
||||
return invite.invite;
|
||||
}
|
||||
|
||||
async inviteBatch(
|
||||
workspaceId: string,
|
||||
emails: string[],
|
||||
sendInviteMail = false
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const inviteBatch = await this.workspaceServerService.server.gql({
|
||||
query: inviteByEmailsMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
emails,
|
||||
sendInviteMail,
|
||||
},
|
||||
});
|
||||
return inviteBatch.inviteBatch;
|
||||
}
|
||||
|
||||
async generateInviteLink(
|
||||
workspaceId: string,
|
||||
expireTime: WorkspaceInviteLinkExpireTime
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const inviteLink = await this.workspaceServerService.server.gql({
|
||||
query: inviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
expireTime,
|
||||
},
|
||||
});
|
||||
return inviteLink.inviteLink;
|
||||
}
|
||||
|
||||
async revokeInviteLink(workspaceId: string, signal?: AbortSignal) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const revoke = await this.workspaceServerService.server.gql({
|
||||
query: revokeInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
return revoke.revokeInviteLink;
|
||||
}
|
||||
|
||||
async revokeMemberPermission(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const revoke = await this.workspaceServerService.server.gql({
|
||||
query: revokeMemberPermissionMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
return revoke.revoke;
|
||||
}
|
||||
|
||||
async acceptInvite(
|
||||
workspaceId: string,
|
||||
inviteId: string,
|
||||
sendAcceptMail = false
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const accept = await this.workspaceServerService.server.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
inviteId,
|
||||
sendAcceptMail,
|
||||
},
|
||||
});
|
||||
return accept.acceptInviteById;
|
||||
}
|
||||
|
||||
async approveMember(workspaceId: string, userId: string) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const member = await this.workspaceServerService.server.gql({
|
||||
query: approveWorkspaceTeamMemberMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
return member.approveMember;
|
||||
}
|
||||
|
||||
async adjustMemberPermission(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
permission: Permission
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const member = await this.workspaceServerService.server.gql({
|
||||
query: grantWorkspaceTeamMemberMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
userId,
|
||||
permission,
|
||||
},
|
||||
});
|
||||
return member.grantMember;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user