mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(core): add cloud workspace member limit (#5500)
<img width="521" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/2cac78ef-07ed-4e06-b739-1279f913d0e1"> <img width="514" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/eed0db08-8550-4686-8ea1-251f1c4c7fee">
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export * from './accept-invite-page';
|
export * from './accept-invite-page';
|
||||||
export * from './invite-modal';
|
export * from './invite-modal';
|
||||||
|
export * from './member-limit-modal';
|
||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface MemberLimitModalProps {
|
||||||
|
isFreePlan: boolean;
|
||||||
|
open: boolean;
|
||||||
|
plan: string;
|
||||||
|
quota: string;
|
||||||
|
setOpen: (value: boolean) => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MemberLimitModal = ({
|
||||||
|
isFreePlan,
|
||||||
|
open,
|
||||||
|
plan,
|
||||||
|
quota,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: MemberLimitModalProps) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
if (isFreePlan) {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
}, [onConfirm, setOpen, isFreePlan]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={t['com.affine.payment.member-limit.title']()}
|
||||||
|
description={t[
|
||||||
|
isFreePlan
|
||||||
|
? 'com.affine.payment.member-limit.free.description'
|
||||||
|
: 'com.affine.payment.member-limit.pro.description'
|
||||||
|
]({ planName: plan, quota: quota })}
|
||||||
|
cancelButtonOptions={{ style: { display: isFreePlan ? '' : 'none' } }}
|
||||||
|
confirmButtonOptions={{
|
||||||
|
type: 'primary',
|
||||||
|
children:
|
||||||
|
t[
|
||||||
|
isFreePlan
|
||||||
|
? 'com.affine.payment.member-limit.free.confirm'
|
||||||
|
: 'com.affine.payment.member-limit.pro.confirm'
|
||||||
|
](),
|
||||||
|
}}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
></ConfirmModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
InviteModal,
|
InviteModal,
|
||||||
type InviteModalProps,
|
type InviteModalProps,
|
||||||
|
MemberLimitModal,
|
||||||
} from '@affine/component/member-components';
|
} from '@affine/component/member-components';
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
@@ -13,8 +14,18 @@ import { Button, IconButton } from '@affine/component/ui/button';
|
|||||||
import { Loading } from '@affine/component/ui/loading';
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
|
import type { CheckedUser } from '@affine/core/hooks/affine/use-current-user';
|
||||||
|
import { useCurrentUser } from '@affine/core/hooks/affine/use-current-user';
|
||||||
|
import { useInviteMember } from '@affine/core/hooks/affine/use-invite-member';
|
||||||
|
import { useMemberCount } from '@affine/core/hooks/affine/use-member-count';
|
||||||
|
import { type Member, useMembers } from '@affine/core/hooks/affine/use-members';
|
||||||
|
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
||||||
|
import { useUserQuota } from '@affine/core/hooks/use-quota';
|
||||||
|
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Permission } from '@affine/graphql';
|
import { Permission, SubscriptionPlan } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightBigIcon, MoreVerticalIcon } from '@blocksuite/icons';
|
import { ArrowRightBigIcon, MoreVerticalIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -29,18 +40,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { openSettingModalAtom } from '../../../../../atoms';
|
|
||||||
import type { CheckedUser } from '../../../../../hooks/affine/use-current-user';
|
|
||||||
import { useCurrentUser } from '../../../../../hooks/affine/use-current-user';
|
|
||||||
import { useInviteMember } from '../../../../../hooks/affine/use-invite-member';
|
|
||||||
import { useMemberCount } from '../../../../../hooks/affine/use-member-count';
|
|
||||||
import {
|
|
||||||
type Member,
|
|
||||||
useMembers,
|
|
||||||
} from '../../../../../hooks/affine/use-members';
|
|
||||||
import { useRevokeMemberPermission } from '../../../../../hooks/affine/use-revoke-member-permission';
|
|
||||||
import { useUserQuota } from '../../../../../hooks/use-quota';
|
|
||||||
import { AffineErrorBoundary } from '../../../affine-error-boundary';
|
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
@@ -70,6 +69,19 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
const workspaceId = workspaceMetadata.id;
|
const workspaceId = workspaceMetadata.id;
|
||||||
const memberCount = useMemberCount(workspaceId);
|
const memberCount = useMemberCount(workspaceId);
|
||||||
|
|
||||||
|
const checkMemberCountLimit = useCallback(
|
||||||
|
(memberCount: number, memberLimit?: number) => {
|
||||||
|
if (memberLimit === undefined) return false;
|
||||||
|
return memberCount >= memberLimit;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const quota = useUserQuota();
|
||||||
|
const [subscription] = useUserSubscription();
|
||||||
|
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||||
|
const isLimited = checkMemberCountLimit(memberCount, quota?.memberLimit);
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { invite, isMutating } = useInviteMember(workspaceId);
|
const { invite, isMutating } = useInviteMember(workspaceId);
|
||||||
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
|
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
|
||||||
@@ -107,6 +119,14 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
[invite, pushNotification, t]
|
[invite, pushNotification, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
const handleUpgradeConfirm = useCallback(() => {
|
||||||
|
setSettingModalAtom({
|
||||||
|
open: true,
|
||||||
|
activeTab: 'plans',
|
||||||
|
});
|
||||||
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
|
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
|
||||||
|
|
||||||
@@ -134,16 +154,6 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
[pushNotification, revokeMemberPermission, t]
|
[pushNotification, revokeMemberPermission, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
|
||||||
const handleUpgrade = useCallback(() => {
|
|
||||||
setSettingModalAtom({
|
|
||||||
open: true,
|
|
||||||
activeTab: 'plans',
|
|
||||||
});
|
|
||||||
}, [setSettingModalAtom]);
|
|
||||||
|
|
||||||
const quota = useUserQuota();
|
|
||||||
|
|
||||||
const desc = useMemo(() => {
|
const desc = useMemo(() => {
|
||||||
if (!quota) return null;
|
if (!quota) return null;
|
||||||
|
|
||||||
@@ -157,7 +167,10 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
{upgradable ? (
|
{upgradable ? (
|
||||||
<>
|
<>
|
||||||
,
|
,
|
||||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
<div
|
||||||
|
className={style.goUpgradeWrapper}
|
||||||
|
onClick={handleUpgradeConfirm}
|
||||||
|
>
|
||||||
<span className={style.goUpgrade}>
|
<span className={style.goUpgrade}>
|
||||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||||
</span>
|
</span>
|
||||||
@@ -167,7 +180,7 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}, [handleUpgrade, quota, t, upgradable]);
|
}, [handleUpgradeConfirm, quota, t, upgradable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -179,12 +192,23 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<>
|
<>
|
||||||
<Button onClick={openModal}>{t['Invite Members']()}</Button>
|
<Button onClick={openModal}>{t['Invite Members']()}</Button>
|
||||||
<InviteModal
|
{isLimited ? (
|
||||||
open={open}
|
<MemberLimitModal
|
||||||
setOpen={setOpen}
|
isFreePlan={plan === SubscriptionPlan.Free}
|
||||||
onConfirm={onInviteConfirm}
|
open={open}
|
||||||
isMutating={isMutating}
|
plan={quota?.humanReadable.name ?? ''}
|
||||||
/>
|
quota={quota?.humanReadable.memberLimit ?? ''}
|
||||||
|
setOpen={setOpen}
|
||||||
|
onConfirm={handleUpgradeConfirm}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InviteModal
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
onConfirm={onInviteConfirm}
|
||||||
|
isMutating={isMutating}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
|||||||
@@ -799,6 +799,11 @@
|
|||||||
"com.affine.payment.upgrade-success-page.support": "If you have any questions, please contact our <1> customer support</1>.",
|
"com.affine.payment.upgrade-success-page.support": "If you have any questions, please contact our <1> customer support</1>.",
|
||||||
"com.affine.payment.upgrade-success-page.text": "Congratulations! Your AFFiNE account has been successfully upgraded to a Pro account.",
|
"com.affine.payment.upgrade-success-page.text": "Congratulations! Your AFFiNE account has been successfully upgraded to a Pro account.",
|
||||||
"com.affine.payment.upgrade-success-page.title": "Upgrade Successful!",
|
"com.affine.payment.upgrade-success-page.title": "Upgrade Successful!",
|
||||||
|
"com.affine.payment.member-limit.title": "You have reached the limit",
|
||||||
|
"com.affine.payment.member-limit.free.description": "Each {{planName}} user can invite up to {{quota}} members to join their workspace. You can upgrade your account to unlock more members.",
|
||||||
|
"com.affine.payment.member-limit.pro.description": "Each {{planName}} user can invite up to {{quota}} members to join their workspace. If you want to continue adding collaboration members, you can create a new workspace.",
|
||||||
|
"com.affine.payment.member-limit.free.confirm": "Upgrade",
|
||||||
|
"com.affine.payment.member-limit.pro.confirm": "Got it",
|
||||||
"com.affine.publicLinkDisableModal.button.cancel": "Cancel",
|
"com.affine.publicLinkDisableModal.button.cancel": "Cancel",
|
||||||
"com.affine.publicLinkDisableModal.button.disable": "Disable",
|
"com.affine.publicLinkDisableModal.button.disable": "Disable",
|
||||||
"com.affine.publicLinkDisableModal.description": "Disabling this public link will prevent anyone with the link from accessing this page.",
|
"com.affine.publicLinkDisableModal.description": "Disabling this public link will prevent anyone with the link from accessing this page.",
|
||||||
|
|||||||
Reference in New Issue
Block a user