From 014556b61fb72a182c1bae378d889e2e07f8bd9c Mon Sep 17 00:00:00 2001 From: JimmFly Date: Wed, 26 Mar 2025 02:45:12 +0000 Subject: [PATCH] feat(core): impl invitation link (#11181) feat(core): add invitee to getInviteInfoQuery feat(core): enable invitation link refactor(core): replace AcceptInviteService to InvitationService --- .../graphql/src/graphql/get-invite-info.gql | 6 ++ packages/common/graphql/src/graphql/index.ts | 6 ++ packages/common/graphql/src/schema.ts | 7 ++ .../member-components/failed-to-send-page.tsx | 48 +++++++++ .../components/member-components/index.tsx | 3 + .../invite-team-modal/link-invite.tsx | 99 ++++++++++--------- .../invite-team-modal/modal-content.tsx | 9 +- .../invite-team-modal/styles.css.ts | 11 +++ .../member-components/join-failed-page.tsx | 2 +- .../request-to-join-page.tsx | 71 +++++++++++++ .../member-components/sent-request-page.tsx | 48 +++++++++ .../member-components/styles.css.ts | 20 +++- .../core/src/components/notification/list.tsx | 10 +- .../core/src/desktop/pages/invite/index.tsx | 98 +++++++++++++----- .../frontend/core/src/modules/cloud/index.ts | 7 +- .../{accept-invite.ts => invitation.ts} | 39 +++----- .../i18n/src/i18n-completenesses.json | 2 +- packages/frontend/i18n/src/i18n.gen.ts | 34 +++++++ packages/frontend/i18n/src/resources/en.json | 5 + 19 files changed, 409 insertions(+), 116 deletions(-) create mode 100644 packages/frontend/component/src/components/member-components/failed-to-send-page.tsx create mode 100644 packages/frontend/component/src/components/member-components/request-to-join-page.tsx create mode 100644 packages/frontend/component/src/components/member-components/sent-request-page.tsx rename packages/frontend/core/src/modules/cloud/services/{accept-invite.ts => invitation.ts} (65%) diff --git a/packages/common/graphql/src/graphql/get-invite-info.gql b/packages/common/graphql/src/graphql/get-invite-info.gql index faee37e8d..21f347766 100644 --- a/packages/common/graphql/src/graphql/get-invite-info.gql +++ b/packages/common/graphql/src/graphql/get-invite-info.gql @@ -11,5 +11,11 @@ query getInviteInfo($inviteId: String!) { avatarUrl } status + invitee { + id + name + email + avatarUrl + } } } diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index b439f096b..cdca835f0 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -776,6 +776,12 @@ export const getInviteInfoQuery = { avatarUrl } status + invitee { + id + name + email + avatarUrl + } } }`, }; diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 89354ca07..8972af76a 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -3242,6 +3242,13 @@ export type GetInviteInfoQuery = { name: string; avatarUrl: string | null; }; + invitee: { + __typename?: 'WorkspaceUserType'; + id: string; + name: string; + email: string; + avatarUrl: string | null; + }; }; }; diff --git a/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx b/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx new file mode 100644 index 000000000..525816e56 --- /dev/null +++ b/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx @@ -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 ( + + + + + ), + 2: , + 3: , + }} + values={{ + workspaceName: inviteInfo.workspace.name, + userEmail: user?.email, + }} + /> + + } + > + ); +}; diff --git a/packages/frontend/component/src/components/member-components/index.tsx b/packages/frontend/component/src/components/member-components/index.tsx index 23d2f8531..034ed6b9c 100644 --- a/packages/frontend/component/src/components/member-components/index.tsx +++ b/packages/frontend/component/src/components/member-components/index.tsx @@ -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'; diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx b/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx index de33f10e6..759fd6b6f 100644 --- a/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx +++ b/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx @@ -113,53 +113,62 @@ export const LinkInvite = ({ return ( <> -
- {t['com.affine.payment.member.team.invite.link-expiration']()} -
- - - {currentSelectedLabel} - - -
- {t['com.affine.payment.member.team.invite.invitation-link']()} -
-
- +
+ {t['com.affine.payment.member.team.invite.link-expiration']()} +
+ - {invitationLink ? ( - <> - +
+
+
+ {t['com.affine.payment.member.team.invite.invitation-link']()} +
+
+ + {invitationLink ? ( + <> + + } onClick={onReset} /> + + ) : ( + - } onClick={onReset} /> - - ) : ( - - )} + )} +
+

+ {t[ + 'com.affine.payment.member.team.invite.invitation-link.description' + ]()} +

); diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx b/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx index 8670b9c1c..d6f6f93ad 100644 --- a/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx +++ b/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx @@ -15,7 +15,7 @@ export const ModalContent = ({ inviteEmail, setInviteEmail, inviteMethod, - // onInviteMethodChange, + onInviteMethodChange, handleConfirm, isMutating, isValidEmail, @@ -48,7 +48,7 @@ export const ModalContent = ({ } - 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', - }, }, ]} /> diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts b/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts index e3ddb5a76..4e8e76723 100644 --- a/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts +++ b/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts @@ -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'), +}); diff --git a/packages/frontend/component/src/components/member-components/join-failed-page.tsx b/packages/frontend/component/src/components/member-components/join-failed-page.tsx index 3f44eb493..71e86ab4d 100644 --- a/packages/frontend/component/src/components/member-components/join-failed-page.tsx +++ b/packages/frontend/component/src/components/member-components/join-failed-page.tsx @@ -20,7 +20,7 @@ export const JoinFailedPage = ({ title={t['com.affine.fail-to-join-workspace.title']()} subtitle={ userFriendlyError.name === ErrorNames.MEMBER_QUOTA_EXCEEDED ? ( -
+
void; + onSignOut: () => void; +}) => { + const t = useI18n(); + + return ( + +
+ + {inviteInfo.user.name} +
+
{t['invited you to join']()}
+
+ + + {inviteInfo.workspace.name} + +
+
+ } + > + + {user ? ( +
+ + {user.email} + + + +
+ ) : null} + + ); +}; diff --git a/packages/frontend/component/src/components/member-components/sent-request-page.tsx b/packages/frontend/component/src/components/member-components/sent-request-page.tsx new file mode 100644 index 000000000..f88a16b63 --- /dev/null +++ b/packages/frontend/component/src/components/member-components/sent-request-page.tsx @@ -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 ( + + + +
+ ), + 2: , + 3: , + }} + values={{ + workspaceName: inviteInfo.workspace.name, + userEmail: user?.email, + }} + /> + + } + > + ); +}; diff --git a/packages/frontend/component/src/components/member-components/styles.css.ts b/packages/frontend/component/src/components/member-components/styles.css.ts index 6b377e6e9..3d4e597da 100644 --- a/packages/frontend/component/src/components/member-components/styles.css.ts +++ b/packages/frontend/component/src/components/member-components/styles.css.ts @@ -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', }); diff --git a/packages/frontend/core/src/components/notification/list.tsx b/packages/frontend/core/src/components/notification/list.tsx index 5b2157d78..1e7f83efb 100644 --- a/packages/frontend/core/src/components/notification/list.tsx +++ b/packages/frontend/core/src/components/notification/list.tsx @@ -6,7 +6,7 @@ import { Scrollable, Skeleton, } from '@affine/component'; -import { AcceptInviteService } from '@affine/core/modules/cloud'; +import { InvitationService } from '@affine/core/modules/cloud'; import { type Notification, NotificationListService, @@ -314,7 +314,7 @@ const InvitationNotificationItem = ({ const memberInactived = !body.createdByUser; const workspaceInactived = !body.workspace; const workspacesService = useService(WorkspacesService); - const acceptInviteService = useService(AcceptInviteService); + const invitationService = useService(InvitationService); const notificationListService = useService(NotificationListService); const inviteId = body.inviteId; const [isAccepting, setIsAccepting] = useState(false); @@ -347,8 +347,8 @@ const InvitationNotificationItem = ({ const handleAcceptInvite = useCallback(() => { setIsAccepting(true); - acceptInviteService - .waitForAcceptInvite(inviteId) + invitationService + .acceptInvite(inviteId) .catch(err => { const userFriendlyError = UserFriendlyError.fromAny(err); if (userFriendlyError.is('ALREADY_IN_SPACE')) { @@ -384,7 +384,7 @@ const InvitationNotificationItem = ({ setIsAccepting(false); }); }, [ - acceptInviteService, + invitationService, handleReadAndOpenWorkspace, inviteId, notification, diff --git a/packages/frontend/core/src/desktop/pages/invite/index.tsx b/packages/frontend/core/src/desktop/pages/invite/index.tsx index aa0aaba13..3242d588e 100644 --- a/packages/frontend/core/src/desktop/pages/invite/index.tsx +++ b/packages/frontend/core/src/desktop/pages/invite/index.tsx @@ -1,31 +1,39 @@ import { AcceptInvitePage, ExpiredPage, + FailedToSendPage, JoinFailedPage, + RequestToJoinPage, + SentRequestPage, } from '@affine/component/member-components'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { WorkspacesService } from '@affine/core/modules/workspace'; import { UserFriendlyError } from '@affine/error'; +import { WorkspaceMemberStatus } from '@affine/graphql'; 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 { RouteLogic, useNavigateHelper, } 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 { jumpToPage } = useNavigateHelper(); - const acceptInviteService = useService(AcceptInviteService); + + const invitationService = useService(InvitationService); const workspacesService = useService(WorkspacesService); - const error = useLiveData(acceptInviteService.error$); - const inviteId = useLiveData(acceptInviteService.inviteId$); - const inviteInfo = useLiveData(acceptInviteService.inviteInfo$); - const accepted = useLiveData(acceptInviteService.accepted$); - const loading = useLiveData(acceptInviteService.loading$); + const authService = useService(AuthService); + const user = useLiveData(authService.session.account$); + const error = useLiveData(invitationService.error$); + const inviteId = useLiveData(invitationService.inviteId$); + const inviteInfo = useLiveData(invitationService.inviteInfo$); + const loading = useLiveData(invitationService.loading$); + const workspaces = useLiveData(workspacesService.list.workspaces$); const navigateHelper = useNavigateHelper(); + const [accepted, setAccepted] = useState(false); const openWorkspace = useAsyncCallback(async () => { if (!inviteInfo?.workspace.id) { @@ -40,26 +48,40 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => { }, [navigateHelper]); useEffect(() => { - acceptInviteService.acceptInvite({ - inviteId: targetInviteId, - }); - }, [acceptInviteService, targetInviteId]); - - useEffect(() => { - if (error && inviteId === targetInviteId) { - const err = UserFriendlyError.fromAny(error); - if (err.is('ALREADY_IN_SPACE')) { - return openWorkspace(); - } + // if workspace already exists, open it + if ( + !accepted && + inviteInfo?.workspace.id && + workspaces.some(w => w.id === inviteInfo.workspace.id) + ) { + 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) { return null; } if (!inviteInfo) { - // if invite is expired return ; } @@ -67,17 +89,35 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => { return ; } - if (accepted) { + // for email invite + if (accepted && inviteInfo.status === WorkspaceMemberStatus.Accepted) { return ( ); - } else { - // invite is expired - return ; } + + if (inviteInfo.status === WorkspaceMemberStatus.UnderReview) { + return ; + } + + if ( + inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeatAndReview || + inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeat + ) { + return ; + } + + return ( + + ); }; /** @@ -87,13 +127,17 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => { */ export const Component = () => { const authService = useService(AuthService); + const invitationService = useService(InvitationService); const isRevalidating = useLiveData(authService.session.isRevalidating$); const loginStatus = useLiveData(authService.session.status$); const params = useParams<{ inviteId: string }>(); useEffect(() => { authService.session.revalidate(); - }, [authService]); + if (params.inviteId) { + invitationService.getInviteInfo({ inviteId: params.inviteId }); + } + }, [authService, invitationService, params.inviteId]); const { jumpToSignIn } = useNavigateHelper(); diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index fa11f6781..1541b50ee 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -7,13 +7,13 @@ export { AccountLoggedOut } from './events/account-logged-out'; export { AuthProvider } from './provider/auth'; export { ValidatorProvider } from './provider/validator'; export { ServerScope } from './scopes/server'; -export { AcceptInviteService } from './services/accept-invite'; export { AuthService } from './services/auth'; export { CaptchaService } from './services/captcha'; export { DefaultServerService } from './services/default-server'; export { EventSourceService } from './services/eventsource'; export { FetchService } from './services/fetch'; export { GraphQLService } from './services/graphql'; +export { InvitationService } from './services/invitation'; export { InvoicesService } from './services/invoices'; export { PublicUserService } from './services/public-user'; export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license'; @@ -33,6 +33,7 @@ export { WorkspaceServerService } from './services/workspace-server'; export { WorkspaceSubscriptionService } from './services/workspace-subscription'; export type { ServerConfig } from './types'; +// eslint-disable-next-line simple-import-sort/imports import { type Framework } from '@toeverything/infra'; import { DocScope } from '../doc/scopes/doc'; @@ -56,7 +57,7 @@ import { configureDefaultAuthProvider } from './impl/auth'; import { AuthProvider } from './provider/auth'; import { ValidatorProvider } from './provider/validator'; import { ServerScope } from './scopes/server'; -import { AcceptInviteService } from './services/accept-invite'; +import { InvitationService } from './services/invitation'; import { AuthService } from './services/auth'; import { BlocksuiteWriterInfoService } from './services/blocksuite-writer-info'; import { CaptchaService } from './services/captcha'; @@ -153,7 +154,7 @@ export function configureCloudModule(framework: Framework) { .service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore]) .store(SelfhostGenerateLicenseStore, [GraphQLService]) .store(InviteInfoStore, [GraphQLService]) - .service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore]) + .service(InvitationService, [AcceptInviteStore, InviteInfoStore]) .store(AcceptInviteStore, [GraphQLService]) .service(PublicUserService, [PublicUserStore]) .store(PublicUserStore, [GraphQLService]) diff --git a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts b/packages/frontend/core/src/modules/cloud/services/invitation.ts similarity index 65% rename from packages/frontend/core/src/modules/cloud/services/accept-invite.ts rename to packages/frontend/core/src/modules/cloud/services/invitation.ts index 30e414b38..072c29589 100644 --- a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts +++ b/packages/frontend/core/src/modules/cloud/services/invitation.ts @@ -16,20 +16,19 @@ import type { InviteInfoStore } from '../stores/invite-info'; export type InviteInfo = GetInviteInfoQuery['getInviteInfo']; -export class AcceptInviteService extends Service { +export class InvitationService extends Service { constructor( - private readonly store: AcceptInviteStore, + private readonly acceptInviteStore: AcceptInviteStore, private readonly inviteInfoStore: InviteInfoStore ) { super(); } inviteId$ = new LiveData(undefined); inviteInfo$ = new LiveData(undefined); - accepted$ = new LiveData(false); loading$ = new LiveData(false); error$ = new LiveData(null); - readonly acceptInvite = effect( + readonly getInviteInfo = effect( switchMap(({ inviteId }: { inviteId: string }) => { if (!inviteId) { return EMPTY; @@ -39,16 +38,6 @@ export class AcceptInviteService extends Service { }).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; }), smartRetry({ @@ -59,7 +48,6 @@ export class AcceptInviteService extends Service { this.inviteId$.setValue(inviteId); this.loading$.setValue(true); this.inviteInfo$.setValue(undefined); - this.accepted$.setValue(false); }), onComplete(() => { this.loading$.setValue(false); @@ -68,21 +56,20 @@ export class AcceptInviteService extends Service { }) ); - async waitForAcceptInvite(inviteId: string) { - this.acceptInvite({ inviteId }); + async acceptInvite(inviteId: string) { + this.getInviteInfo({ inviteId }); await this.loading$.waitFor(f => !f); - if (this.accepted$.value) { - return true; // invite is accepted + if (!this.inviteInfo$.value) { + throw new Error('Invalid invite id'); } - - if (this.error$.value) { - throw this.error$.value; - } - - return false; // invite is expired + return await this.acceptInviteStore.acceptInvite( + this.inviteInfo$.value.workspace.id, + inviteId, + true + ); } override dispose(): void { - this.acceptInvite.unsubscribe(); + this.getInviteInfo.unsubscribe(); } } diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 9d9eaeeee..b6b479bcd 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -6,7 +6,7 @@ "el-GR": 94, "en": 100, "es-AR": 94, - "es-CL": 96, + "es-CL": 95, "es": 94, "fa": 94, "fr": 94, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 262946d89..0467031b8 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7203,6 +7203,18 @@ export function useAFFiNEI18N(): { * `Please contact your workspace owner to add more seats.` */ ["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` */ @@ -8445,6 +8457,28 @@ export const TypedTrans: { ["1"]: JSX.Element; ["2"]: JSX.Element; }>>; + /** + * `You requested to join <1/> <2>{{workspaceName}} with <3>{{userEmail}}, the workspace owner and team admins will review your request.` + */ + ["com.affine.sent-request-to-join-workspace.description"]: ComponentType, { + ["1"]: JSX.Element; + ["2"]: JSX.Element; + ["3"]: JSX.Element; + }>>; + /** + * `Unable to process your request to join <1/> <2>{{workspaceName}} with <3>{{userEmail}}, the workspace has reached its member limit. Please contact the workspace owner for available seats.` + */ + ["com.affine.failed-to-send-request.description"]: ComponentType, { + ["1"]: JSX.Element; + ["2"]: JSX.Element; + ["3"]: JSX.Element; + }>>; /** * `Import your Readwise highlights to AFFiNE. Please visit Readwise, click "Get Access Token", and paste the token below.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 572836f18..8d2d0b819 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1792,6 +1792,11 @@ "com.affine.fail-to-join-workspace.title": "Join Failed", "com.affine.fail-to-join-workspace.description-1": "Unable to join <1/> <2>{{workspaceName}} due to insufficient seats available.", "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}} with <3>{{userEmail}}, 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}} with <3>{{userEmail}}, the workspace has reached its member limit. Please contact the workspace owner for available seats.", "com.affine.integration.name.readwise": "Readwise", "com.affine.integration.integrations": "Integrations", "com.affine.integration.setting.description": "Elevate your AFFiNE experience with diverse add-ons and seamless integrations.",