mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
feat(core): optimize team workspace member management (#9737)
close AF-2106 AF-2077 AF-2089 feat(core): handle need more seat status feat(core): prevent invite members when team plan is canceled
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { useDowngradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||
import { getDowngradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -231,6 +233,7 @@ export const TeamResumeAction = ({
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(WorkspaceSubscriptionService).subscription;
|
||||
const t = useI18n();
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
try {
|
||||
@@ -243,10 +246,14 @@ export const TeamResumeAction = ({
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
notify.success({
|
||||
title: t['com.affine.payment.resume.success.title'](),
|
||||
message: t['com.affine.payment.resume.success.team.message'](),
|
||||
});
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, onOpenChange]);
|
||||
}, [subscription, idempotencyKey, onOpenChange, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Loading, notify } from '@affine/component';
|
||||
import { Button, Loading, notify, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
InviteTeamMemberModal,
|
||||
type InviteTeamMemberModalProps,
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
WorkspaceSubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import {
|
||||
WorkspaceMembersService,
|
||||
WorkspacePermissionService,
|
||||
@@ -17,11 +21,12 @@ import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting
|
||||
import { copyTextToClipboard } from '@affine/core/utils/clipboard';
|
||||
import { emailRegex } from '@affine/core/utils/email-regex';
|
||||
import type { WorkspaceInviteLinkExpireTime } from '@affine/graphql';
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import { SubscriptionPlan, UserFriendlyError } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ExportIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { SettingState } from '../../types';
|
||||
@@ -51,6 +56,8 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
isTeam?: boolean;
|
||||
}) => {
|
||||
const workspaceShareSettingService = useService(WorkspaceShareSettingService);
|
||||
const subscription = useService(WorkspaceSubscriptionService).subscription;
|
||||
const workspaceSubscription = useLiveData(subscription.subscription$);
|
||||
const inviteLink = useLiveData(
|
||||
workspaceShareSettingService.sharePreview.inviteLink$
|
||||
);
|
||||
@@ -89,9 +96,77 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
const [openMemberLimit, setOpenMemberLimit] = useState(false);
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
|
||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||
const goToTeamBilling = useCallback(() => {
|
||||
onChangeSettingState({
|
||||
activeTab: 'workspace:billing',
|
||||
});
|
||||
}, [onChangeSettingState]);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const resume = useAsyncCallback(async () => {
|
||||
try {
|
||||
setIsMutating(true);
|
||||
await subscription.resumeSubscription(
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.Team
|
||||
);
|
||||
await subscription.waitForRevalidation();
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
closeConfirmModal();
|
||||
notify.success({
|
||||
title: t['com.affine.payment.resume.success.title'](),
|
||||
message: t['com.affine.payment.resume.success.team.message'](),
|
||||
});
|
||||
} catch (err) {
|
||||
const error = UserFriendlyError.fromAnyError(err);
|
||||
notify.error({
|
||||
title: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, closeConfirmModal, t]);
|
||||
const openInviteModal = useCallback(() => {
|
||||
if (isTeam && workspaceSubscription?.canceledAt) {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.member.team.retry-payment.title'](),
|
||||
description:
|
||||
t[
|
||||
`com.affine.payment.member.team.disabled-subscription.${isOwner ? 'owner' : 'admin'}.description`
|
||||
](),
|
||||
confirmText:
|
||||
t[
|
||||
isOwner
|
||||
? 'com.affine.payment.member.team.disabled-subscription.resume-subscription'
|
||||
: 'Got it'
|
||||
](),
|
||||
cancelText: t['Cancel'](),
|
||||
cancelButtonOptions: {
|
||||
style: {
|
||||
visibility: isOwner ? 'visible' : 'hidden',
|
||||
},
|
||||
},
|
||||
onConfirm: isOwner ? resume : undefined,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
loading: isMutating,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setOpenInvite(true);
|
||||
}, []);
|
||||
}, [
|
||||
isMutating,
|
||||
isOwner,
|
||||
isTeam,
|
||||
openConfirmModal,
|
||||
resume,
|
||||
t,
|
||||
workspaceSubscription?.canceledAt,
|
||||
]);
|
||||
|
||||
const onGenerateInviteLink = useCallback(
|
||||
async (expireTime: WorkspaceInviteLinkExpireTime) => {
|
||||
@@ -253,7 +328,11 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
</SettingRow>
|
||||
|
||||
<div className={styles.membersPanel}>
|
||||
<MemberList isOwner={!!isOwner} isAdmin={!!isAdmin} />
|
||||
<MemberList
|
||||
isOwner={!!isOwner}
|
||||
isAdmin={!!isAdmin}
|
||||
goToTeamBilling={goToTeamBilling}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,9 +29,11 @@ import * as styles from './styles.css';
|
||||
export const MemberList = ({
|
||||
isOwner,
|
||||
isAdmin,
|
||||
goToTeamBilling,
|
||||
}: {
|
||||
isOwner: boolean;
|
||||
isAdmin: boolean;
|
||||
goToTeamBilling: () => void;
|
||||
}) => {
|
||||
const membersService = useService(WorkspaceMembersService);
|
||||
const memberCount = useLiveData(membersService.members.memberCount$);
|
||||
@@ -85,6 +87,7 @@ export const MemberList = ({
|
||||
member={member}
|
||||
isOwner={isOwner}
|
||||
isAdmin={isAdmin}
|
||||
goToTeamBilling={goToTeamBilling}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -130,11 +133,13 @@ const MemberItem = ({
|
||||
isOwner,
|
||||
isAdmin,
|
||||
currentAccount,
|
||||
goToTeamBilling,
|
||||
}: {
|
||||
member: Member;
|
||||
isAdmin: boolean;
|
||||
isOwner: boolean;
|
||||
currentAccount: AuthAccountInfo;
|
||||
goToTeamBilling: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -208,7 +213,10 @@ const MemberItem = ({
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.roleOrStatus, {
|
||||
pending: member.status !== WorkspaceMemberStatus.Accepted,
|
||||
pending:
|
||||
member.status !== WorkspaceMemberStatus.Accepted &&
|
||||
member.status !== WorkspaceMemberStatus.NeedMoreSeat,
|
||||
error: member.status === WorkspaceMemberStatus.NeedMoreSeat,
|
||||
})}
|
||||
>
|
||||
{t.t(memberStatus)}
|
||||
@@ -220,6 +228,7 @@ const MemberItem = ({
|
||||
openAssignModal={handleOpenAssignModal}
|
||||
isAdmin={isAdmin}
|
||||
isOwner={isOwner}
|
||||
goToTeamBilling={goToTeamBilling}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -249,9 +258,10 @@ const MemberItem = ({
|
||||
const getMemberStatus = (member: Member): I18nString => {
|
||||
switch (member.status) {
|
||||
case WorkspaceMemberStatus.NeedMoreSeat:
|
||||
case WorkspaceMemberStatus.NeedMoreSeatAndReview:
|
||||
return 'insufficient-team-seat';
|
||||
case WorkspaceMemberStatus.Pending:
|
||||
return 'Pending';
|
||||
case WorkspaceMemberStatus.NeedMoreSeatAndReview:
|
||||
case WorkspaceMemberStatus.UnderReview:
|
||||
return 'Under-Review';
|
||||
case WorkspaceMemberStatus.Accepted:
|
||||
|
||||
@@ -14,11 +14,13 @@ export const MemberOptions = ({
|
||||
isOwner,
|
||||
isAdmin,
|
||||
openAssignModal,
|
||||
goToTeamBilling,
|
||||
}: {
|
||||
member: Member;
|
||||
isOwner: boolean;
|
||||
isAdmin: boolean;
|
||||
openAssignModal: () => void;
|
||||
goToTeamBilling: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const membersService = useService(WorkspaceMembersService);
|
||||
@@ -163,13 +165,49 @@ export const MemberOptions = ({
|
||||
});
|
||||
}, [member, membersService, t]);
|
||||
|
||||
const handleRetryPayment = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.member.team.retry-payment.title'](),
|
||||
description:
|
||||
t[
|
||||
`com.affine.payment.member.team.retry-payment.${isOwner ? 'owner' : 'admin'}.description`
|
||||
](),
|
||||
confirmText:
|
||||
t[
|
||||
isOwner
|
||||
? 'com.affine.payment.member.team.retry-payment.update-payment'
|
||||
: 'Got it'
|
||||
](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm: isOwner ? goToTeamBilling : undefined,
|
||||
cancelText: t['Cancel'](),
|
||||
cancelButtonOptions: {
|
||||
style: {
|
||||
visibility: isOwner ? 'visible' : 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [goToTeamBilling, isOwner, openConfirmModal, t]);
|
||||
|
||||
const operationButtonInfo = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: t['com.affine.payment.member.team.retry-payment'](),
|
||||
onClick: handleRetryPayment,
|
||||
show: member.status === WorkspaceMemberStatus.NeedMoreSeat,
|
||||
},
|
||||
{
|
||||
label: t['com.affine.payment.member.team.approve'](),
|
||||
onClick: handleApprove,
|
||||
show: member.status === WorkspaceMemberStatus.UnderReview,
|
||||
},
|
||||
{
|
||||
label: t['com.affine.payment.member.team.approve'](),
|
||||
onClick: handleRetryPayment,
|
||||
show: member.status === WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
},
|
||||
{
|
||||
label: t['com.affine.payment.member.team.decline'](),
|
||||
onClick: handleDecline,
|
||||
@@ -230,11 +268,13 @@ export const MemberOptions = ({
|
||||
handleChangeToCollaborator,
|
||||
handleDecline,
|
||||
handleRemove,
|
||||
handleRetryPayment,
|
||||
handleRevoke,
|
||||
isAdmin,
|
||||
isOwner,
|
||||
isTeam,
|
||||
member,
|
||||
member.permission,
|
||||
member.status,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ export const roleOrStatus = style({
|
||||
'&.pending': {
|
||||
color: cssVarV2('text/emphasis'),
|
||||
},
|
||||
'&.error': {
|
||||
color: cssVarV2('status/error'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,43 +1,80 @@
|
||||
import { AcceptInvitePage } from '@affine/component/member-components';
|
||||
import type { GetInviteInfoQuery } from '@affine/graphql';
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
ErrorNames,
|
||||
getInviteInfoQuery,
|
||||
UserFriendlyError,
|
||||
} from '@affine/graphql';
|
||||
AcceptInvitePage,
|
||||
JoinFailedPage,
|
||||
} from '@affine/component/member-components';
|
||||
import { ErrorNames, UserFriendlyError } from '@affine/graphql';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../components/hooks/use-navigate-helper';
|
||||
import { AuthService, GraphQLService } from '../../../modules/cloud';
|
||||
import { AppContainer } from '../../components/app-container';
|
||||
import { AcceptInviteService, AuthService } from '../../../modules/cloud';
|
||||
|
||||
const AcceptInvite = ({ inviteId }: { inviteId: string }) => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const acceptInviteService = useService(AcceptInviteService);
|
||||
const error = useLiveData(acceptInviteService.error$);
|
||||
const inviteInfo = useLiveData(acceptInviteService.inviteInfo$);
|
||||
const accepted = useLiveData(acceptInviteService.accepted$);
|
||||
const loading = useLiveData(acceptInviteService.loading$);
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
const openWorkspace = useCallback(() => {
|
||||
if (!inviteInfo?.workspace.id) {
|
||||
return;
|
||||
}
|
||||
jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE);
|
||||
}, [jumpToPage, inviteInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
acceptInviteService.revalidate({
|
||||
inviteId,
|
||||
});
|
||||
}, [acceptInviteService, inviteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const err = UserFriendlyError.fromAnyError(error);
|
||||
if (err.name === ErrorNames.ALREADY_IN_SPACE) {
|
||||
return navigateHelper.jumpToIndex();
|
||||
}
|
||||
}
|
||||
}, [error, navigateHelper]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!inviteInfo) {
|
||||
// if invite is expired
|
||||
return <Navigate to="/expired" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <JoinFailedPage inviteInfo={inviteInfo} error={error} />;
|
||||
}
|
||||
|
||||
if (accepted) {
|
||||
return (
|
||||
<AcceptInvitePage
|
||||
inviteInfo={inviteInfo}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// invite is expired
|
||||
return <Navigate to="/expired" />;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* /invite/:inviteId page
|
||||
*
|
||||
* only for web
|
||||
*/
|
||||
const AcceptInvite = ({
|
||||
inviteInfo,
|
||||
}: {
|
||||
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||
}) => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
|
||||
const openWorkspace = useCallback(() => {
|
||||
jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE);
|
||||
}, [inviteInfo.workspace.id, jumpToPage]);
|
||||
|
||||
return (
|
||||
<AcceptInvitePage inviteInfo={inviteInfo} onOpenWorkspace={openWorkspace} />
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const authService = useService(AuthService);
|
||||
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
||||
@@ -57,73 +94,13 @@ export const Component = () => {
|
||||
}
|
||||
}, [isRevalidating, jumpToSignIn, loginStatus, params.inviteId]);
|
||||
|
||||
if (!params.inviteId) {
|
||||
return <Navigate to="/expired" />;
|
||||
}
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <Middle />;
|
||||
return <AcceptInvite inviteId={params.inviteId} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Middle = () => {
|
||||
const graphqlService = useService(GraphQLService);
|
||||
const params = useParams<{ inviteId: string }>();
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
const [data, setData] = useState<{
|
||||
inviteId: string;
|
||||
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setData(null);
|
||||
const inviteId = params.inviteId || '';
|
||||
const res = await graphqlService.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
|
||||
// If the inviteId is invalid, redirect to 404 page
|
||||
if (!res || !res?.getInviteInfo) {
|
||||
return navigateHelper.jumpTo404();
|
||||
}
|
||||
|
||||
// No mater sign in or not, we need to accept the invite
|
||||
await graphqlService.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: res.getInviteInfo.workspace.id,
|
||||
inviteId,
|
||||
sendAcceptMail: true,
|
||||
},
|
||||
});
|
||||
|
||||
setData({
|
||||
inviteId,
|
||||
inviteInfo: res.getInviteInfo,
|
||||
});
|
||||
return;
|
||||
})().catch(error => {
|
||||
const userFriendlyError = UserFriendlyError.fromAnyError(error);
|
||||
console.error(userFriendlyError);
|
||||
if (userFriendlyError.name === ErrorNames.ALREADY_IN_SPACE) {
|
||||
return navigateHelper.jumpToIndex();
|
||||
}
|
||||
if (
|
||||
userFriendlyError.name === ErrorNames.USER_NOT_FOUND ||
|
||||
userFriendlyError.name === ErrorNames.SPACE_OWNER_NOT_FOUND
|
||||
) {
|
||||
return navigateHelper.jumpToExpired();
|
||||
}
|
||||
return navigateHelper.jumpTo404();
|
||||
});
|
||||
}, [graphqlService, navigateHelper, params.inviteId]);
|
||||
|
||||
if (!data) {
|
||||
return <AppContainer fallback />;
|
||||
}
|
||||
|
||||
return <AcceptInvite inviteInfo={data.inviteInfo} />;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export { AccountLoggedIn } from './events/account-logged-in';
|
||||
export { AccountLoggedOut } from './events/account-logged-out';
|
||||
export { ServerInitialized } from './events/server-initialized';
|
||||
export { ValidatorProvider } from './provider/validator';
|
||||
export { AcceptInviteService } from './services/accept-invite';
|
||||
export { AuthService } from './services/auth';
|
||||
export { CaptchaService } from './services/captcha';
|
||||
export { DefaultServerService } from './services/default-server';
|
||||
@@ -52,6 +53,7 @@ import { WorkspaceInvoices } from './entities/workspace-invoices';
|
||||
import { WorkspaceSubscription } from './entities/workspace-subscription';
|
||||
import { ValidatorProvider } from './provider/validator';
|
||||
import { ServerScope } from './scopes/server';
|
||||
import { AcceptInviteService } from './services/accept-invite';
|
||||
import { AuthService } from './services/auth';
|
||||
import { CaptchaService } from './services/captcha';
|
||||
import { CloudDocMetaService } from './services/cloud-doc-meta';
|
||||
@@ -71,8 +73,10 @@ import { UserQuotaService } from './services/user-quota';
|
||||
import { WorkspaceInvoicesService } from './services/workspace-invoices';
|
||||
import { WorkspaceServerService } from './services/workspace-server';
|
||||
import { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
||||
import { AcceptInviteStore } from './stores/accept-invite';
|
||||
import { AuthStore } from './stores/auth';
|
||||
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
||||
import { InviteInfoStore } from './stores/invite-info';
|
||||
import { InvoicesStore } from './stores/invoices';
|
||||
import { SelfhostGenerateLicenseStore } from './stores/selfhost-generate-license';
|
||||
import { SelfhostLicenseStore } from './stores/selfhost-license';
|
||||
@@ -136,7 +140,10 @@ export function configureCloudModule(framework: Framework) {
|
||||
.store(InvoicesStore, [GraphQLService])
|
||||
.entity(Invoices, [InvoicesStore])
|
||||
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
|
||||
.store(SelfhostGenerateLicenseStore, [GraphQLService]);
|
||||
.store(SelfhostGenerateLicenseStore, [GraphQLService])
|
||||
.store(InviteInfoStore, [GraphQLService])
|
||||
.service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore])
|
||||
.store(AcceptInviteStore, [GraphQLService]);
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { GetInviteInfoQuery } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../error';
|
||||
import type { AcceptInviteStore } from '../stores/accept-invite';
|
||||
import type { InviteInfoStore } from '../stores/invite-info';
|
||||
|
||||
export type InviteInfo = GetInviteInfoQuery['getInviteInfo'];
|
||||
|
||||
export class AcceptInviteService extends Service {
|
||||
constructor(
|
||||
private readonly store: AcceptInviteStore,
|
||||
private readonly inviteInfoStore: InviteInfoStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
inviteInfo$ = new LiveData<InviteInfo | undefined>(undefined);
|
||||
accepted$ = new LiveData<boolean>(false);
|
||||
loading$ = new LiveData(false);
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
readonly revalidate = effect(
|
||||
exhaustMap(({ inviteId }: { inviteId: string }) => {
|
||||
if (!inviteId) {
|
||||
return EMPTY;
|
||||
}
|
||||
return fromPromise(async () => {
|
||||
return await this.inviteInfoStore.getInviteInfo(inviteId);
|
||||
}).pipe(
|
||||
mergeMap(res => {
|
||||
this.inviteInfo$.setValue(res);
|
||||
return fromPromise(async () => {
|
||||
return await this.store.acceptInvite(
|
||||
res.workspace.id,
|
||||
inviteId,
|
||||
true
|
||||
);
|
||||
});
|
||||
}),
|
||||
mergeMap(res => {
|
||||
this.accepted$.next(res);
|
||||
return EMPTY;
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
count: 3,
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => {
|
||||
this.loading$.setValue(true);
|
||||
this.inviteInfo$.setValue(undefined);
|
||||
this.accepted$.setValue(false);
|
||||
}),
|
||||
onComplete(() => {
|
||||
this.loading$.setValue(false);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
override dispose(): void {
|
||||
this.revalidate.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { acceptInviteByInviteIdMutation } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
|
||||
export class AcceptInviteStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async acceptInvite(
|
||||
workspaceId: string,
|
||||
inviteId: string,
|
||||
sendAcceptMail?: boolean,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const data = await this.gqlService.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
|
||||
variables: {
|
||||
workspaceId,
|
||||
inviteId,
|
||||
sendAcceptMail,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return data.acceptInviteById;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getInviteInfoQuery } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
|
||||
export class InviteInfoStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getInviteInfo(inviteId?: string, signal?: AbortSignal) {
|
||||
if (!inviteId) {
|
||||
throw new Error('No inviteId');
|
||||
}
|
||||
const data = await this.gqlService.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return data.getInviteInfo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user