mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00: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:
@@ -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 './expired';
|
||||
export * from './failed-to-send-page';
|
||||
export * from './invite-modal';
|
||||
export * from './invite-team-modal';
|
||||
export * from './join-failed-page';
|
||||
export * from './member-limit-modal';
|
||||
export * from './request-to-join-page';
|
||||
export * from './sent-request-page';
|
||||
|
||||
@@ -113,53 +113,62 @@ export const LinkInvite = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalSubTitle}>
|
||||
{t['com.affine.payment.member.team.invite.link-expiration']()}
|
||||
</div>
|
||||
<Menu
|
||||
items={items}
|
||||
contentOptions={{
|
||||
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'),
|
||||
<div className={styles.invitationLinkContainer}>
|
||||
<div className={styles.modalSubTitle}>
|
||||
{t['com.affine.payment.member.team.invite.link-expiration']()}
|
||||
</div>
|
||||
<Menu
|
||||
items={items}
|
||||
contentOptions={{
|
||||
style: {
|
||||
width: 'var(--radix-dropdown-menu-trigger-width)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{invitationLink ? (
|
||||
<>
|
||||
<Button onClick={onCopy} variant="secondary">
|
||||
{t['com.affine.payment.member.team.invite.copy']()}
|
||||
>
|
||||
<MenuTrigger style={{ width: '100%' }}>
|
||||
{currentSelectedLabel}
|
||||
</MenuTrigger>
|
||||
</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>
|
||||
<IconButton icon={<CloseIcon />} onClick={onReset} />
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={onGenerate} variant="secondary">
|
||||
{t['com.affine.payment.member.team.invite.generate']()}
|
||||
</Button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<p className={styles.invitationLinkDescription}>
|
||||
{t[
|
||||
'com.affine.payment.member.team.invite.invitation-link.description'
|
||||
]()}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ModalContent = ({
|
||||
inviteEmail,
|
||||
setInviteEmail,
|
||||
inviteMethod,
|
||||
// onInviteMethodChange,
|
||||
onInviteMethodChange,
|
||||
handleConfirm,
|
||||
isMutating,
|
||||
isValidEmail,
|
||||
@@ -48,7 +48,7 @@ export const ModalContent = ({
|
||||
<RadioGroup
|
||||
width={'100%'}
|
||||
value={inviteMethod}
|
||||
// onChange={onInviteMethodChange}
|
||||
onChange={onInviteMethodChange}
|
||||
items={[
|
||||
{
|
||||
label: (
|
||||
@@ -65,13 +65,10 @@ export const ModalContent = ({
|
||||
label: (
|
||||
<RadioItem
|
||||
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',
|
||||
style: {
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -81,6 +81,12 @@ export const modalSubTitle = style({
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
export const invitationLinkContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const radioItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -107,3 +113,8 @@ export const invitationLinkContent = style({
|
||||
display: 'flex',
|
||||
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']()}
|
||||
subtitle={
|
||||
userFriendlyError.name === ErrorNames.MEMBER_QUOTA_EXCEEDED ? (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.lineHeight}>
|
||||
<Trans
|
||||
i18nKey={'com.affine.fail-to-join-workspace.description-1'}
|
||||
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({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
// marginTop: 10,
|
||||
});
|
||||
|
||||
export const inviteName = style({
|
||||
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({
|
||||
@@ -30,7 +46,7 @@ export const content = style({
|
||||
});
|
||||
|
||||
export const userWrapper = style({
|
||||
display: 'flex',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user