fix(core): adjust share menu (#10164)

close AF-2218 AF-2215 AF-2221
This commit is contained in:
JimmFly
2025-02-14 10:32:12 +00:00
parent 36800f2d24
commit 9048b38069
20 changed files with 328 additions and 184 deletions

View File

@@ -1,4 +1,4 @@
import type { DocRole, GetPageGrantedUsersListQuery } from '@affine/graphql'; import { DocRole, type GetPageGrantedUsersListQuery } from '@affine/graphql';
import { import {
backoffRetry, backoffRetry,
catchErrorInto, catchErrorInto,
@@ -123,12 +123,18 @@ export class DocGrantedUsersService extends Service {
} }
async updateUserRole(userId: string, role: DocRole) { async updateUserRole(userId: string, role: DocRole) {
await this.store.updateDocUserRole( const res = await this.store.updateDocUserRole(
this.workspaceService.workspace.id, this.workspaceService.workspace.id,
this.docService.doc.id, this.docService.doc.id,
userId, userId,
role role
); );
if (res) {
if (role === DocRole.Owner) {
this.reset();
this.loadMore();
return res;
}
this.grantedUsers$.next( this.grantedUsers$.next(
this.grantedUsers$.value.map(user => { this.grantedUsers$.value.map(user => {
if (user.user.id === userId) { if (user.user.id === userId) {
@@ -139,6 +145,9 @@ export class DocGrantedUsersService extends Service {
); );
} }
return res;
}
async updateDocDefaultRole(role: DocRole) { async updateDocDefaultRole(role: DocRole) {
return await this.store.updateDocDefaultRole({ return await this.store.updateDocDefaultRole({
docId: this.docService.doc.id, docId: this.docService.doc.id,

View File

@@ -2,7 +2,6 @@ import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const copyLinkContainerStyle = style({ export const copyLinkContainerStyle = style({
padding: '4px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', width: '100%',
@@ -18,6 +17,7 @@ export const copyLinkButtonStyle = style({
flex: 1, flex: 1,
padding: '4px 12px', padding: '4px 12px',
paddingRight: '6px', paddingRight: '6px',
borderRadius: '4px',
borderRight: 'none', borderRight: 'none',
borderTopRightRadius: '0', borderTopRightRadius: '0',
borderBottomRightRadius: '0', borderBottomRightRadius: '0',
@@ -35,6 +35,7 @@ export const copyLinkButtonStyle = style({
export const copyLinkLabelContainerStyle = style({ export const copyLinkLabelContainerStyle = style({
width: '100%', width: '100%',
borderRight: 'none', borderRight: 'none',
borderRadius: '4px',
borderTopRightRadius: '0', borderTopRightRadius: '0',
borderBottomRightRadius: '0', borderBottomRightRadius: '0',
position: 'relative', position: 'relative',
@@ -70,6 +71,7 @@ export const copyLinkShortcutStyle = style({
}); });
export const copyLinkTriggerStyle = style({ export const copyLinkTriggerStyle = style({
padding: '4px 12px 4px 8px', padding: '4px 12px 4px 8px',
borderRadius: '4px',
borderLeft: 'none', borderLeft: 'none',
borderTopLeftRadius: '0', borderTopLeftRadius: '0',
borderBottomLeftRadius: '0', borderBottomLeftRadius: '0',

View File

@@ -1,10 +1,12 @@
import { Menu, MenuItem, MenuTrigger } from '@affine/component'; import { Menu, MenuItem, MenuTrigger, Tooltip } from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { DocGrantedUsersService } from '@affine/core/modules/permissions'; import { DocGrantedUsersService } from '@affine/core/modules/permissions';
import { ShareInfoService } from '@affine/core/modules/share-doc'; import { ShareInfoService } from '@affine/core/modules/share-doc';
import { DocRole } from '@affine/graphql'; import { DocRole } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { InformationIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { PlanTag } from '../plan-tag'; import { PlanTag } from '../plan-tag';
@@ -42,7 +44,8 @@ export const MembersPermission = ({
() => getRoleName(t, docDefaultRole), () => getRoleName(t, docDefaultRole),
[docDefaultRole, t] [docDefaultRole, t]
); );
const showTips =
docDefaultRole === DocRole.Reader || docDefaultRole === DocRole.Editor;
const changePermission = useCallback( const changePermission = useCallback(
async (docRole: DocRole) => { async (docRole: DocRole) => {
await docGrantedUsersService.updateDocDefaultRole(docRole); await docGrantedUsersService.updateDocDefaultRole(docRole);
@@ -76,12 +79,16 @@ export const MembersPermission = ({
<div className={styles.labelStyle}> <div className={styles.labelStyle}>
{t['com.affine.share-menu.option.permission.label']()} {t['com.affine.share-menu.option.permission.label']()}
</div> </div>
{disabled ? (
<div className={clsx(styles.menuTriggerStyle, 'disable')}>
{showTips ? <Tips disable={disabled} /> : null} {currentRoleName}
</div>
) : (
<Menu <Menu
contentOptions={{ contentOptions={{
align: 'end', align: 'end',
}} }}
items={ items={
disabled ? null : (
<> <>
<MenuItem <MenuItem
onSelect={selectManage} onSelect={selectManage}
@@ -98,7 +105,7 @@ export const MembersPermission = ({
<div className={styles.publicItemRowStyle}> <div className={styles.publicItemRowStyle}>
<div className={styles.tagContainerStyle}> <div className={styles.tagContainerStyle}>
{t['com.affine.share-menu.option.permission.can-edit']()} {t['com.affine.share-menu.option.permission.can-edit']()}
<PlanTag /> {hittingPaywall ? <PlanTag /> : null}
</div> </div>
</div> </div>
</MenuItem> </MenuItem>
@@ -109,12 +116,11 @@ export const MembersPermission = ({
<div className={styles.publicItemRowStyle}> <div className={styles.publicItemRowStyle}>
<div className={styles.tagContainerStyle}> <div className={styles.tagContainerStyle}>
{t['com.affine.share-menu.option.permission.can-read']()} {t['com.affine.share-menu.option.permission.can-read']()}
<PlanTag /> {hittingPaywall ? <PlanTag /> : null}
</div> </div>
</div> </div>
</MenuItem> </MenuItem>
</> </>
)
} }
> >
<MenuTrigger <MenuTrigger
@@ -123,11 +129,26 @@ export const MembersPermission = ({
contentStyle={{ contentStyle={{
width: '100%', width: '100%',
}} }}
disabled={disabled} prefix={showTips ? <Tips /> : undefined}
> >
{currentRoleName} {currentRoleName}
</MenuTrigger> </MenuTrigger>
</Menu> </Menu>
)}
</div> </div>
); );
}; };
export const Tips = ({ disable }: { disable?: boolean }) => {
const t = useI18n();
return (
<Tooltip content={t['com.affine.share-menu.option.permission.tips']()}>
<InformationIcon
className={clsx(styles.informationIcon, {
disable: disable,
})}
/>
</Tooltip>
);
};

View File

@@ -11,6 +11,7 @@ import {
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import clsx from 'clsx';
import { useEffect } from 'react'; import { useEffect } from 'react';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -92,13 +93,18 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
<div className={styles.labelStyle}> <div className={styles.labelStyle}>
{t['com.affine.share-menu.option.link.label']()} {t['com.affine.share-menu.option.link.label']()}
</div> </div>
{disabled ? (
<div className={clsx(styles.menuTriggerStyle, 'disable')}>
{isSharedPage
? t['com.affine.share-menu.option.link.readonly']()
: t['com.affine.share-menu.option.link.no-access']()}
</div>
) : (
<Menu <Menu
contentOptions={{ contentOptions={{
align: 'end', align: 'end',
}} }}
items={ items={
disabled ? null : (
<> <>
<MenuItem <MenuItem
prefixIcon={<LockIcon />} prefixIcon={<LockIcon />}
@@ -122,7 +128,6 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
</div> </div>
</MenuItem> </MenuItem>
</> </>
)
} }
> >
<MenuTrigger <MenuTrigger
@@ -132,13 +137,13 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
contentStyle={{ contentStyle={{
width: '100%', width: '100%',
}} }}
disabled={disabled}
> >
{isSharedPage {isSharedPage
? t['com.affine.share-menu.option.link.readonly']() ? t['com.affine.share-menu.option.link.readonly']()
: t['com.affine.share-menu.option.link.no-access']()} : t['com.affine.share-menu.option.link.no-access']()}
</MenuTrigger> </MenuTrigger>
</Menu> </Menu>
)}
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const menuTriggerStyle = style({ export const menuTriggerStyle = style({
@@ -8,6 +9,13 @@ export const menuTriggerStyle = style({
display: 'flex', display: 'flex',
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
fontWeight: 400, fontWeight: 400,
selectors: {
'&.disable': {
alignItems: 'center',
gap: '4px',
color: cssVarV2('text/disable'),
},
},
}); });
export const rowContainerStyle = style({ export const rowContainerStyle = style({
@@ -36,3 +44,13 @@ export const tagContainerStyle = style({
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: '8px',
}); });
export const informationIcon = style({
color: cssVarV2('icon/primary'),
fontSize: '20px',
selectors: {
'&.disable': {
color: cssVarV2('icon/disable'),
},
},
});

View File

@@ -1,14 +1,14 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const headerStyle = style({
export const tabList = style({
display: 'flex', display: 'flex',
alignItems: 'center', gap: '12px',
fontSize: cssVar('fontSm'), height: '28px',
fontWeight: 600, });
lineHeight: '22px', export const tab = style({
padding: '0 4px', padding: '0px 4px 6px',
gap: '4px',
}); });
export const content = style({ export const content = style({
display: 'flex', display: 'flex',
@@ -17,7 +17,6 @@ export const content = style({
}); });
export const menuStyle = style({ export const menuStyle = style({
width: '390px', width: '390px',
minHeight: '310px',
maxHeight: '562px', maxHeight: '562px',
padding: '12px', padding: '12px',
}); });

View File

@@ -1,6 +1,7 @@
import { Input } from '@affine/component'; import { Input } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { SearchIcon } from '@blocksuite/icons/rc'; import { SearchIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -9,11 +10,12 @@ export const InviteInput = ({ onFocus }: { onFocus: () => void }) => {
return ( return (
<Input <Input
preFix={<SearchIcon fontSize={20} />} preFix={<SearchIcon className={styles.iconStyle} />}
className={styles.inputStyle} className={styles.inputStyle}
onFocus={onFocus} onFocus={onFocus}
inputStyle={{ inputStyle={{
paddingLeft: '0', paddingLeft: '0',
fontSize: cssVar('fontSm'),
}} }}
placeholder={t['com.affine.share-menu.invite-editor.placeholder']()} placeholder={t['com.affine.share-menu.invite-editor.placeholder']()}
/> />

View File

@@ -16,10 +16,11 @@ export const headerStyle = style({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`, borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
cursor: 'pointer', cursor: 'pointer',
gap: '4px', gap: '4px',
padding: '4px 4px 6px', padding: '0px 4px 6px',
height: '28px',
color: cssVarV2('text/secondary'), color: cssVarV2('text/secondary'),
}); });
export const iconStyle = style({ export const iconStyle = style({
@@ -54,7 +55,7 @@ export const searchInput = style({
flexGrow: 1, flexGrow: 1,
border: 'none', border: 'none',
outline: 'none', outline: 'none',
fontSize: '14px', fontSize: cssVar('fontSm'),
fontFamily: 'inherit', fontFamily: 'inherit',
color: 'inherit', color: 'inherit',
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -70,6 +71,7 @@ export const InputContainer = style({
padding: '4px', padding: '4px',
flexWrap: 'wrap', flexWrap: 'wrap',
width: '100%', width: '100%',
margin: '6px 0px',
border: `1px solid ${cssVarV2('input/border/default')}`, border: `1px solid ${cssVarV2('input/border/default')}`,
selectors: { selectors: {
@@ -135,6 +137,12 @@ export const checkbox = style({
color: cssVarV2('icon/primary'), color: cssVarV2('icon/primary'),
}); });
export const resultContainer = style({
minHeight: '155px',
maxHeight: '394px',
overflow: 'hidden',
});
export const noFound = style({ export const noFound = style({
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
color: cssVarV2('text/secondary'), color: cssVarV2('text/secondary'),

View File

@@ -152,11 +152,12 @@ export const InviteMemberEditor = ({
const switchToMemberManagementTab = useCallback(() => { const switchToMemberManagementTab = useCallback(() => {
workspaceDialogService.open('setting', { workspaceDialogService.open('setting', {
activeTab: 'workspace:preference', activeTab: 'workspace:members',
}); });
}, [workspaceDialogService]); }, [workspaceDialogService]);
const handleClickMember = useCallback((member: Member) => { const handleClickMember = useCallback(
(member: Member) => {
setSelectedMembers(prev => { setSelectedMembers(prev => {
if (prev.some(m => m.id === member.id)) { if (prev.some(m => m.id === member.id)) {
// if the member is already in the list, just return // if the member is already in the list, just return
@@ -164,7 +165,10 @@ export const InviteMemberEditor = ({
} }
return [...prev, member]; return [...prev, member];
}); });
}, []); focusInput();
},
[focusInput]
);
const handleRoleChange = useCallback((role: DocRole) => { const handleRoleChange = useCallback((role: DocRole) => {
setInviteDocRoleType(role); setInviteDocRoleType(role);
@@ -221,6 +225,7 @@ export const InviteMemberEditor = ({
/> />
)} )}
</div> </div>
{selectedMembers.length ? (
<div className={styles.sentEmail}> <div className={styles.sentEmail}>
<Checkbox <Checkbox
className={styles.checkbox} className={styles.checkbox}
@@ -230,8 +235,11 @@ export const InviteMemberEditor = ({
{t['com.affine.share-menu.invite-editor.sent-email']()} {t['com.affine.share-menu.invite-editor.sent-email']()}
{` (coming soon)`} {` (coming soon)`}
</div> </div>
) : null}
<div className={styles.resultContainer}>
<Result onClickMember={handleClickMember} /> <Result onClickMember={handleClickMember} />
</div> </div>
</div>
<div className={styles.footerStyle}> <div className={styles.footerStyle}>
<span <span
className={styles.manageMemberStyle} className={styles.manageMemberStyle}
@@ -263,6 +271,7 @@ const Result = ({
onClickMember: (member: Member) => void; onClickMember: (member: Member) => void;
}) => { }) => {
const memberSearchService = useService(MemberSearchService); const memberSearchService = useService(MemberSearchService);
const searchText = useLiveData(memberSearchService.searchText$);
const result = useLiveData(memberSearchService.result$); const result = useLiveData(memberSearchService.result$);
const isLoading = useLiveData(memberSearchService.isLoading$); const isLoading = useLiveData(memberSearchService.isLoading$);
@@ -274,14 +283,7 @@ const Result = ({
const itemContentRenderer = useCallback( const itemContentRenderer = useCallback(
(_index: number, data: Member) => { (_index: number, data: Member) => {
const handleSelect = () => { return <MemberItem member={data} onSelect={onClickMember} />;
onClickMember(data);
};
return (
<div onClick={handleSelect}>
<MemberItem member={data} />
</div>
);
}, },
[onClickMember] [onClickMember]
); );
@@ -292,6 +294,10 @@ const Result = ({
memberSearchService.loadMore(); memberSearchService.loadMore();
}, [memberSearchService]); }, [memberSearchService]);
if (!searchText) {
return null;
}
if (!activeMembers || activeMembers.length === 0) { if (!activeMembers || activeMembers.length === 0) {
if (isLoading) { if (isLoading) {
return <Loading />; return <Loading />;
@@ -303,7 +309,13 @@ const Result = ({
); );
} }
return ( return activeMembers.length < 8 ? (
<div>
{activeMembers.map(member => (
<MemberItem key={member.id} member={member} onSelect={onClickMember} />
))}
</div>
) : (
<Virtuoso <Virtuoso
components={{ components={{
Scroller, Scroller,
@@ -370,7 +382,7 @@ const RoleSelector = ({
> >
<div className={styles.planTagContainer}> <div className={styles.planTagContainer}>
{t['com.affine.share-menu.option.permission.can-edit']()} {t['com.affine.share-menu.option.permission.can-edit']()}
<PlanTag /> {hittingPaywall ? <PlanTag /> : null}
</div> </div>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
@@ -379,7 +391,7 @@ const RoleSelector = ({
> >
<div className={styles.planTagContainer}> <div className={styles.planTagContainer}>
{t['com.affine.share-menu.option.permission.can-read']()} {t['com.affine.share-menu.option.permission.can-read']()}
<PlanTag /> {hittingPaywall ? <PlanTag /> : null}
</div> </div>
</MenuItem> </MenuItem>
</> </>

View File

@@ -62,7 +62,3 @@ export const memberRoleStyle = style({
}, },
}, },
}); });
export const tooltipContentStyle = style({
wordBreak: 'break-word',
});

View File

@@ -1,11 +1,21 @@
import { Avatar, Tooltip } from '@affine/component'; import { Avatar } from '@affine/component';
import type { Member } from '@affine/core/modules/permissions'; import type { Member } from '@affine/core/modules/permissions';
import { useCallback } from 'react';
import * as styles from './member-item.css'; import * as styles from './member-item.css';
export const MemberItem = ({ member }: { member: Member }) => { export const MemberItem = ({
member,
onSelect,
}: {
member: Member;
onSelect: (item: Member) => void;
}) => {
const handleSelect = useCallback(() => {
onSelect(member);
}, [member, onSelect]);
return ( return (
<div className={styles.memberItemStyle}> <div className={styles.memberItemStyle} onClick={handleSelect}>
<div className={styles.memberContainerStyle}> <div className={styles.memberContainerStyle}>
<Avatar <Avatar
key={member.id} key={member.id}
@@ -14,24 +24,8 @@ export const MemberItem = ({ member }: { member: Member }) => {
size={36} size={36}
/> />
<div className={styles.memberInfoStyle}> <div className={styles.memberInfoStyle}>
<Tooltip
content={member.name}
rootOptions={{ delayDuration: 1000 }}
options={{
className: styles.tooltipContentStyle,
}}
>
<div className={styles.memberNameStyle}>{member.name}</div> <div className={styles.memberNameStyle}>{member.name}</div>
</Tooltip>
<Tooltip
content={member.email}
rootOptions={{ delayDuration: 1000 }}
options={{
className: styles.tooltipContentStyle,
}}
>
<div className={styles.memberEmailStyle}>{member.email}</div> <div className={styles.memberEmailStyle}>{member.email}</div>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,15 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const inputStyle = style({ export const inputStyle = style({
marginTop: '6px', marginTop: '6px',
padding: '4px', padding: '4px',
gap: '4px', gap: '4px',
borderRadius: '4px',
height: '30px',
});
export const iconStyle = style({
fontSize: '20px',
color: cssVarV2('icon/primary'),
}); });

View File

@@ -6,6 +6,7 @@ import {
MenuTrigger, MenuTrigger,
notify, notify,
Tooltip, Tooltip,
useConfirmModal,
} from '@affine/component'; } from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService } from '@affine/core/modules/cloud'; import { AuthService } from '@affine/core/modules/cloud';
@@ -14,12 +15,13 @@ import {
DocGrantedUsersService, DocGrantedUsersService,
type GrantedUser, type GrantedUser,
GuardService, GuardService,
WorkspacePermissionService,
} from '@affine/core/modules/permissions'; } from '@affine/core/modules/permissions';
import { DocRole, UserFriendlyError } from '@affine/graphql'; import { DocRole, UserFriendlyError } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { PlanTag } from '../plan-tag'; import { PlanTag } from '../plan-tag';
import * as styles from './member-item.css'; import * as styles from './member-item.css';
@@ -130,65 +132,86 @@ const Options = ({
const docGrantedUsersService = useService(DocGrantedUsersService); const docGrantedUsersService = useService(DocGrantedUsersService);
const docService = useService(DocService); const docService = useService(DocService);
const guardService = useService(GuardService); const guardService = useService(GuardService);
const workspacePermissionService = useService(WorkspacePermissionService);
const canTransferOwner = useLiveData( const isWorkspaceOwner = useLiveData(
guardService.can$('Doc_TransferOwner', docService.doc.id) workspacePermissionService.permission.isOwner$
); );
const { openConfirmModal } = useConfirmModal();
const canTransferOwner =
useLiveData(guardService.can$('Doc_TransferOwner', docService.doc.id)) &&
!!isWorkspaceOwner;
const canManageUsers = useLiveData( const canManageUsers = useLiveData(
guardService.can$('Doc_Users_Manage', docService.doc.id) guardService.can$('Doc_Users_Manage', docService.doc.id)
); );
const changeToManager = useAsyncCallback(async () => { const updateUserRole = useCallback(
async (userId: string, role: DocRole) => {
try { try {
await docGrantedUsersService.updateUserRole(userId, DocRole.Manager); const res = await docGrantedUsersService.updateUserRole(userId, role);
if (res) {
notify.success({
title:
t['com.affine.share-menu.member-management.update-success'](),
});
} else {
notify.error({
title: t['com.affine.share-menu.member-management.update-fail'](),
});
}
} catch (error) { } catch (error) {
const err = UserFriendlyError.fromAnyError(error); const err = UserFriendlyError.fromAnyError(error);
notify.error({ notify.error({
title: t[`error.${err.name}`](err.data), title: t[`error.${err.name}`](err.data),
}); });
} }
}, [docGrantedUsersService, userId, t]); },
[docGrantedUsersService, t]
);
const changeToManager = useAsyncCallback(async () => {
await updateUserRole(userId, DocRole.Manager);
}, [updateUserRole, userId]);
const changeToEditor = useAsyncCallback(async () => { const changeToEditor = useAsyncCallback(async () => {
if (hittingPaywall) { if (hittingPaywall) {
openPaywallModal(); openPaywallModal();
return; return;
} }
try { await updateUserRole(userId, DocRole.Editor);
await docGrantedUsersService.updateUserRole(userId, DocRole.Editor); }, [hittingPaywall, updateUserRole, userId, openPaywallModal]);
} catch (error) {
const err = UserFriendlyError.fromAnyError(error);
notify.error({
title: t[`error.${err.name}`](err.data),
});
}
}, [docGrantedUsersService, hittingPaywall, openPaywallModal, userId, t]);
const changeToReader = useAsyncCallback(async () => { const changeToReader = useAsyncCallback(async () => {
if (hittingPaywall) { if (hittingPaywall) {
openPaywallModal(); openPaywallModal();
return; return;
} }
try { await updateUserRole(userId, DocRole.Reader);
await docGrantedUsersService.updateUserRole(userId, DocRole.Reader); }, [hittingPaywall, updateUserRole, userId, openPaywallModal]);
} catch (error) {
const err = UserFriendlyError.fromAnyError(error);
notify.error({
title: t[`error.${err.name}`](err.data),
});
}
}, [docGrantedUsersService, hittingPaywall, openPaywallModal, userId, t]);
const changeToOwner = useAsyncCallback(async () => { const changeToOwner = useAsyncCallback(async () => {
try { await updateUserRole(userId, DocRole.Owner);
await docGrantedUsersService.updateUserRole(userId, DocRole.Owner); }, [updateUserRole, userId]);
} catch (error) {
const err = UserFriendlyError.fromAnyError(error); const openTransferOwnerModal = useCallback(() => {
notify.error({ openConfirmModal({
title: t[`error.${err.name}`](err.data), title:
t[
'com.affine.share-menu.member-management.set-as-owner.confirm.title'
](),
description:
t[
'com.affine.share-menu.member-management.set-as-owner.confirm.description'
](),
onConfirm: changeToOwner,
confirmText: t['Confirm'](),
confirmButtonOptions: {
variant: 'primary',
},
cancelText: t['Cancel'](),
}); });
} }, [changeToOwner, openConfirmModal, t]);
}, [docGrantedUsersService, userId, t]);
const removeMember = useAsyncCallback(async () => { const removeMember = useAsyncCallback(async () => {
try { try {
@@ -213,16 +236,16 @@ const Options = ({
label: t['com.affine.share-menu.option.permission.can-edit'](), label: t['com.affine.share-menu.option.permission.can-edit'](),
onClick: changeToEditor, onClick: changeToEditor,
role: DocRole.Editor, role: DocRole.Editor,
showPlanTag: true, showPlanTag: hittingPaywall,
}, },
{ {
label: t['com.affine.share-menu.option.permission.can-read'](), label: t['com.affine.share-menu.option.permission.can-read'](),
onClick: changeToReader, onClick: changeToReader,
role: DocRole.Reader, role: DocRole.Reader,
showPlanTag: true, showPlanTag: hittingPaywall,
}, },
]; ];
}, [changeToEditor, changeToManager, changeToReader, t]); }, [changeToEditor, changeToManager, changeToReader, hittingPaywall, t]);
return ( return (
<> <>
@@ -238,7 +261,7 @@ const Options = ({
</div> </div>
</MenuItem> </MenuItem>
))} ))}
<MenuItem onSelect={changeToOwner} disabled={!canTransferOwner}> <MenuItem onSelect={openTransferOwnerModal} disabled={!canTransferOwner}>
{t['com.affine.share-menu.member-management.set-as-owner']()} {t['com.affine.share-menu.member-management.set-as-owner']()}
</MenuItem> </MenuItem>
<MenuSeparator /> <MenuSeparator />

View File

@@ -16,10 +16,11 @@ export const headerStyle = style({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`, borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
cursor: 'pointer', cursor: 'pointer',
gap: '4px', gap: '4px',
padding: '4px 4px 6px', padding: '0px 4px 6px',
height: '28px',
color: cssVarV2('text/secondary'), color: cssVarV2('text/secondary'),
}); });
export const iconStyle = style({ export const iconStyle = style({
@@ -46,7 +47,9 @@ export const memberListStyle = style({
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1,
paddingTop: '6px', paddingTop: '6px',
maxHeight: '455px', height: '100%',
minHeight: '206px',
maxHeight: '394px',
}); });
export const scrollableRootStyle = style({ export const scrollableRootStyle = style({

View File

@@ -111,6 +111,17 @@ const MemberList = ({
[hittingPaywall, openPaywallModal] [hittingPaywall, openPaywallModal]
); );
return ( return (
<div className={styles.memberListStyle}>
{grantedUserList.length < 8 ? (
grantedUserList.map(item => (
<MemberItem
key={item.user.id}
grantedUser={item}
openPaywallModal={openPaywallModal}
hittingPaywall={hittingPaywall}
/>
))
) : (
<Virtuoso <Virtuoso
components={{ components={{
Scroller, Scroller,
@@ -120,5 +131,7 @@ const MemberList = ({
totalCount={grantedUserCount} totalCount={grantedUserCount}
endReached={loadMore} endReached={loadMore}
/> />
)}
</div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const result = style({ export const result = style({
minHeight: '200px', minHeight: '342px',
maxHeight: '342px', maxHeight: '342px',
}); });

View File

@@ -149,12 +149,13 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
value={currentTab} value={currentTab}
onValueChange={onValueChange} onValueChange={onValueChange}
> >
<Tabs.List> <Tabs.List className={styles.tabList}>
<Tabs.Trigger value={ShareMenuTab.Share}> <Tabs.Trigger value={ShareMenuTab.Share} className={styles.tab}>
{t['com.affine.share-menu.shareButton']()} {t['com.affine.share-menu.shareButton']()}
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger <Tabs.Trigger
value={ShareMenuTab.Export} value={ShareMenuTab.Export}
className={styles.tab}
style={{ style={{
display: BUILD_CONFIG.isMobileEdition ? 'none' : undefined, display: BUILD_CONFIG.isMobileEdition ? 'none' : undefined,
}} }}

View File

@@ -14,7 +14,7 @@
"it-IT": 88, "it-IT": 88,
"it": 1, "it": 1,
"ja": 88, "ja": 88,
"ko": 64, "ko": 63,
"pl": 88, "pl": 88,
"pt-BR": 88, "pt-BR": 88,
"ru": 88, "ru": 88,

View File

@@ -3586,6 +3586,10 @@ export function useAFFiNEI18N(): {
* `Real-time syncing & collaboration for more people.` * `Real-time syncing & collaboration for more people.`
*/ */
["com.affine.payment.cloud.pro.benefit.g1-8"](): string; ["com.affine.payment.cloud.pro.benefit.g1-8"](): string;
/**
* `Granular edit access to docs.`
*/
["com.affine.payment.cloud.pro.benefit.g1-9"](): string;
/** /**
* `For family and small teams.` * `For family and small teams.`
*/ */
@@ -5886,6 +5890,10 @@ export function useAFFiNEI18N(): {
* `Members in workspace` * `Members in workspace`
*/ */
["com.affine.share-menu.option.permission.label"](): string; ["com.affine.share-menu.option.permission.label"](): string;
/**
* `Workspace admins and owner automatically have Can manage permissions.`
*/
["com.affine.share-menu.option.permission.tips"](): string;
/** /**
* `Publish to web` * `Publish to web`
*/ */
@@ -5933,6 +5941,22 @@ export function useAFFiNEI18N(): {
* `Set as owner` * `Set as owner`
*/ */
["com.affine.share-menu.member-management.set-as-owner"](): string; ["com.affine.share-menu.member-management.set-as-owner"](): string;
/**
* `Make this person the owner?`
*/
["com.affine.share-menu.member-management.set-as-owner.confirm.title"](): string;
/**
* `The new owner will be effective immediately, and you might lose access to this doc if other users remove you, please confirm.`
*/
["com.affine.share-menu.member-management.set-as-owner.confirm.description"](): string;
/**
* `Permission updated`
*/
["com.affine.share-menu.member-management.update-success"](): string;
/**
* `Failed to update permission`
*/
["com.affine.share-menu.member-management.update-fail"](): string;
/** /**
* `{{memberCount}} collaborators in the doc` * `{{memberCount}} collaborators in the doc`
*/ */

View File

@@ -894,6 +894,7 @@
"com.affine.payment.cloud.pro.benefit.g1-6": "Add comments on Doc and Edgeless.", "com.affine.payment.cloud.pro.benefit.g1-6": "Add comments on Doc and Edgeless.",
"com.affine.payment.cloud.pro.benefit.g1-7": "Community support.", "com.affine.payment.cloud.pro.benefit.g1-7": "Community support.",
"com.affine.payment.cloud.pro.benefit.g1-8": "Real-time syncing & collaboration for more people.", "com.affine.payment.cloud.pro.benefit.g1-8": "Real-time syncing & collaboration for more people.",
"com.affine.payment.cloud.pro.benefit.g1-9": "Granular edit access to docs.",
"com.affine.payment.cloud.pro.description": "For family and small teams.", "com.affine.payment.cloud.pro.description": "For family and small teams.",
"com.affine.payment.cloud.pro.name": "Pro", "com.affine.payment.cloud.pro.name": "Pro",
"com.affine.payment.cloud.pro.title.billed-yearly": "annually", "com.affine.payment.cloud.pro.title.billed-yearly": "annually",
@@ -1470,6 +1471,7 @@
"com.affine.share-menu.option.permission.can-edit": "Can edit", "com.affine.share-menu.option.permission.can-edit": "Can edit",
"com.affine.share-menu.option.permission.can-read": "Can read", "com.affine.share-menu.option.permission.can-read": "Can read",
"com.affine.share-menu.option.permission.label": "Members in workspace", "com.affine.share-menu.option.permission.label": "Members in workspace",
"com.affine.share-menu.option.permission.tips": "Workspace admins and owner automatically have Can manage permissions.",
"com.affine.share-menu.publish-to-web": "Publish to web", "com.affine.share-menu.publish-to-web": "Publish to web",
"com.affine.share-menu.share-privately": "Share privately", "com.affine.share-menu.share-privately": "Share privately",
"com.affine.share-menu.shareButton": "Share", "com.affine.share-menu.shareButton": "Share",
@@ -1479,6 +1481,10 @@
"com.affine.share-menu.member-management.member-count-more": "{{member1}}, {{member2}} and {{memberCount}} others", "com.affine.share-menu.member-management.member-count-more": "{{member1}}, {{member2}} and {{memberCount}} others",
"com.affine.share-menu.member-management.remove": "Remove", "com.affine.share-menu.member-management.remove": "Remove",
"com.affine.share-menu.member-management.set-as-owner": "Set as owner", "com.affine.share-menu.member-management.set-as-owner": "Set as owner",
"com.affine.share-menu.member-management.set-as-owner.confirm.title": "Make this person the owner?",
"com.affine.share-menu.member-management.set-as-owner.confirm.description": "The new owner will be effective immediately, and you might lose access to this doc if other users remove you, please confirm.",
"com.affine.share-menu.member-management.update-success": "Permission updated",
"com.affine.share-menu.member-management.update-fail": "Failed to update permission",
"com.affine.share-menu.member-management.header": "{{memberCount}} collaborators in the doc", "com.affine.share-menu.member-management.header": "{{memberCount}} collaborators in the doc",
"com.affine.share-menu.member-management.add-collaborators": "Add collaborators", "com.affine.share-menu.member-management.add-collaborators": "Add collaborators",
"com.affine.share-menu.invite-editor.header": "Send invite", "com.affine.share-menu.invite-editor.header": "Send invite",