feat(core): add doc grant feature to share menu (#9672)
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
export type { Member } from './entities/members';
|
||||
export {
|
||||
DocGrantedUsersService,
|
||||
type GrantedUser,
|
||||
} from './services/doc-granted-users';
|
||||
export { MemberSearchService } from './services/member-search';
|
||||
export { WorkspaceMembersService } from './services/members';
|
||||
export { WorkspacePermissionService } from './services/permission';
|
||||
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { DocScope, DocService } from '../doc';
|
||||
import {
|
||||
WorkspaceScope,
|
||||
WorkspaceService,
|
||||
@@ -12,8 +18,12 @@ import {
|
||||
} from '../workspace';
|
||||
import { WorkspaceMembers } from './entities/members';
|
||||
import { WorkspacePermission } from './entities/permission';
|
||||
import { DocGrantedUsersService } from './services/doc-granted-users';
|
||||
import { MemberSearchService } from './services/member-search';
|
||||
import { WorkspaceMembersService } from './services/members';
|
||||
import { WorkspacePermissionService } from './services/permission';
|
||||
import { DocGrantedUsersStore } from './stores/doc-granted-users';
|
||||
import { MemberSearchStore } from './stores/member-search';
|
||||
import { WorkspaceMembersStore } from './stores/members';
|
||||
import { WorkspacePermissionStore } from './stores/permission';
|
||||
|
||||
@@ -29,5 +39,17 @@ export function configurePermissionsModule(framework: Framework) {
|
||||
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
||||
.service(WorkspaceMembersService, [WorkspaceMembersStore, WorkspaceService])
|
||||
.store(WorkspaceMembersStore, [WorkspaceServerService])
|
||||
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]);
|
||||
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService])
|
||||
.service(MemberSearchService, [MemberSearchStore, WorkspaceService])
|
||||
.store(MemberSearchStore, [WorkspaceServerService]);
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.scope(DocScope)
|
||||
.service(DocGrantedUsersService, [
|
||||
DocGrantedUsersStore,
|
||||
WorkspaceService,
|
||||
DocService,
|
||||
])
|
||||
.store(DocGrantedUsersStore, [WorkspaceServerService]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { DocRole, GetPageGrantedUsersListQuery } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../../cloud';
|
||||
import type { DocService } from '../../doc';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocGrantedUsersStore } from '../stores/doc-granted-users';
|
||||
|
||||
export type GrantedUser =
|
||||
GetPageGrantedUsersListQuery['workspace']['doc']['grantedUsersList']['edges'][number]['node'];
|
||||
|
||||
export class DocGrantedUsersService extends Service {
|
||||
constructor(
|
||||
private readonly store: DocGrantedUsersStore,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly docService: DocService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
readonly PAGE_SIZE = 8;
|
||||
|
||||
nextCursor$ = new LiveData<string | undefined>(undefined);
|
||||
hasMore$ = new LiveData(true);
|
||||
grantedUserCount$ = new LiveData(0);
|
||||
grantedUsers$ = new LiveData<GrantedUser[]>([]);
|
||||
isLoading$ = new LiveData(false);
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
readonly loadMore = effect(
|
||||
exhaustMap(() => {
|
||||
if (!this.hasMore$.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return fromPromise(async signal => {
|
||||
return await this.store.fetchDocGrantedUsersList(
|
||||
this.workspaceService.workspace.id,
|
||||
this.docService.doc.id,
|
||||
{
|
||||
first: this.PAGE_SIZE,
|
||||
after: this.nextCursor$.value,
|
||||
},
|
||||
signal
|
||||
);
|
||||
}).pipe(
|
||||
mergeMap(({ edges, pageInfo, totalCount }) => {
|
||||
this.grantedUsers$.next([
|
||||
...this.grantedUsers$.value,
|
||||
...edges.map(edge => edge.node),
|
||||
]);
|
||||
|
||||
this.grantedUserCount$.next(totalCount);
|
||||
this.hasMore$.next(pageInfo.hasNextPage);
|
||||
this.nextCursor$.next(pageInfo.endCursor ?? undefined);
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => {
|
||||
this.isLoading$.setValue(true);
|
||||
}),
|
||||
onComplete(() => this.isLoading$.setValue(false))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
reset() {
|
||||
this.grantedUsers$.setValue([]);
|
||||
this.grantedUserCount$.setValue(0);
|
||||
this.hasMore$.setValue(true);
|
||||
this.nextCursor$.setValue(undefined);
|
||||
this.isLoading$.setValue(false);
|
||||
this.error$.setValue(null);
|
||||
this.loadMore.reset();
|
||||
}
|
||||
|
||||
async grantUsersRole(userIds: string[], role: DocRole) {
|
||||
await this.store.grantDocUserRoles({
|
||||
docId: this.docService.doc.id,
|
||||
workspaceId: this.workspaceService.workspace.id,
|
||||
userIds,
|
||||
role,
|
||||
});
|
||||
this.grantedUsers$.next(
|
||||
this.grantedUsers$.value.map(user => {
|
||||
if (userIds.includes(user.user.id)) {
|
||||
return { ...user, role };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async revokeUsersRole(userId: string) {
|
||||
await this.store.revokeDocUserRoles(
|
||||
this.workspaceService.workspace.id,
|
||||
this.docService.doc.id,
|
||||
userId
|
||||
);
|
||||
this.grantedUsers$.next(
|
||||
this.grantedUsers$.value.filter(user => user.user.id !== userId)
|
||||
);
|
||||
}
|
||||
|
||||
async updateUserRole(userId: string, role: DocRole) {
|
||||
await this.store.updateDocUserRole(
|
||||
this.workspaceService.workspace.id,
|
||||
this.docService.doc.id,
|
||||
userId,
|
||||
role
|
||||
);
|
||||
this.grantedUsers$.next(
|
||||
this.grantedUsers$.value.map(user => {
|
||||
if (user.user.id === userId) {
|
||||
return { ...user, role };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.loadMore.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../../cloud';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { Member } from '../entities/members';
|
||||
import type { MemberSearchStore } from '../stores/member-search';
|
||||
|
||||
export class MemberSearchService extends Service {
|
||||
constructor(
|
||||
private readonly store: MemberSearchStore,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
readonly PAGE_SIZE = 8;
|
||||
readonly searchText$ = new LiveData<string>('');
|
||||
readonly isLoading$ = new LiveData(false);
|
||||
readonly error$ = new LiveData<any>(null);
|
||||
readonly result$ = new LiveData<Member[]>([]);
|
||||
readonly hasMore$ = new LiveData(true);
|
||||
|
||||
readonly loadMore = effect(
|
||||
exhaustMap(() => {
|
||||
if (!this.hasMore$.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return fromPromise(async signal => {
|
||||
return this.store.getMembersByEmailOrName(
|
||||
this.workspaceService.workspace.id,
|
||||
this.searchText$.value || undefined,
|
||||
this.result$.value.length,
|
||||
this.PAGE_SIZE,
|
||||
signal
|
||||
);
|
||||
}).pipe(
|
||||
mergeMap(data => {
|
||||
this.result$.setValue([...this.result$.value, ...data.members]);
|
||||
this.hasMore$.setValue(data.members.length === this.PAGE_SIZE);
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => {
|
||||
this.isLoading$.setValue(true);
|
||||
}),
|
||||
onComplete(() => this.isLoading$.setValue(false))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
reset() {
|
||||
this.result$.setValue([]);
|
||||
this.hasMore$.setValue(true);
|
||||
this.searchText$.setValue('');
|
||||
this.error$.setValue(null);
|
||||
this.loadMore.reset();
|
||||
}
|
||||
|
||||
search(searchText?: string) {
|
||||
if (this.searchText$.value === searchText) {
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
this.searchText$.setValue(searchText ?? '');
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DocRole,
|
||||
getPageGrantedUsersListQuery,
|
||||
type GrantDocUserRolesInput,
|
||||
grantDocUserRolesMutation,
|
||||
type PaginationInput,
|
||||
revokeDocUserRolesMutation,
|
||||
updateDocUserRoleMutation,
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
export class DocGrantedUsersStore extends Store {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetchDocGrantedUsersList(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
pagination: PaginationInput,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const res = await this.workspaceServerService.server.gql({
|
||||
query: getPageGrantedUsersListQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
docId,
|
||||
pagination,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return res.workspace.doc.grantedUsersList;
|
||||
}
|
||||
|
||||
async grantDocUserRoles(input: GrantDocUserRolesInput) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const res = await this.workspaceServerService.server.gql({
|
||||
query: grantDocUserRolesMutation,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
|
||||
return res.grantDocUserRoles;
|
||||
}
|
||||
|
||||
async revokeDocUserRoles(workspaceId: string, docId: string, userId: string) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const res = await this.workspaceServerService.server.gql({
|
||||
query: revokeDocUserRolesMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.revokeDocUserRoles;
|
||||
}
|
||||
|
||||
async updateDocUserRole(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
userId: string,
|
||||
role: DocRole
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const res = await this.workspaceServerService.server.gql({
|
||||
query: updateDocUserRoleMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workspaceId,
|
||||
docId,
|
||||
userId,
|
||||
role,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.updateDocUserRole;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { WorkspaceServerService } from '../../cloud';
|
||||
|
||||
export class MemberSearchStore extends Store {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getMembersByEmailOrName(
|
||||
workspaceId: string,
|
||||
query?: string,
|
||||
skip?: number,
|
||||
take?: number,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const data = await this.workspaceServerService.server.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
skip,
|
||||
take,
|
||||
query,
|
||||
},
|
||||
context: {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
|
||||
return data.workspace;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user