mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
refactor(server): workspace doc query (#10042)
This commit is contained in:
@@ -117,6 +117,9 @@ export class DocGrantedUsersService extends Service {
|
||||
this.grantedUsers$.next(
|
||||
this.grantedUsers$.value.filter(user => user.user.id !== userId)
|
||||
);
|
||||
if (this.grantedUserCount$.value > 0) {
|
||||
this.grantedUserCount$.next(this.grantedUserCount$.value - 1);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserRole(userId: string, role: DocRole) {
|
||||
@@ -136,6 +139,14 @@ export class DocGrantedUsersService extends Service {
|
||||
);
|
||||
}
|
||||
|
||||
async updateDocDefaultRole(role: DocRole) {
|
||||
return await this.store.updateDocDefaultRole({
|
||||
docId: this.docService.doc.id,
|
||||
workspaceId: this.workspaceService.workspace.id,
|
||||
role,
|
||||
});
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.loadMore.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
grantDocUserRolesMutation,
|
||||
type PaginationInput,
|
||||
revokeDocUserRolesMutation,
|
||||
type UpdateDocDefaultRoleInput,
|
||||
updateDocDefaultRoleMutation,
|
||||
updateDocUserRoleMutation,
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
@@ -92,4 +94,18 @@ export class DocGrantedUsersStore extends Store {
|
||||
|
||||
return res.updateDocUserRole;
|
||||
}
|
||||
|
||||
async updateDocDefaultRole(input: UpdateDocDefaultRoleInput) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const res = await this.workspaceServerService.server.gql({
|
||||
query: updateDocDefaultRoleMutation,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
|
||||
return res.updateDocDefaultRole;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
GetWorkspacePublicPageByIdQuery,
|
||||
PublicDocMode,
|
||||
} from '@affine/graphql';
|
||||
import type { GetWorkspacePageByIdQuery, PublicDocMode } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
@@ -20,14 +17,11 @@ import type { DocService } from '../../doc';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { ShareStore } from '../stores/share';
|
||||
|
||||
type ShareInfoType = GetWorkspacePublicPageByIdQuery['workspace']['publicDoc'];
|
||||
type ShareInfoType = GetWorkspacePageByIdQuery['workspace']['doc'];
|
||||
|
||||
export class ShareInfo extends Entity {
|
||||
info$ = new LiveData<ShareInfoType | undefined | null>(null);
|
||||
isShared$ = this.info$.map(info =>
|
||||
// null means not loaded yet, undefined means not shared
|
||||
info !== null ? info !== undefined : null
|
||||
);
|
||||
isShared$ = this.info$.map(info => info?.public);
|
||||
sharedMode$ = this.info$.map(info => (info !== null ? info?.mode : null));
|
||||
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PublicDocMode } from '@affine/graphql';
|
||||
import {
|
||||
getWorkspacePublicPageByIdQuery,
|
||||
getWorkspacePageByIdQuery,
|
||||
publishPageMutation,
|
||||
revokePublicPageMutation,
|
||||
} from '@affine/graphql';
|
||||
@@ -22,7 +22,7 @@ export class ShareStore extends Store {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const data = await this.workspaceServerService.server.gql({
|
||||
query: getWorkspacePublicPageByIdQuery,
|
||||
query: getWorkspacePageByIdQuery,
|
||||
variables: {
|
||||
pageId: docId,
|
||||
workspaceId,
|
||||
@@ -31,7 +31,7 @@ export class ShareStore extends Store {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
return data.workspace.publicDoc ?? undefined;
|
||||
return data.workspace.doc ?? undefined;
|
||||
}
|
||||
|
||||
async enableSharePage(
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DocGrantedUsersService } from '@affine/core/modules/permissions';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
import { DocRole } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { PlanTag } from '../plan-tag';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const getRoleName = (role: DocRole, t: ReturnType<typeof useI18n>) => {
|
||||
const getRoleName = (t: ReturnType<typeof useI18n>, role?: DocRole) => {
|
||||
switch (role) {
|
||||
case DocRole.Manager:
|
||||
return t['com.affine.share-menu.option.permission.can-manage']();
|
||||
@@ -18,7 +22,7 @@ const getRoleName = (role: DocRole, t: ReturnType<typeof useI18n>) => {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
// TODO(@JimmFly): impl the real permission
|
||||
|
||||
export const MembersPermission = ({
|
||||
openPaywallModal,
|
||||
hittingPaywall,
|
||||
@@ -29,32 +33,44 @@ export const MembersPermission = ({
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [docRole, setDocRole] = useState<DocRole>(DocRole.Manager);
|
||||
const currentRoleName = useMemo(() => getRoleName(docRole, t), [docRole, t]);
|
||||
const shareInfoService = useService(ShareInfoService);
|
||||
const docGrantedUsersService = useService(DocGrantedUsersService);
|
||||
const docDefaultRole = useLiveData(
|
||||
shareInfoService.shareInfo.info$
|
||||
)?.defaultRole;
|
||||
const currentRoleName = useMemo(
|
||||
() => getRoleName(t, docDefaultRole),
|
||||
[docDefaultRole, t]
|
||||
);
|
||||
|
||||
const changePermission = useCallback((newPermission: DocRole) => {
|
||||
setDocRole(newPermission);
|
||||
}, []);
|
||||
const changePermission = useCallback(
|
||||
async (docRole: DocRole) => {
|
||||
await docGrantedUsersService.updateDocDefaultRole(docRole);
|
||||
shareInfoService.shareInfo.revalidate();
|
||||
},
|
||||
[docGrantedUsersService, shareInfoService.shareInfo]
|
||||
);
|
||||
|
||||
const selectManage = useCallback(() => {
|
||||
changePermission(DocRole.Manager);
|
||||
const selectManage = useAsyncCallback(async () => {
|
||||
await changePermission(DocRole.Manager);
|
||||
}, [changePermission]);
|
||||
|
||||
const selectEdit = useCallback(() => {
|
||||
const selectEdit = useAsyncCallback(async () => {
|
||||
if (hittingPaywall) {
|
||||
openPaywallModal?.();
|
||||
return;
|
||||
}
|
||||
changePermission(DocRole.Editor);
|
||||
await changePermission(DocRole.Editor);
|
||||
}, [changePermission, hittingPaywall, openPaywallModal]);
|
||||
|
||||
const selectRead = useCallback(() => {
|
||||
const selectRead = useAsyncCallback(async () => {
|
||||
if (hittingPaywall) {
|
||||
openPaywallModal?.();
|
||||
return;
|
||||
}
|
||||
changePermission(DocRole.Reader);
|
||||
await changePermission(DocRole.Reader);
|
||||
}, [changePermission, hittingPaywall, openPaywallModal]);
|
||||
|
||||
return (
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.labelStyle}>
|
||||
@@ -69,7 +85,7 @@ export const MembersPermission = ({
|
||||
<>
|
||||
<MenuItem
|
||||
onSelect={selectManage}
|
||||
selected={docRole === DocRole.Manager}
|
||||
selected={docDefaultRole === DocRole.Manager}
|
||||
>
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
{t['com.affine.share-menu.option.permission.can-manage']()}
|
||||
@@ -77,7 +93,7 @@ export const MembersPermission = ({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={selectEdit}
|
||||
selected={docRole === DocRole.Editor}
|
||||
selected={docDefaultRole === DocRole.Editor}
|
||||
>
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div className={styles.tagContainerStyle}>
|
||||
@@ -88,7 +104,7 @@ export const MembersPermission = ({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={selectRead}
|
||||
selected={docRole === DocRole.Reader}
|
||||
selected={docDefaultRole === DocRole.Reader}
|
||||
>
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div className={styles.tagContainerStyle}>
|
||||
|
||||
@@ -124,7 +124,10 @@ export const sentEmail = style({
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontSm'),
|
||||
cursor: 'pointer',
|
||||
|
||||
// TODO(@JimmFly): remove this when we have a sent email feature
|
||||
cursor: 'not-allowed',
|
||||
color: cssVarV2('text/disable'),
|
||||
});
|
||||
|
||||
export const checkbox = style({
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
type CompositionEventHandler,
|
||||
useCallback,
|
||||
@@ -70,7 +71,6 @@ export const InviteMemberEditor = ({
|
||||
);
|
||||
|
||||
const memberSearchService = useService(MemberSearchService);
|
||||
const searchText = useLiveData(memberSearchService.searchText$);
|
||||
|
||||
useEffect(() => {
|
||||
// reset the search text when the component is mounted
|
||||
@@ -78,20 +78,25 @@ export const InviteMemberEditor = ({
|
||||
memberSearchService.loadMore();
|
||||
}, [memberSearchService]);
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() => debounce((value: string) => memberSearchService.search(value), 300),
|
||||
[memberSearchService]
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [composing, setComposing] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchText(value);
|
||||
if (!composing) {
|
||||
memberSearchService.search(value);
|
||||
debouncedSearch(value);
|
||||
}
|
||||
},
|
||||
[composing, memberSearchService]
|
||||
[composing, debouncedSearch]
|
||||
);
|
||||
|
||||
const [shouldSendEmail, setShouldSendEmail] = useState(false);
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
|
||||
const onInvite = useAsyncCallback(async () => {
|
||||
@@ -122,15 +127,11 @@ export const InviteMemberEditor = ({
|
||||
useCallback(
|
||||
e => {
|
||||
setComposing(false);
|
||||
memberSearchService.search(e.currentTarget.value);
|
||||
debouncedSearch(e.currentTarget.value);
|
||||
},
|
||||
[memberSearchService]
|
||||
[debouncedSearch]
|
||||
);
|
||||
|
||||
const onCheckboxChange = useCallback(() => {
|
||||
setShouldSendEmail(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const focusInput = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
@@ -220,13 +221,14 @@ export const InviteMemberEditor = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.sentEmail} onClick={onCheckboxChange}>
|
||||
<div className={styles.sentEmail}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={shouldSendEmail}
|
||||
disabled // not supported yet
|
||||
checked={false}
|
||||
disabled // TODO(@JimmFly): implement this
|
||||
/>
|
||||
{t['com.affine.share-menu.invite-editor.sent-email']()}
|
||||
{` (coming soon)`}
|
||||
</div>
|
||||
<Result onClickMember={handleClickMember} />
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import {
|
||||
DocGrantedUsersService,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
import { DocRole, UserFriendlyError } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PlanTag } from '../plan-tag';
|
||||
@@ -32,6 +34,10 @@ export const MemberItem = ({
|
||||
openPaywallModal: () => void;
|
||||
}) => {
|
||||
const user = grantedUser.user;
|
||||
const session = useService(AuthService).session;
|
||||
const account = useLiveData(session.account$);
|
||||
const disableManage =
|
||||
account?.id === user.id || grantedUser.role === DocRole.Owner;
|
||||
|
||||
const role = useMemo(() => {
|
||||
switch (grantedUser.role) {
|
||||
@@ -78,30 +84,33 @@ export const MemberItem = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
items={
|
||||
<Options
|
||||
userId={user.id}
|
||||
memberRole={grantedUser.role}
|
||||
hittingPaywall={hittingPaywall}
|
||||
openPaywallModal={openPaywallModal}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
align: 'start',
|
||||
}}
|
||||
>
|
||||
<MenuTrigger
|
||||
variant="plain"
|
||||
className={styles.menuTriggerStyle}
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
{disableManage ? (
|
||||
<div className={clsx(styles.memberRoleStyle, 'disable')}>{role}</div>
|
||||
) : (
|
||||
<Menu
|
||||
items={
|
||||
<Options
|
||||
userId={user.id}
|
||||
memberRole={grantedUser.role}
|
||||
hittingPaywall={hittingPaywall}
|
||||
openPaywallModal={openPaywallModal}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
align: 'start',
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
<MenuTrigger
|
||||
variant="plain"
|
||||
className={styles.menuTriggerStyle}
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -184,6 +193,7 @@ const Options = ({
|
||||
const removeMember = useAsyncCallback(async () => {
|
||||
try {
|
||||
await docGrantedUsersService.revokeUsersRole(userId);
|
||||
docGrantedUsersService.loadMore();
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
notify.error({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const result = style({
|
||||
minHeight: '164px',
|
||||
minHeight: '200px',
|
||||
maxHeight: '342px',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user