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,20 +123,29 @@ 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
); );
this.grantedUsers$.next( if (res) {
this.grantedUsers$.value.map(user => { if (role === DocRole.Owner) {
if (user.user.id === userId) { this.reset();
return { ...user, role }; this.loadMore();
} return res;
return user; }
}) this.grantedUsers$.next(
); this.grantedUsers$.value.map(user => {
if (user.user.id === userId) {
return { ...user, role };
}
return user;
})
);
}
return res;
} }
async updateDocDefaultRole(role: DocRole) { async updateDocDefaultRole(role: DocRole) {

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>
<Menu {disabled ? (
contentOptions={{ <div className={clsx(styles.menuTriggerStyle, 'disable')}>
align: 'end', {showTips ? <Tips disable={disabled} /> : null} {currentRoleName}
}} </div>
items={ ) : (
disabled ? null : ( <Menu
contentOptions={{
align: 'end',
}}
items={
<> <>
<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,25 +116,39 @@ 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
className={styles.menuTriggerStyle}
variant="plain"
contentStyle={{
width: '100%',
}}
disabled={disabled}
> >
{currentRoleName} <MenuTrigger
</MenuTrigger> className={styles.menuTriggerStyle}
</Menu> variant="plain"
contentStyle={{
width: '100%',
}}
prefix={showTips ? <Tips /> : undefined}
>
{currentRoleName}
</MenuTrigger>
</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 ? (
<Menu <div className={clsx(styles.menuTriggerStyle, 'disable')}>
contentOptions={{ {isSharedPage
align: 'end', ? t['com.affine.share-menu.option.link.readonly']()
}} : t['com.affine.share-menu.option.link.no-access']()}
items={ </div>
disabled ? null : ( ) : (
<Menu
contentOptions={{
align: 'end',
}}
items={
<> <>
<MenuItem <MenuItem
prefixIcon={<LockIcon />} prefixIcon={<LockIcon />}
@@ -122,23 +128,22 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
</div> </div>
</MenuItem> </MenuItem>
</> </>
) }
}
>
<MenuTrigger
className={styles.menuTriggerStyle}
data-testid="share-link-menu-trigger"
variant="plain"
contentStyle={{
width: '100%',
}}
disabled={disabled}
> >
{isSharedPage <MenuTrigger
? t['com.affine.share-menu.option.link.readonly']() className={styles.menuTriggerStyle}
: t['com.affine.share-menu.option.link.no-access']()} data-testid="share-link-menu-trigger"
</MenuTrigger> variant="plain"
</Menu> contentStyle={{
width: '100%',
}}
>
{isSharedPage
? t['com.affine.share-menu.option.link.readonly']()
: t['com.affine.share-menu.option.link.no-access']()}
</MenuTrigger>
</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,19 +152,23 @@ 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(
setSelectedMembers(prev => { (member: Member) => {
if (prev.some(m => m.id === member.id)) { setSelectedMembers(prev => {
// if the member is already in the list, just return if (prev.some(m => m.id === member.id)) {
return prev; // if the member is already in the list, just return
} return prev;
return [...prev, member]; }
}); return [...prev, member];
}, []); });
focusInput();
},
[focusInput]
);
const handleRoleChange = useCallback((role: DocRole) => { const handleRoleChange = useCallback((role: DocRole) => {
setInviteDocRoleType(role); setInviteDocRoleType(role);
@@ -221,16 +225,20 @@ export const InviteMemberEditor = ({
/> />
)} )}
</div> </div>
<div className={styles.sentEmail}> {selectedMembers.length ? (
<Checkbox <div className={styles.sentEmail}>
className={styles.checkbox} <Checkbox
checked={false} className={styles.checkbox}
disabled // TODO(@JimmFly): implement this checked={false}
/> disabled // TODO(@JimmFly): implement this
{t['com.affine.share-menu.invite-editor.sent-email']()} />
{` (coming soon)`} {t['com.affine.share-menu.invite-editor.sent-email']()}
{` (coming soon)`}
</div>
) : null}
<div className={styles.resultContainer}>
<Result onClickMember={handleClickMember} />
</div> </div>
<Result onClickMember={handleClickMember} />
</div> </div>
<div className={styles.footerStyle}> <div className={styles.footerStyle}>
<span <span
@@ -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 <div className={styles.memberNameStyle}>{member.name}</div>
content={member.name} <div className={styles.memberEmailStyle}>{member.email}</div>
rootOptions={{ delayDuration: 1000 }}
options={{
className: styles.tooltipContentStyle,
}}
>
<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>
</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 updateUserRole = useCallback(
async (userId: string, role: DocRole) => {
try {
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) {
const err = UserFriendlyError.fromAnyError(error);
notify.error({
title: t[`error.${err.name}`](err.data),
});
}
},
[docGrantedUsersService, t]
);
const changeToManager = useAsyncCallback(async () => { const changeToManager = useAsyncCallback(async () => {
try { await updateUserRole(userId, DocRole.Manager);
await docGrantedUsersService.updateUserRole(userId, DocRole.Manager); }, [updateUserRole, userId]);
} catch (error) {
const err = UserFriendlyError.fromAnyError(error);
notify.error({
title: t[`error.${err.name}`](err.data),
});
}
}, [docGrantedUsersService, userId, t]);
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'
}, [docGrantedUsersService, userId, t]); ](),
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]);
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,14 +111,27 @@ const MemberList = ({
[hittingPaywall, openPaywallModal] [hittingPaywall, openPaywallModal]
); );
return ( return (
<Virtuoso <div className={styles.memberListStyle}>
components={{ {grantedUserList.length < 8 ? (
Scroller, grantedUserList.map(item => (
}} <MemberItem
data={grantedUserList} key={item.user.id}
itemContent={itemContentRenderer} grantedUser={item}
totalCount={grantedUserCount} openPaywallModal={openPaywallModal}
endReached={loadMore} hittingPaywall={hittingPaywall}
/> />
))
) : (
<Virtuoso
components={{
Scroller,
}}
data={grantedUserList}
itemContent={itemContentRenderer}
totalCount={grantedUserCount}
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",