mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
@@ -1,4 +1,4 @@
|
||||
import type { DocRole, GetPageGrantedUsersListQuery } from '@affine/graphql';
|
||||
import { DocRole, type GetPageGrantedUsersListQuery } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
@@ -123,20 +123,29 @@ export class DocGrantedUsersService extends Service {
|
||||
}
|
||||
|
||||
async updateUserRole(userId: string, role: DocRole) {
|
||||
await this.store.updateDocUserRole(
|
||||
const res = 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;
|
||||
})
|
||||
);
|
||||
if (res) {
|
||||
if (role === DocRole.Owner) {
|
||||
this.reset();
|
||||
this.loadMore();
|
||||
return res;
|
||||
}
|
||||
this.grantedUsers$.next(
|
||||
this.grantedUsers$.value.map(user => {
|
||||
if (user.user.id === userId) {
|
||||
return { ...user, role };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateDocDefaultRole(role: DocRole) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const copyLinkContainerStyle = style({
|
||||
padding: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
@@ -18,6 +17,7 @@ export const copyLinkButtonStyle = style({
|
||||
flex: 1,
|
||||
padding: '4px 12px',
|
||||
paddingRight: '6px',
|
||||
borderRadius: '4px',
|
||||
borderRight: 'none',
|
||||
borderTopRightRadius: '0',
|
||||
borderBottomRightRadius: '0',
|
||||
@@ -35,6 +35,7 @@ export const copyLinkButtonStyle = style({
|
||||
export const copyLinkLabelContainerStyle = style({
|
||||
width: '100%',
|
||||
borderRight: 'none',
|
||||
borderRadius: '4px',
|
||||
borderTopRightRadius: '0',
|
||||
borderBottomRightRadius: '0',
|
||||
position: 'relative',
|
||||
@@ -70,6 +71,7 @@ export const copyLinkShortcutStyle = style({
|
||||
});
|
||||
export const copyLinkTriggerStyle = style({
|
||||
padding: '4px 12px 4px 8px',
|
||||
borderRadius: '4px',
|
||||
borderLeft: 'none',
|
||||
borderTopLeftRadius: '0',
|
||||
borderBottomLeftRadius: '0',
|
||||
|
||||
@@ -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 { 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 { InformationIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { PlanTag } from '../plan-tag';
|
||||
@@ -42,7 +44,8 @@ export const MembersPermission = ({
|
||||
() => getRoleName(t, docDefaultRole),
|
||||
[docDefaultRole, t]
|
||||
);
|
||||
|
||||
const showTips =
|
||||
docDefaultRole === DocRole.Reader || docDefaultRole === DocRole.Editor;
|
||||
const changePermission = useCallback(
|
||||
async (docRole: DocRole) => {
|
||||
await docGrantedUsersService.updateDocDefaultRole(docRole);
|
||||
@@ -76,12 +79,16 @@ export const MembersPermission = ({
|
||||
<div className={styles.labelStyle}>
|
||||
{t['com.affine.share-menu.option.permission.label']()}
|
||||
</div>
|
||||
<Menu
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
items={
|
||||
disabled ? null : (
|
||||
{disabled ? (
|
||||
<div className={clsx(styles.menuTriggerStyle, 'disable')}>
|
||||
{showTips ? <Tips disable={disabled} /> : null} {currentRoleName}
|
||||
</div>
|
||||
) : (
|
||||
<Menu
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
onSelect={selectManage}
|
||||
@@ -98,7 +105,7 @@ export const MembersPermission = ({
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div className={styles.tagContainerStyle}>
|
||||
{t['com.affine.share-menu.option.permission.can-edit']()}
|
||||
<PlanTag />
|
||||
{hittingPaywall ? <PlanTag /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
@@ -109,25 +116,39 @@ export const MembersPermission = ({
|
||||
<div className={styles.publicItemRowStyle}>
|
||||
<div className={styles.tagContainerStyle}>
|
||||
{t['com.affine.share-menu.option.permission.can-read']()}
|
||||
<PlanTag />
|
||||
{hittingPaywall ? <PlanTag /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuTrigger
|
||||
className={styles.menuTriggerStyle}
|
||||
variant="plain"
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
disabled={disabled}
|
||||
}
|
||||
>
|
||||
{currentRoleName}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
<MenuTrigger
|
||||
className={styles.menuTriggerStyle}
|
||||
variant="plain"
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
prefix={showTips ? <Tips /> : undefined}
|
||||
>
|
||||
{currentRoleName}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
@@ -92,13 +93,18 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||
<div className={styles.labelStyle}>
|
||||
{t['com.affine.share-menu.option.link.label']()}
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
items={
|
||||
disabled ? null : (
|
||||
{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
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
prefixIcon={<LockIcon />}
|
||||
@@ -122,23 +128,22 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuTrigger
|
||||
className={styles.menuTriggerStyle}
|
||||
data-testid="share-link-menu-trigger"
|
||||
variant="plain"
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
disabled={disabled}
|
||||
}
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.option.link.readonly']()
|
||||
: t['com.affine.share-menu.option.link.no-access']()}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
<MenuTrigger
|
||||
className={styles.menuTriggerStyle}
|
||||
data-testid="share-link-menu-trigger"
|
||||
variant="plain"
|
||||
contentStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.option.link.readonly']()
|
||||
: t['com.affine.share-menu.option.link.no-access']()}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const menuTriggerStyle = style({
|
||||
@@ -8,6 +9,13 @@ export const menuTriggerStyle = style({
|
||||
display: 'flex',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 400,
|
||||
selectors: {
|
||||
'&.disable': {
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: cssVarV2('text/disable'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const rowContainerStyle = style({
|
||||
@@ -36,3 +44,13 @@ export const tagContainerStyle = style({
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const informationIcon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: '20px',
|
||||
selectors: {
|
||||
'&.disable': {
|
||||
color: cssVarV2('icon/disable'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const headerStyle = style({
|
||||
|
||||
export const tabList = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
gap: '4px',
|
||||
gap: '12px',
|
||||
height: '28px',
|
||||
});
|
||||
export const tab = style({
|
||||
padding: '0px 4px 6px',
|
||||
});
|
||||
export const content = style({
|
||||
display: 'flex',
|
||||
@@ -17,7 +17,6 @@ export const content = style({
|
||||
});
|
||||
export const menuStyle = style({
|
||||
width: '390px',
|
||||
minHeight: '310px',
|
||||
maxHeight: '562px',
|
||||
padding: '12px',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Input } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -9,11 +10,12 @@ export const InviteInput = ({ onFocus }: { onFocus: () => void }) => {
|
||||
|
||||
return (
|
||||
<Input
|
||||
preFix={<SearchIcon fontSize={20} />}
|
||||
preFix={<SearchIcon className={styles.iconStyle} />}
|
||||
className={styles.inputStyle}
|
||||
onFocus={onFocus}
|
||||
inputStyle={{
|
||||
paddingLeft: '0',
|
||||
fontSize: cssVar('fontSm'),
|
||||
}}
|
||||
placeholder={t['com.affine.share-menu.invite-editor.placeholder']()}
|
||||
/>
|
||||
|
||||
@@ -16,10 +16,11 @@ export const headerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`,
|
||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
cursor: 'pointer',
|
||||
gap: '4px',
|
||||
padding: '4px 4px 6px',
|
||||
padding: '0px 4px 6px',
|
||||
height: '28px',
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
export const iconStyle = style({
|
||||
@@ -54,7 +55,7 @@ export const searchInput = style({
|
||||
flexGrow: 1,
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
fontSize: '14px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
@@ -70,6 +71,7 @@ export const InputContainer = style({
|
||||
padding: '4px',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
margin: '6px 0px',
|
||||
border: `1px solid ${cssVarV2('input/border/default')}`,
|
||||
|
||||
selectors: {
|
||||
@@ -135,6 +137,12 @@ export const checkbox = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
export const resultContainer = style({
|
||||
minHeight: '155px',
|
||||
maxHeight: '394px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const noFound = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
|
||||
@@ -152,19 +152,23 @@ export const InviteMemberEditor = ({
|
||||
|
||||
const switchToMemberManagementTab = useCallback(() => {
|
||||
workspaceDialogService.open('setting', {
|
||||
activeTab: 'workspace:preference',
|
||||
activeTab: 'workspace:members',
|
||||
});
|
||||
}, [workspaceDialogService]);
|
||||
|
||||
const handleClickMember = useCallback((member: Member) => {
|
||||
setSelectedMembers(prev => {
|
||||
if (prev.some(m => m.id === member.id)) {
|
||||
// if the member is already in the list, just return
|
||||
return prev;
|
||||
}
|
||||
return [...prev, member];
|
||||
});
|
||||
}, []);
|
||||
const handleClickMember = useCallback(
|
||||
(member: Member) => {
|
||||
setSelectedMembers(prev => {
|
||||
if (prev.some(m => m.id === member.id)) {
|
||||
// if the member is already in the list, just return
|
||||
return prev;
|
||||
}
|
||||
return [...prev, member];
|
||||
});
|
||||
focusInput();
|
||||
},
|
||||
[focusInput]
|
||||
);
|
||||
|
||||
const handleRoleChange = useCallback((role: DocRole) => {
|
||||
setInviteDocRoleType(role);
|
||||
@@ -221,16 +225,20 @@ export const InviteMemberEditor = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.sentEmail}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={false}
|
||||
disabled // TODO(@JimmFly): implement this
|
||||
/>
|
||||
{t['com.affine.share-menu.invite-editor.sent-email']()}
|
||||
{` (coming soon)`}
|
||||
{selectedMembers.length ? (
|
||||
<div className={styles.sentEmail}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={false}
|
||||
disabled // TODO(@JimmFly): implement this
|
||||
/>
|
||||
{t['com.affine.share-menu.invite-editor.sent-email']()}
|
||||
{` (coming soon)`}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.resultContainer}>
|
||||
<Result onClickMember={handleClickMember} />
|
||||
</div>
|
||||
<Result onClickMember={handleClickMember} />
|
||||
</div>
|
||||
<div className={styles.footerStyle}>
|
||||
<span
|
||||
@@ -263,6 +271,7 @@ const Result = ({
|
||||
onClickMember: (member: Member) => void;
|
||||
}) => {
|
||||
const memberSearchService = useService(MemberSearchService);
|
||||
const searchText = useLiveData(memberSearchService.searchText$);
|
||||
const result = useLiveData(memberSearchService.result$);
|
||||
const isLoading = useLiveData(memberSearchService.isLoading$);
|
||||
|
||||
@@ -274,14 +283,7 @@ const Result = ({
|
||||
|
||||
const itemContentRenderer = useCallback(
|
||||
(_index: number, data: Member) => {
|
||||
const handleSelect = () => {
|
||||
onClickMember(data);
|
||||
};
|
||||
return (
|
||||
<div onClick={handleSelect}>
|
||||
<MemberItem member={data} />
|
||||
</div>
|
||||
);
|
||||
return <MemberItem member={data} onSelect={onClickMember} />;
|
||||
},
|
||||
[onClickMember]
|
||||
);
|
||||
@@ -292,6 +294,10 @@ const Result = ({
|
||||
memberSearchService.loadMore();
|
||||
}, [memberSearchService]);
|
||||
|
||||
if (!searchText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!activeMembers || activeMembers.length === 0) {
|
||||
if (isLoading) {
|
||||
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
|
||||
components={{
|
||||
Scroller,
|
||||
@@ -370,7 +382,7 @@ const RoleSelector = ({
|
||||
>
|
||||
<div className={styles.planTagContainer}>
|
||||
{t['com.affine.share-menu.option.permission.can-edit']()}
|
||||
<PlanTag />
|
||||
{hittingPaywall ? <PlanTag /> : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
@@ -379,7 +391,7 @@ const RoleSelector = ({
|
||||
>
|
||||
<div className={styles.planTagContainer}>
|
||||
{t['com.affine.share-menu.option.permission.can-read']()}
|
||||
<PlanTag />
|
||||
{hittingPaywall ? <PlanTag /> : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@@ -62,7 +62,3 @@ export const memberRoleStyle = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const tooltipContentStyle = style({
|
||||
wordBreak: 'break-word',
|
||||
});
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { Avatar, Tooltip } from '@affine/component';
|
||||
import { Avatar } from '@affine/component';
|
||||
import type { Member } from '@affine/core/modules/permissions';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
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 (
|
||||
<div className={styles.memberItemStyle}>
|
||||
<div className={styles.memberItemStyle} onClick={handleSelect}>
|
||||
<div className={styles.memberContainerStyle}>
|
||||
<Avatar
|
||||
key={member.id}
|
||||
@@ -14,24 +24,8 @@ export const MemberItem = ({ member }: { member: Member }) => {
|
||||
size={36}
|
||||
/>
|
||||
<div className={styles.memberInfoStyle}>
|
||||
<Tooltip
|
||||
content={member.name}
|
||||
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 className={styles.memberNameStyle}>{member.name}</div>
|
||||
<div className={styles.memberEmailStyle}>{member.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const inputStyle = style({
|
||||
marginTop: '6px',
|
||||
padding: '4px',
|
||||
gap: '4px',
|
||||
borderRadius: '4px',
|
||||
height: '30px',
|
||||
});
|
||||
|
||||
export const iconStyle = style({
|
||||
fontSize: '20px',
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuTrigger,
|
||||
notify,
|
||||
Tooltip,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
@@ -14,12 +15,13 @@ import {
|
||||
DocGrantedUsersService,
|
||||
type GrantedUser,
|
||||
GuardService,
|
||||
WorkspacePermissionService,
|
||||
} from '@affine/core/modules/permissions';
|
||||
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 { useCallback, useMemo } from 'react';
|
||||
|
||||
import { PlanTag } from '../plan-tag';
|
||||
import * as styles from './member-item.css';
|
||||
@@ -130,65 +132,86 @@ const Options = ({
|
||||
const docGrantedUsersService = useService(DocGrantedUsersService);
|
||||
const docService = useService(DocService);
|
||||
const guardService = useService(GuardService);
|
||||
|
||||
const canTransferOwner = useLiveData(
|
||||
guardService.can$('Doc_TransferOwner', docService.doc.id)
|
||||
const workspacePermissionService = useService(WorkspacePermissionService);
|
||||
const isWorkspaceOwner = useLiveData(
|
||||
workspacePermissionService.permission.isOwner$
|
||||
);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const canTransferOwner =
|
||||
useLiveData(guardService.can$('Doc_TransferOwner', docService.doc.id)) &&
|
||||
!!isWorkspaceOwner;
|
||||
const canManageUsers = useLiveData(
|
||||
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 () => {
|
||||
try {
|
||||
await docGrantedUsersService.updateUserRole(userId, DocRole.Manager);
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
notify.error({
|
||||
title: t[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
}, [docGrantedUsersService, userId, t]);
|
||||
await updateUserRole(userId, DocRole.Manager);
|
||||
}, [updateUserRole, userId]);
|
||||
|
||||
const changeToEditor = useAsyncCallback(async () => {
|
||||
if (hittingPaywall) {
|
||||
openPaywallModal();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await docGrantedUsersService.updateUserRole(userId, DocRole.Editor);
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
notify.error({
|
||||
title: t[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
}, [docGrantedUsersService, hittingPaywall, openPaywallModal, userId, t]);
|
||||
await updateUserRole(userId, DocRole.Editor);
|
||||
}, [hittingPaywall, updateUserRole, userId, openPaywallModal]);
|
||||
|
||||
const changeToReader = useAsyncCallback(async () => {
|
||||
if (hittingPaywall) {
|
||||
openPaywallModal();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await docGrantedUsersService.updateUserRole(userId, DocRole.Reader);
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
notify.error({
|
||||
title: t[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
}, [docGrantedUsersService, hittingPaywall, openPaywallModal, userId, t]);
|
||||
await updateUserRole(userId, DocRole.Reader);
|
||||
}, [hittingPaywall, updateUserRole, userId, openPaywallModal]);
|
||||
|
||||
const changeToOwner = useAsyncCallback(async () => {
|
||||
try {
|
||||
await docGrantedUsersService.updateUserRole(userId, DocRole.Owner);
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
notify.error({
|
||||
title: t[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
}, [docGrantedUsersService, userId, t]);
|
||||
await updateUserRole(userId, DocRole.Owner);
|
||||
}, [updateUserRole, userId]);
|
||||
|
||||
const openTransferOwnerModal = useCallback(() => {
|
||||
openConfirmModal({
|
||||
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]);
|
||||
|
||||
const removeMember = useAsyncCallback(async () => {
|
||||
try {
|
||||
@@ -213,16 +236,16 @@ const Options = ({
|
||||
label: t['com.affine.share-menu.option.permission.can-edit'](),
|
||||
onClick: changeToEditor,
|
||||
role: DocRole.Editor,
|
||||
showPlanTag: true,
|
||||
showPlanTag: hittingPaywall,
|
||||
},
|
||||
{
|
||||
label: t['com.affine.share-menu.option.permission.can-read'](),
|
||||
onClick: changeToReader,
|
||||
role: DocRole.Reader,
|
||||
showPlanTag: true,
|
||||
showPlanTag: hittingPaywall,
|
||||
},
|
||||
];
|
||||
}, [changeToEditor, changeToManager, changeToReader, t]);
|
||||
}, [changeToEditor, changeToManager, changeToReader, hittingPaywall, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -238,7 +261,7 @@ const Options = ({
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onSelect={changeToOwner} disabled={!canTransferOwner}>
|
||||
<MenuItem onSelect={openTransferOwnerModal} disabled={!canTransferOwner}>
|
||||
{t['com.affine.share-menu.member-management.set-as-owner']()}
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
|
||||
@@ -16,10 +16,11 @@ export const headerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`,
|
||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
cursor: 'pointer',
|
||||
gap: '4px',
|
||||
padding: '4px 4px 6px',
|
||||
padding: '0px 4px 6px',
|
||||
height: '28px',
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
export const iconStyle = style({
|
||||
@@ -46,7 +47,9 @@ export const memberListStyle = style({
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
paddingTop: '6px',
|
||||
maxHeight: '455px',
|
||||
height: '100%',
|
||||
minHeight: '206px',
|
||||
maxHeight: '394px',
|
||||
});
|
||||
|
||||
export const scrollableRootStyle = style({
|
||||
|
||||
@@ -111,14 +111,27 @@ const MemberList = ({
|
||||
[hittingPaywall, openPaywallModal]
|
||||
);
|
||||
return (
|
||||
<Virtuoso
|
||||
components={{
|
||||
Scroller,
|
||||
}}
|
||||
data={grantedUserList}
|
||||
itemContent={itemContentRenderer}
|
||||
totalCount={grantedUserCount}
|
||||
endReached={loadMore}
|
||||
/>
|
||||
<div className={styles.memberListStyle}>
|
||||
{grantedUserList.length < 8 ? (
|
||||
grantedUserList.map(item => (
|
||||
<MemberItem
|
||||
key={item.user.id}
|
||||
grantedUser={item}
|
||||
openPaywallModal={openPaywallModal}
|
||||
hittingPaywall={hittingPaywall}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Virtuoso
|
||||
components={{
|
||||
Scroller,
|
||||
}}
|
||||
data={grantedUserList}
|
||||
itemContent={itemContentRenderer}
|
||||
totalCount={grantedUserCount}
|
||||
endReached={loadMore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const result = style({
|
||||
minHeight: '200px',
|
||||
minHeight: '342px',
|
||||
maxHeight: '342px',
|
||||
});
|
||||
|
||||
@@ -149,12 +149,13 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
value={currentTab}
|
||||
onValueChange={onValueChange}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value={ShareMenuTab.Share}>
|
||||
<Tabs.List className={styles.tabList}>
|
||||
<Tabs.Trigger value={ShareMenuTab.Share} className={styles.tab}>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value={ShareMenuTab.Export}
|
||||
className={styles.tab}
|
||||
style={{
|
||||
display: BUILD_CONFIG.isMobileEdition ? 'none' : undefined,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user