mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-24 05:07:06 +08:00
feat(core): impl invitation link (#11181)
feat(core): add invitee to getInviteInfoQuery feat(core): enable invitation link refactor(core): replace AcceptInviteService to InvitationService
This commit is contained in:
@@ -11,5 +11,11 @@ query getInviteInfo($inviteId: String!) {
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
|
invitee {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -776,6 +776,12 @@ export const getInviteInfoQuery = {
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
|
invitee {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3242,6 +3242,13 @@ export type GetInviteInfoQuery = {
|
|||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
};
|
};
|
||||||
|
invitee: {
|
||||||
|
__typename?: 'WorkspaceUserType';
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
AuthPageContainer,
|
||||||
|
type User,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import type { GetInviteInfoQuery } from '@affine/graphql';
|
||||||
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { Avatar } from '../../ui/avatar';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
export const FailedToSendPage = ({
|
||||||
|
user,
|
||||||
|
inviteInfo,
|
||||||
|
}: {
|
||||||
|
user: User | null;
|
||||||
|
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<AuthPageContainer
|
||||||
|
title={t['com.affine.failed-to-send-request.title']()}
|
||||||
|
subtitle={
|
||||||
|
<div className={styles.lineHeight}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.failed-to-send-request.description"
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<div className={styles.avatarWrapper}>
|
||||||
|
<Avatar
|
||||||
|
url={`data:image/png;base64,${inviteInfo.workspace.avatar}`}
|
||||||
|
name={inviteInfo.workspace.name}
|
||||||
|
size={20}
|
||||||
|
colorfulFallback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
2: <span className={styles.inviteName} />,
|
||||||
|
3: <span className={styles.inviteName} />,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
workspaceName: inviteInfo.workspace.name,
|
||||||
|
userEmail: user?.email,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
></AuthPageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
export * from './accept-invite-page';
|
export * from './accept-invite-page';
|
||||||
export * from './expired';
|
export * from './expired';
|
||||||
|
export * from './failed-to-send-page';
|
||||||
export * from './invite-modal';
|
export * from './invite-modal';
|
||||||
export * from './invite-team-modal';
|
export * from './invite-team-modal';
|
||||||
export * from './join-failed-page';
|
export * from './join-failed-page';
|
||||||
export * from './member-limit-modal';
|
export * from './member-limit-modal';
|
||||||
|
export * from './request-to-join-page';
|
||||||
|
export * from './sent-request-page';
|
||||||
|
|||||||
+54
-45
@@ -113,53 +113,62 @@ export const LinkInvite = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.modalSubTitle}>
|
<div className={styles.invitationLinkContainer}>
|
||||||
{t['com.affine.payment.member.team.invite.link-expiration']()}
|
<div className={styles.modalSubTitle}>
|
||||||
</div>
|
{t['com.affine.payment.member.team.invite.link-expiration']()}
|
||||||
<Menu
|
</div>
|
||||||
items={items}
|
<Menu
|
||||||
contentOptions={{
|
items={items}
|
||||||
style: {
|
contentOptions={{
|
||||||
width: 'var(--radix-dropdown-menu-trigger-width)',
|
style: {
|
||||||
},
|
width: 'var(--radix-dropdown-menu-trigger-width)',
|
||||||
}}
|
},
|
||||||
>
|
|
||||||
<MenuTrigger style={{ width: '100%' }}>
|
|
||||||
{currentSelectedLabel}
|
|
||||||
</MenuTrigger>
|
|
||||||
</Menu>
|
|
||||||
<div className={styles.modalSubTitle}>
|
|
||||||
{t['com.affine.payment.member.team.invite.invitation-link']()}
|
|
||||||
</div>
|
|
||||||
<div className={styles.invitationLinkContent}>
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
invitationLink
|
|
||||||
? invitationLink.link
|
|
||||||
: 'https://your-app.com/invite/xxxxxxxx'
|
|
||||||
}
|
|
||||||
inputMode="none"
|
|
||||||
disabled
|
|
||||||
inputStyle={{
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
color: cssVarV2(
|
|
||||||
invitationLink ? 'text/primary' : 'text/placeholder'
|
|
||||||
),
|
|
||||||
backgroundColor: cssVarV2('layer/background/primary'),
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
{invitationLink ? (
|
<MenuTrigger style={{ width: '100%' }}>
|
||||||
<>
|
{currentSelectedLabel}
|
||||||
<Button onClick={onCopy} variant="secondary">
|
</MenuTrigger>
|
||||||
{t['com.affine.payment.member.team.invite.copy']()}
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div className={styles.invitationLinkContainer}>
|
||||||
|
<div className={styles.modalSubTitle}>
|
||||||
|
{t['com.affine.payment.member.team.invite.invitation-link']()}
|
||||||
|
</div>
|
||||||
|
<div className={styles.invitationLinkContent}>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
invitationLink
|
||||||
|
? invitationLink.link
|
||||||
|
: 'https://your-app.com/invite/xxxxxxxx'
|
||||||
|
}
|
||||||
|
inputMode="none"
|
||||||
|
disabled
|
||||||
|
inputStyle={{
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVarV2(
|
||||||
|
invitationLink ? 'text/primary' : 'text/placeholder'
|
||||||
|
),
|
||||||
|
backgroundColor: cssVarV2('layer/background/primary'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{invitationLink ? (
|
||||||
|
<>
|
||||||
|
<Button onClick={onCopy} variant="secondary">
|
||||||
|
{t['com.affine.payment.member.team.invite.copy']()}
|
||||||
|
</Button>
|
||||||
|
<IconButton icon={<CloseIcon />} onClick={onReset} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onGenerate} variant="secondary">
|
||||||
|
{t['com.affine.payment.member.team.invite.generate']()}
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton icon={<CloseIcon />} onClick={onReset} />
|
)}
|
||||||
</>
|
</div>
|
||||||
) : (
|
<p className={styles.invitationLinkDescription}>
|
||||||
<Button onClick={onGenerate} variant="secondary">
|
{t[
|
||||||
{t['com.affine.payment.member.team.invite.generate']()}
|
'com.affine.payment.member.team.invite.invitation-link.description'
|
||||||
</Button>
|
]()}
|
||||||
)}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-6
@@ -15,7 +15,7 @@ export const ModalContent = ({
|
|||||||
inviteEmail,
|
inviteEmail,
|
||||||
setInviteEmail,
|
setInviteEmail,
|
||||||
inviteMethod,
|
inviteMethod,
|
||||||
// onInviteMethodChange,
|
onInviteMethodChange,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
isMutating,
|
isMutating,
|
||||||
isValidEmail,
|
isValidEmail,
|
||||||
@@ -48,7 +48,7 @@ export const ModalContent = ({
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
value={inviteMethod}
|
value={inviteMethod}
|
||||||
// onChange={onInviteMethodChange}
|
onChange={onInviteMethodChange}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
@@ -65,13 +65,10 @@ export const ModalContent = ({
|
|||||||
label: (
|
label: (
|
||||||
<RadioItem
|
<RadioItem
|
||||||
icon={<LinkIcon className={styles.iconStyle} />}
|
icon={<LinkIcon className={styles.iconStyle} />}
|
||||||
label={`${t['com.affine.payment.member.team.invite.invite-link']()}(Coming soon)`}
|
label={t['com.affine.payment.member.team.invite.invite-link']()}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
value: 'link',
|
value: 'link',
|
||||||
style: {
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+11
@@ -81,6 +81,12 @@ export const modalSubTitle = style({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const invitationLinkContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
});
|
||||||
|
|
||||||
export const radioItem = style({
|
export const radioItem = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -107,3 +113,8 @@ export const invitationLinkContent = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const invitationLinkDescription = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const JoinFailedPage = ({
|
|||||||
title={t['com.affine.fail-to-join-workspace.title']()}
|
title={t['com.affine.fail-to-join-workspace.title']()}
|
||||||
subtitle={
|
subtitle={
|
||||||
userFriendlyError.name === ErrorNames.MEMBER_QUOTA_EXCEEDED ? (
|
userFriendlyError.name === ErrorNames.MEMBER_QUOTA_EXCEEDED ? (
|
||||||
<div className={styles.content}>
|
<div className={styles.lineHeight}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'com.affine.fail-to-join-workspace.description-1'}
|
i18nKey={'com.affine.fail-to-join-workspace.description-1'}
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
AuthPageContainer,
|
||||||
|
type User,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import type { GetInviteInfoQuery } from '@affine/graphql';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { SignOutIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
import { Avatar } from '../../ui/avatar';
|
||||||
|
import { Button, IconButton } from '../../ui/button';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
export const RequestToJoinPage = ({
|
||||||
|
user,
|
||||||
|
inviteInfo,
|
||||||
|
requestToJoin,
|
||||||
|
onSignOut,
|
||||||
|
}: {
|
||||||
|
user: User | null;
|
||||||
|
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||||
|
requestToJoin: () => void;
|
||||||
|
onSignOut: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthPageContainer
|
||||||
|
subtitle={
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.userWrapper}>
|
||||||
|
<Avatar
|
||||||
|
url={inviteInfo.user.avatarUrl || ''}
|
||||||
|
name={inviteInfo.user.name}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<span className={styles.inviteName}>{inviteInfo.user.name}</span>
|
||||||
|
</div>
|
||||||
|
<div>{t['invited you to join']()}</div>
|
||||||
|
<div className={styles.userWrapper}>
|
||||||
|
<Avatar
|
||||||
|
url={`data:image/png;base64,${inviteInfo.workspace.avatar}`}
|
||||||
|
name={inviteInfo.workspace.name}
|
||||||
|
size={20}
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
colorfulFallback
|
||||||
|
/>
|
||||||
|
<span className={styles.inviteName}>
|
||||||
|
{inviteInfo.workspace.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="primary" size="large" onClick={requestToJoin}>
|
||||||
|
{t['com.affine.request-to-join-workspace.button']()}
|
||||||
|
</Button>
|
||||||
|
{user ? (
|
||||||
|
<div className={styles.userInfoWrapper}>
|
||||||
|
<Avatar url={user.avatar ?? user.image} name={user.label} />
|
||||||
|
<span>{user.email}</span>
|
||||||
|
<IconButton
|
||||||
|
onClick={onSignOut}
|
||||||
|
size="20"
|
||||||
|
tooltip={t['404.signOut']()}
|
||||||
|
>
|
||||||
|
<SignOutIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</AuthPageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
AuthPageContainer,
|
||||||
|
type User,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import type { GetInviteInfoQuery } from '@affine/graphql';
|
||||||
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { Avatar } from '../../ui/avatar';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
export const SentRequestPage = ({
|
||||||
|
user,
|
||||||
|
inviteInfo,
|
||||||
|
}: {
|
||||||
|
user: User | null;
|
||||||
|
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<AuthPageContainer
|
||||||
|
title={t['com.affine.sent-request-to-join-workspace.title']()}
|
||||||
|
subtitle={
|
||||||
|
<div className={styles.lineHeight}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.sent-request-to-join-workspace.description"
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<div className={styles.avatarWrapper}>
|
||||||
|
<Avatar
|
||||||
|
url={`data:image/png;base64,${inviteInfo.workspace.avatar}`}
|
||||||
|
name={inviteInfo.workspace.name}
|
||||||
|
size={20}
|
||||||
|
colorfulFallback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
2: <span className={styles.inviteName} />,
|
||||||
|
3: <span className={styles.inviteName} />,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
workspaceName: inviteInfo.workspace.name,
|
||||||
|
userEmail: user?.email,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
></AuthPageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,11 +15,27 @@ export const inviteModalContent = style({
|
|||||||
export const inviteModalButtonContainer = style({
|
export const inviteModalButtonContainer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
// marginTop: 10,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const inviteName = style({
|
export const inviteName = style({
|
||||||
color: cssVarV2('text/primary'),
|
color: cssVarV2('text/primary'),
|
||||||
|
fontWeight: '600',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const avatarWrapper = style({
|
||||||
|
verticalAlign: 'sub',
|
||||||
|
display: 'inline-block',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userInfoWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
marginTop: '28px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lineHeight = style({
|
||||||
|
lineHeight: '1.5',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
@@ -30,7 +46,7 @@ export const content = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const userWrapper = style({
|
export const userWrapper = style({
|
||||||
display: 'flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Scrollable,
|
Scrollable,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { AcceptInviteService } from '@affine/core/modules/cloud';
|
import { InvitationService } from '@affine/core/modules/cloud';
|
||||||
import {
|
import {
|
||||||
type Notification,
|
type Notification,
|
||||||
NotificationListService,
|
NotificationListService,
|
||||||
@@ -314,7 +314,7 @@ const InvitationNotificationItem = ({
|
|||||||
const memberInactived = !body.createdByUser;
|
const memberInactived = !body.createdByUser;
|
||||||
const workspaceInactived = !body.workspace;
|
const workspaceInactived = !body.workspace;
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const acceptInviteService = useService(AcceptInviteService);
|
const invitationService = useService(InvitationService);
|
||||||
const notificationListService = useService(NotificationListService);
|
const notificationListService = useService(NotificationListService);
|
||||||
const inviteId = body.inviteId;
|
const inviteId = body.inviteId;
|
||||||
const [isAccepting, setIsAccepting] = useState(false);
|
const [isAccepting, setIsAccepting] = useState(false);
|
||||||
@@ -347,8 +347,8 @@ const InvitationNotificationItem = ({
|
|||||||
|
|
||||||
const handleAcceptInvite = useCallback(() => {
|
const handleAcceptInvite = useCallback(() => {
|
||||||
setIsAccepting(true);
|
setIsAccepting(true);
|
||||||
acceptInviteService
|
invitationService
|
||||||
.waitForAcceptInvite(inviteId)
|
.acceptInvite(inviteId)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const userFriendlyError = UserFriendlyError.fromAny(err);
|
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||||
if (userFriendlyError.is('ALREADY_IN_SPACE')) {
|
if (userFriendlyError.is('ALREADY_IN_SPACE')) {
|
||||||
@@ -384,7 +384,7 @@ const InvitationNotificationItem = ({
|
|||||||
setIsAccepting(false);
|
setIsAccepting(false);
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
acceptInviteService,
|
invitationService,
|
||||||
handleReadAndOpenWorkspace,
|
handleReadAndOpenWorkspace,
|
||||||
inviteId,
|
inviteId,
|
||||||
notification,
|
notification,
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
AcceptInvitePage,
|
AcceptInvitePage,
|
||||||
ExpiredPage,
|
ExpiredPage,
|
||||||
|
FailedToSendPage,
|
||||||
JoinFailedPage,
|
JoinFailedPage,
|
||||||
|
RequestToJoinPage,
|
||||||
|
SentRequestPage,
|
||||||
} from '@affine/component/member-components';
|
} from '@affine/component/member-components';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||||
import { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
|
import { WorkspaceMemberStatus } from '@affine/graphql';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Navigate, useParams } from 'react-router-dom';
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RouteLogic,
|
RouteLogic,
|
||||||
useNavigateHelper,
|
useNavigateHelper,
|
||||||
} from '../../../components/hooks/use-navigate-helper';
|
} from '../../../components/hooks/use-navigate-helper';
|
||||||
import { AcceptInviteService, AuthService } from '../../../modules/cloud';
|
import { AuthService, InvitationService } from '../../../modules/cloud';
|
||||||
|
|
||||||
const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||||
const { jumpToPage } = useNavigateHelper();
|
const { jumpToPage } = useNavigateHelper();
|
||||||
const acceptInviteService = useService(AcceptInviteService);
|
|
||||||
|
const invitationService = useService(InvitationService);
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const error = useLiveData(acceptInviteService.error$);
|
const authService = useService(AuthService);
|
||||||
const inviteId = useLiveData(acceptInviteService.inviteId$);
|
const user = useLiveData(authService.session.account$);
|
||||||
const inviteInfo = useLiveData(acceptInviteService.inviteInfo$);
|
const error = useLiveData(invitationService.error$);
|
||||||
const accepted = useLiveData(acceptInviteService.accepted$);
|
const inviteId = useLiveData(invitationService.inviteId$);
|
||||||
const loading = useLiveData(acceptInviteService.loading$);
|
const inviteInfo = useLiveData(invitationService.inviteInfo$);
|
||||||
|
const loading = useLiveData(invitationService.loading$);
|
||||||
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
|
const [accepted, setAccepted] = useState(false);
|
||||||
|
|
||||||
const openWorkspace = useAsyncCallback(async () => {
|
const openWorkspace = useAsyncCallback(async () => {
|
||||||
if (!inviteInfo?.workspace.id) {
|
if (!inviteInfo?.workspace.id) {
|
||||||
@@ -40,26 +48,40 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
|||||||
}, [navigateHelper]);
|
}, [navigateHelper]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
acceptInviteService.acceptInvite({
|
// if workspace already exists, open it
|
||||||
inviteId: targetInviteId,
|
if (
|
||||||
});
|
!accepted &&
|
||||||
}, [acceptInviteService, targetInviteId]);
|
inviteInfo?.workspace.id &&
|
||||||
|
workspaces.some(w => w.id === inviteInfo.workspace.id)
|
||||||
useEffect(() => {
|
) {
|
||||||
if (error && inviteId === targetInviteId) {
|
return openWorkspace();
|
||||||
const err = UserFriendlyError.fromAny(error);
|
|
||||||
if (err.is('ALREADY_IN_SPACE')) {
|
|
||||||
return openWorkspace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [error, inviteId, navigateHelper, openWorkspace, targetInviteId]);
|
}, [accepted, inviteInfo?.workspace.id, openWorkspace, workspaces]);
|
||||||
|
|
||||||
|
const requestToJoin = useAsyncCallback(async () => {
|
||||||
|
await invitationService
|
||||||
|
.acceptInvite(targetInviteId)
|
||||||
|
.then(() => {
|
||||||
|
invitationService.getInviteInfo({ inviteId: targetInviteId });
|
||||||
|
setAccepted(true);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const err = UserFriendlyError.fromAny(error);
|
||||||
|
if (err.is('ALREADY_IN_SPACE')) {
|
||||||
|
return openWorkspace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [invitationService, openWorkspace, targetInviteId]);
|
||||||
|
|
||||||
|
const onSignOut = useAsyncCallback(async () => {
|
||||||
|
await authService.signOut();
|
||||||
|
}, [authService]);
|
||||||
|
|
||||||
if (loading || inviteId !== targetInviteId) {
|
if (loading || inviteId !== targetInviteId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inviteInfo) {
|
if (!inviteInfo) {
|
||||||
// if invite is expired
|
|
||||||
return <ExpiredPage onOpenAffine={onOpenAffine} />;
|
return <ExpiredPage onOpenAffine={onOpenAffine} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,17 +89,35 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
|||||||
return <JoinFailedPage inviteInfo={inviteInfo} error={error} />;
|
return <JoinFailedPage inviteInfo={inviteInfo} error={error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accepted) {
|
// for email invite
|
||||||
|
if (accepted && inviteInfo.status === WorkspaceMemberStatus.Accepted) {
|
||||||
return (
|
return (
|
||||||
<AcceptInvitePage
|
<AcceptInvitePage
|
||||||
inviteInfo={inviteInfo}
|
|
||||||
onOpenWorkspace={openWorkspace}
|
onOpenWorkspace={openWorkspace}
|
||||||
|
inviteInfo={inviteInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// invite is expired
|
|
||||||
return <ExpiredPage onOpenAffine={onOpenAffine} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inviteInfo.status === WorkspaceMemberStatus.UnderReview) {
|
||||||
|
return <SentRequestPage user={user} inviteInfo={inviteInfo} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeatAndReview ||
|
||||||
|
inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeat
|
||||||
|
) {
|
||||||
|
return <FailedToSendPage user={user} inviteInfo={inviteInfo} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RequestToJoinPage
|
||||||
|
user={user}
|
||||||
|
inviteInfo={inviteInfo}
|
||||||
|
requestToJoin={requestToJoin}
|
||||||
|
onSignOut={onSignOut}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,13 +127,17 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
|||||||
*/
|
*/
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
const invitationService = useService(InvitationService);
|
||||||
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
||||||
const loginStatus = useLiveData(authService.session.status$);
|
const loginStatus = useLiveData(authService.session.status$);
|
||||||
const params = useParams<{ inviteId: string }>();
|
const params = useParams<{ inviteId: string }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authService.session.revalidate();
|
authService.session.revalidate();
|
||||||
}, [authService]);
|
if (params.inviteId) {
|
||||||
|
invitationService.getInviteInfo({ inviteId: params.inviteId });
|
||||||
|
}
|
||||||
|
}, [authService, invitationService, params.inviteId]);
|
||||||
|
|
||||||
const { jumpToSignIn } = useNavigateHelper();
|
const { jumpToSignIn } = useNavigateHelper();
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ export { AccountLoggedOut } from './events/account-logged-out';
|
|||||||
export { AuthProvider } from './provider/auth';
|
export { AuthProvider } from './provider/auth';
|
||||||
export { ValidatorProvider } from './provider/validator';
|
export { ValidatorProvider } from './provider/validator';
|
||||||
export { ServerScope } from './scopes/server';
|
export { ServerScope } from './scopes/server';
|
||||||
export { AcceptInviteService } from './services/accept-invite';
|
|
||||||
export { AuthService } from './services/auth';
|
export { AuthService } from './services/auth';
|
||||||
export { CaptchaService } from './services/captcha';
|
export { CaptchaService } from './services/captcha';
|
||||||
export { DefaultServerService } from './services/default-server';
|
export { DefaultServerService } from './services/default-server';
|
||||||
export { EventSourceService } from './services/eventsource';
|
export { EventSourceService } from './services/eventsource';
|
||||||
export { FetchService } from './services/fetch';
|
export { FetchService } from './services/fetch';
|
||||||
export { GraphQLService } from './services/graphql';
|
export { GraphQLService } from './services/graphql';
|
||||||
|
export { InvitationService } from './services/invitation';
|
||||||
export { InvoicesService } from './services/invoices';
|
export { InvoicesService } from './services/invoices';
|
||||||
export { PublicUserService } from './services/public-user';
|
export { PublicUserService } from './services/public-user';
|
||||||
export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
|
export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
|
||||||
@@ -33,6 +33,7 @@ export { WorkspaceServerService } from './services/workspace-server';
|
|||||||
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
||||||
export type { ServerConfig } from './types';
|
export type { ServerConfig } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line simple-import-sort/imports
|
||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { DocScope } from '../doc/scopes/doc';
|
import { DocScope } from '../doc/scopes/doc';
|
||||||
@@ -56,7 +57,7 @@ import { configureDefaultAuthProvider } from './impl/auth';
|
|||||||
import { AuthProvider } from './provider/auth';
|
import { AuthProvider } from './provider/auth';
|
||||||
import { ValidatorProvider } from './provider/validator';
|
import { ValidatorProvider } from './provider/validator';
|
||||||
import { ServerScope } from './scopes/server';
|
import { ServerScope } from './scopes/server';
|
||||||
import { AcceptInviteService } from './services/accept-invite';
|
import { InvitationService } from './services/invitation';
|
||||||
import { AuthService } from './services/auth';
|
import { AuthService } from './services/auth';
|
||||||
import { BlocksuiteWriterInfoService } from './services/blocksuite-writer-info';
|
import { BlocksuiteWriterInfoService } from './services/blocksuite-writer-info';
|
||||||
import { CaptchaService } from './services/captcha';
|
import { CaptchaService } from './services/captcha';
|
||||||
@@ -153,7 +154,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
|
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
|
||||||
.store(SelfhostGenerateLicenseStore, [GraphQLService])
|
.store(SelfhostGenerateLicenseStore, [GraphQLService])
|
||||||
.store(InviteInfoStore, [GraphQLService])
|
.store(InviteInfoStore, [GraphQLService])
|
||||||
.service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore])
|
.service(InvitationService, [AcceptInviteStore, InviteInfoStore])
|
||||||
.store(AcceptInviteStore, [GraphQLService])
|
.store(AcceptInviteStore, [GraphQLService])
|
||||||
.service(PublicUserService, [PublicUserStore])
|
.service(PublicUserService, [PublicUserStore])
|
||||||
.store(PublicUserStore, [GraphQLService])
|
.store(PublicUserStore, [GraphQLService])
|
||||||
|
|||||||
+13
-26
@@ -16,20 +16,19 @@ import type { InviteInfoStore } from '../stores/invite-info';
|
|||||||
|
|
||||||
export type InviteInfo = GetInviteInfoQuery['getInviteInfo'];
|
export type InviteInfo = GetInviteInfoQuery['getInviteInfo'];
|
||||||
|
|
||||||
export class AcceptInviteService extends Service {
|
export class InvitationService extends Service {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly store: AcceptInviteStore,
|
private readonly acceptInviteStore: AcceptInviteStore,
|
||||||
private readonly inviteInfoStore: InviteInfoStore
|
private readonly inviteInfoStore: InviteInfoStore
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
inviteId$ = new LiveData<string | undefined>(undefined);
|
inviteId$ = new LiveData<string | undefined>(undefined);
|
||||||
inviteInfo$ = new LiveData<InviteInfo | undefined>(undefined);
|
inviteInfo$ = new LiveData<InviteInfo | undefined>(undefined);
|
||||||
accepted$ = new LiveData<boolean>(false);
|
|
||||||
loading$ = new LiveData(false);
|
loading$ = new LiveData(false);
|
||||||
error$ = new LiveData<any>(null);
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
readonly acceptInvite = effect(
|
readonly getInviteInfo = effect(
|
||||||
switchMap(({ inviteId }: { inviteId: string }) => {
|
switchMap(({ inviteId }: { inviteId: string }) => {
|
||||||
if (!inviteId) {
|
if (!inviteId) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
@@ -39,16 +38,6 @@ export class AcceptInviteService extends Service {
|
|||||||
}).pipe(
|
}).pipe(
|
||||||
mergeMap(res => {
|
mergeMap(res => {
|
||||||
this.inviteInfo$.setValue(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;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
smartRetry({
|
smartRetry({
|
||||||
@@ -59,7 +48,6 @@ export class AcceptInviteService extends Service {
|
|||||||
this.inviteId$.setValue(inviteId);
|
this.inviteId$.setValue(inviteId);
|
||||||
this.loading$.setValue(true);
|
this.loading$.setValue(true);
|
||||||
this.inviteInfo$.setValue(undefined);
|
this.inviteInfo$.setValue(undefined);
|
||||||
this.accepted$.setValue(false);
|
|
||||||
}),
|
}),
|
||||||
onComplete(() => {
|
onComplete(() => {
|
||||||
this.loading$.setValue(false);
|
this.loading$.setValue(false);
|
||||||
@@ -68,21 +56,20 @@ export class AcceptInviteService extends Service {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
async waitForAcceptInvite(inviteId: string) {
|
async acceptInvite(inviteId: string) {
|
||||||
this.acceptInvite({ inviteId });
|
this.getInviteInfo({ inviteId });
|
||||||
await this.loading$.waitFor(f => !f);
|
await this.loading$.waitFor(f => !f);
|
||||||
if (this.accepted$.value) {
|
if (!this.inviteInfo$.value) {
|
||||||
return true; // invite is accepted
|
throw new Error('Invalid invite id');
|
||||||
}
|
}
|
||||||
|
return await this.acceptInviteStore.acceptInvite(
|
||||||
if (this.error$.value) {
|
this.inviteInfo$.value.workspace.id,
|
||||||
throw this.error$.value;
|
inviteId,
|
||||||
}
|
true
|
||||||
|
);
|
||||||
return false; // invite is expired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.acceptInvite.unsubscribe();
|
this.getInviteInfo.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"el-GR": 94,
|
"el-GR": 94,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-AR": 94,
|
"es-AR": 94,
|
||||||
"es-CL": 96,
|
"es-CL": 95,
|
||||||
"es": 94,
|
"es": 94,
|
||||||
"fa": 94,
|
"fa": 94,
|
||||||
"fr": 94,
|
"fr": 94,
|
||||||
|
|||||||
@@ -7203,6 +7203,18 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Please contact your workspace owner to add more seats.`
|
* `Please contact your workspace owner to add more seats.`
|
||||||
*/
|
*/
|
||||||
["com.affine.fail-to-join-workspace.description-2"](): string;
|
["com.affine.fail-to-join-workspace.description-2"](): string;
|
||||||
|
/**
|
||||||
|
* `Request to join`
|
||||||
|
*/
|
||||||
|
["com.affine.request-to-join-workspace.button"](): string;
|
||||||
|
/**
|
||||||
|
* `Request Sent successfully`
|
||||||
|
*/
|
||||||
|
["com.affine.sent-request-to-join-workspace.title"](): string;
|
||||||
|
/**
|
||||||
|
* `Request failed to send`
|
||||||
|
*/
|
||||||
|
["com.affine.failed-to-send-request.title"](): string;
|
||||||
/**
|
/**
|
||||||
* `Readwise`
|
* `Readwise`
|
||||||
*/
|
*/
|
||||||
@@ -8445,6 +8457,28 @@ export const TypedTrans: {
|
|||||||
["1"]: JSX.Element;
|
["1"]: JSX.Element;
|
||||||
["2"]: JSX.Element;
|
["2"]: JSX.Element;
|
||||||
}>>;
|
}>>;
|
||||||
|
/**
|
||||||
|
* `You requested to join <1/> <2>{{workspaceName}}</2> with <3>{{userEmail}}</3>, the workspace owner and team admins will review your request.`
|
||||||
|
*/
|
||||||
|
["com.affine.sent-request-to-join-workspace.description"]: ComponentType<TypedTransProps<Readonly<{
|
||||||
|
workspaceName: string;
|
||||||
|
userEmail: string;
|
||||||
|
}>, {
|
||||||
|
["1"]: JSX.Element;
|
||||||
|
["2"]: JSX.Element;
|
||||||
|
["3"]: JSX.Element;
|
||||||
|
}>>;
|
||||||
|
/**
|
||||||
|
* `Unable to process your request to join <1/> <2>{{workspaceName}}</2> with <3>{{userEmail}}</3>, the workspace has reached its member limit. Please contact the workspace owner for available seats.`
|
||||||
|
*/
|
||||||
|
["com.affine.failed-to-send-request.description"]: ComponentType<TypedTransProps<Readonly<{
|
||||||
|
workspaceName: string;
|
||||||
|
userEmail: string;
|
||||||
|
}>, {
|
||||||
|
["1"]: JSX.Element;
|
||||||
|
["2"]: JSX.Element;
|
||||||
|
["3"]: JSX.Element;
|
||||||
|
}>>;
|
||||||
/**
|
/**
|
||||||
* `Import your Readwise highlights to AFFiNE. Please visit Readwise, click <a>"Get Access Token"</a>, and paste the token below.`
|
* `Import your Readwise highlights to AFFiNE. Please visit Readwise, click <a>"Get Access Token"</a>, and paste the token below.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1792,6 +1792,11 @@
|
|||||||
"com.affine.fail-to-join-workspace.title": "Join Failed",
|
"com.affine.fail-to-join-workspace.title": "Join Failed",
|
||||||
"com.affine.fail-to-join-workspace.description-1": "Unable to join <1/> <2>{{workspaceName}}</2> due to insufficient seats available.",
|
"com.affine.fail-to-join-workspace.description-1": "Unable to join <1/> <2>{{workspaceName}}</2> due to insufficient seats available.",
|
||||||
"com.affine.fail-to-join-workspace.description-2": "Please contact your workspace owner to add more seats.",
|
"com.affine.fail-to-join-workspace.description-2": "Please contact your workspace owner to add more seats.",
|
||||||
|
"com.affine.request-to-join-workspace.button": "Request to join",
|
||||||
|
"com.affine.sent-request-to-join-workspace.title": "Request Sent successfully",
|
||||||
|
"com.affine.sent-request-to-join-workspace.description": "You requested to join <1/> <2>{{workspaceName}}</2> with <3>{{userEmail}}</3>, the workspace owner and team admins will review your request.",
|
||||||
|
"com.affine.failed-to-send-request.title": "Request failed to send",
|
||||||
|
"com.affine.failed-to-send-request.description": "Unable to process your request to join <1/> <2>{{workspaceName}}</2> with <3>{{userEmail}}</3>, the workspace has reached its member limit. Please contact the workspace owner for available seats.",
|
||||||
"com.affine.integration.name.readwise": "Readwise",
|
"com.affine.integration.name.readwise": "Readwise",
|
||||||
"com.affine.integration.integrations": "Integrations",
|
"com.affine.integration.integrations": "Integrations",
|
||||||
"com.affine.integration.setting.description": "Elevate your AFFiNE experience with diverse add-ons and seamless integrations.",
|
"com.affine.integration.setting.description": "Elevate your AFFiNE experience with diverse add-ons and seamless integrations.",
|
||||||
|
|||||||
Reference in New Issue
Block a user