mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 02:56:23 +08:00
feat(infra): framework
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ import type { FC } from 'react';
|
||||
|
||||
export interface FallbackProps<T extends Error = Error> {
|
||||
error: T;
|
||||
resetError: () => void;
|
||||
resetError?: () => void;
|
||||
}
|
||||
|
||||
export const ERROR_REFLECT_KEY = Symbol('ERROR_REFLECT_KEY');
|
||||
|
||||
+7
-10
@@ -1,22 +1,20 @@
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceListService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { CurrentWorkspaceService } from '../../../../modules/workspace/current-workspace';
|
||||
|
||||
export interface DumpInfoProps {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export const DumpInfo = (_props: DumpInfoProps) => {
|
||||
const { globalContextService } = useServices({ GlobalContextService });
|
||||
const location = useLocation();
|
||||
const workspaceList = useService(WorkspaceListService);
|
||||
const currentWorkspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace$
|
||||
const currentWorkspaceId = useLiveData(
|
||||
globalContextService.globalContext.workspaceId.$
|
||||
);
|
||||
const path = location.pathname;
|
||||
const query = useParams();
|
||||
@@ -24,9 +22,8 @@ export const DumpInfo = (_props: DumpInfoProps) => {
|
||||
console.info('DumpInfo', {
|
||||
path,
|
||||
query,
|
||||
currentWorkspaceId: currentWorkspace?.id,
|
||||
workspaceList,
|
||||
currentWorkspaceId: currentWorkspaceId,
|
||||
});
|
||||
}, [path, query, currentWorkspace, workspaceList]);
|
||||
}, [path, query, currentWorkspaceId]);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { AiIcon } from '@blocksuite/icons';
|
||||
import { Doc, useLiveData, useService } from '@toeverything/infra';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import Lottie from 'lottie-react';
|
||||
@@ -39,17 +43,20 @@ const EdgelessOnboardingAnimation = () => {
|
||||
export const AIOnboardingEdgeless = ({
|
||||
onDismiss,
|
||||
}: BaseAIOnboardingDialogProps) => {
|
||||
const { workspaceService, docService } = useServices({
|
||||
WorkspaceService,
|
||||
DocService,
|
||||
});
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const notifyId = useLiveData(edgelessNotifyId$);
|
||||
const generalAIOnboardingOpened = useLiveData(showAIOnboardingGeneral$);
|
||||
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const currentWorkspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace$
|
||||
);
|
||||
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
const doc = useService(Doc);
|
||||
const doc = docService.doc;
|
||||
const mode = useLiveData(doc.mode$);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Button, Modal } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useBlurRoot } from '@affine/core/hooks/use-blur-root';
|
||||
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -65,14 +68,13 @@ const getPlayList = (t: Translate): Array<PlayListItem> => [
|
||||
export const AIOnboardingGeneral = ({
|
||||
onDismiss,
|
||||
}: BaseAIOnboardingDialogProps) => {
|
||||
const { workspaceService } = useServices({ WorkspaceService });
|
||||
|
||||
const videoWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const prevVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const currentWorkspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace$
|
||||
);
|
||||
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const t = useAFFiNEI18N();
|
||||
// const [open, setOpen] = useState(true);
|
||||
const open = useLiveData(showAIOnboardingGeneral$);
|
||||
const [index, setIndex] = useState(0);
|
||||
const list = useMemo(() => getPlayList(t), [t]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { notify } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
@@ -6,37 +7,68 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
import { useAuth } from './use-auth';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
import { useSubscriptionSearch } from './use-subscription';
|
||||
|
||||
export const AfterSignInSendEmail = ({
|
||||
setAuthState,
|
||||
email,
|
||||
onSignedIn,
|
||||
}: AuthPanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
const subscriptionData = useSubscriptionSearch();
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setResendCountDown(c => Math.max(c - 1, 0));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const authService = useService(AuthService);
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
authService.session.revalidate();
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [authService]);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
|
||||
const { resendCountDown, allowSendEmail, signIn } = useAuth();
|
||||
if (loginStatus === 'authenticated') {
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
if (verifyToken) {
|
||||
await signIn(email, verifyToken, challenge);
|
||||
setIsSending(true);
|
||||
try {
|
||||
if (verifyToken) {
|
||||
setResendCountDown(60);
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
});
|
||||
}
|
||||
}, [challenge, email, signIn, verifyToken]);
|
||||
setIsSending(false);
|
||||
}, [authService, challenge, email, verifyToken]);
|
||||
|
||||
const onSignInWithPasswordClick = useCallback(() => {
|
||||
setAuthState('signInWithPassword');
|
||||
@@ -62,12 +94,12 @@ export const AfterSignInSendEmail = ({
|
||||
</AuthContent>
|
||||
|
||||
<div className={style.resendWrapper}>
|
||||
{allowSendEmail ? (
|
||||
{resendCountDown <= 0 ? (
|
||||
<>
|
||||
<Captcha />
|
||||
<Button
|
||||
style={!verifyToken ? { cursor: 'not-allowed' } : {}}
|
||||
disabled={!verifyToken}
|
||||
disabled={!verifyToken || isSending}
|
||||
type="plain"
|
||||
size="large"
|
||||
onClick={onResendClick}
|
||||
@@ -90,23 +122,19 @@ export const AfterSignInSendEmail = ({
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{t['com.affine.auth.sign.auth.code.message']()}
|
||||
{subscriptionData ? null : ( // If with payment, just support email sign in to avoid duplicate redirect to the same stripe url.
|
||||
<React.Fragment>
|
||||
|
||||
<Trans
|
||||
i18nKey="com.affine.auth.sign.auth.code.message.password"
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
className="link"
|
||||
data-testid="sign-in-with-password"
|
||||
onClick={onSignInWithPasswordClick}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Trans
|
||||
i18nKey="com.affine.auth.sign.auth.code.message.password"
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
className="link"
|
||||
data-testid="sign-in-with-password"
|
||||
onClick={onSignInWithPasswordClick}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BackButton onClick={onBackBottomClick} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { notify } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
@@ -8,13 +9,13 @@ import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
import { useAuth } from './use-auth';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
|
||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
@@ -22,21 +23,52 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
email,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setResendCountDown(c => Math.max(c - 1, 0));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
|
||||
const { resendCountDown, allowSendEmail, signUp } = useAuth();
|
||||
|
||||
const authService = useService(AuthService);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
useEffect(() => {
|
||||
const timeout = setInterval(() => {
|
||||
// revalidate session to get the latest status
|
||||
authService.session.revalidate();
|
||||
}, 3000);
|
||||
return () => {
|
||||
clearInterval(timeout);
|
||||
};
|
||||
}, [authService]);
|
||||
if (loginStatus === 'authenticated') {
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
if (verifyToken) {
|
||||
await signUp(email, verifyToken, challenge);
|
||||
setIsSending(true);
|
||||
try {
|
||||
if (verifyToken) {
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
}
|
||||
setResendCountDown(60);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
});
|
||||
}
|
||||
}, [challenge, email, signUp, verifyToken]);
|
||||
setIsSending(false);
|
||||
}, [authService, challenge, email, verifyToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,12 +86,12 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
</AuthContent>
|
||||
|
||||
<div className={style.resendWrapper}>
|
||||
{allowSendEmail ? (
|
||||
{resendCountDown <= 0 ? (
|
||||
<>
|
||||
<Captcha />
|
||||
<Button
|
||||
style={!verifyToken ? { cursor: 'not-allowed' } : {}}
|
||||
disabled={!verifyToken}
|
||||
disabled={!verifyToken || isSending}
|
||||
type="plain"
|
||||
size="large"
|
||||
onClick={onResendClick}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||
import { NoAccess } from './no-access';
|
||||
import { SendEmail } from './send-email';
|
||||
import { SignIn } from './sign-in';
|
||||
import { SignInWithPassword } from './sign-in-with-password';
|
||||
@@ -17,8 +16,7 @@ export type AuthProps = {
|
||||
| 'afterSignInSendEmail'
|
||||
// throw away
|
||||
| 'signInWithPassword'
|
||||
| 'sendEmail'
|
||||
| 'noAccess';
|
||||
| 'sendEmail';
|
||||
setAuthState: (state: AuthProps['state']) => void;
|
||||
setAuthEmail: (state: AuthProps['email']) => void;
|
||||
setEmailType: (state: AuthProps['emailType']) => void;
|
||||
@@ -44,7 +42,6 @@ const config: {
|
||||
afterSignInSendEmail: AfterSignInSendEmail,
|
||||
signInWithPassword: SignInWithPassword,
|
||||
sendEmail: SendEmail,
|
||||
noAccess: NoAccess,
|
||||
};
|
||||
|
||||
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { NewIcon } from '@blocksuite/icons';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const NoAccess: FC<AuthPanelProps> = ({ setAuthState, onSignedIn }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.brand.affineCloud']()}
|
||||
subTitle={t['Early Access Stage']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 162 }}>
|
||||
{t['com.affine.auth.sign.no.access.hint']()}
|
||||
<a href="https://community.affine.pro/c/insider-general/">
|
||||
{t['com.affine.auth.sign.no.access.link']()}
|
||||
</a>
|
||||
</AuthContent>
|
||||
|
||||
<div className={style.accessMessage}>
|
||||
<NewIcon
|
||||
style={{
|
||||
fontSize: 16,
|
||||
marginRight: 4,
|
||||
color: 'var(--affine-icon-color)',
|
||||
}}
|
||||
/>
|
||||
{t['com.affine.auth.sign.no.access.wait']()}
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,14 @@
|
||||
import { notify, Skeleton } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import {
|
||||
useOAuthProviders,
|
||||
useServerFeatures,
|
||||
} from '@affine/core/hooks/affine/use-server-config';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AuthService, ServerConfigService } from '../../../modules/cloud';
|
||||
import { mixpanel } from '../../../utils';
|
||||
import { useAuth } from './use-auth';
|
||||
|
||||
const OAuthProviderMap: Record<
|
||||
OAuthProviderType,
|
||||
@@ -27,33 +26,43 @@ const OAuthProviderMap: Record<
|
||||
};
|
||||
|
||||
export function OAuth() {
|
||||
const { oauth } = useServerFeatures();
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
const oauth = useLiveData(serverConfig.features$.map(r => r?.oauth));
|
||||
const oauthProviders = useLiveData(
|
||||
serverConfig.config$.map(r => r?.oauthProviders)
|
||||
);
|
||||
|
||||
if (!oauth) {
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<Skeleton height={50} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <OAuthProviders />;
|
||||
}
|
||||
|
||||
function OAuthProviders() {
|
||||
const providers = useOAuthProviders();
|
||||
|
||||
return providers.map(provider => (
|
||||
return oauthProviders?.map(provider => (
|
||||
<OAuthProvider key={provider} provider={provider} />
|
||||
));
|
||||
}
|
||||
|
||||
function OAuthProvider({ provider }: { provider: OAuthProviderType }) {
|
||||
const { icon } = OAuthProviderMap[provider];
|
||||
const { oauthSignIn } = useAuth();
|
||||
const authService = useService(AuthService);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setIsConnecting(true);
|
||||
oauthSignIn(provider);
|
||||
mixpanel.track('OAuth', { provider });
|
||||
}, [provider, oauthSignIn]);
|
||||
const onClick = useAsyncCallback(async () => {
|
||||
try {
|
||||
setIsConnecting(true);
|
||||
await authService.signInOauth(provider);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({ message: 'Failed to sign in, please try again.' });
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
mixpanel.track('OAuth', { provider });
|
||||
}
|
||||
}, [authService, provider]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -6,19 +6,19 @@ import {
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import {
|
||||
type PasswordLimitsFragment,
|
||||
sendChangeEmailMutation,
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
sendVerifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useMutation } from '../../../hooks/use-mutation';
|
||||
import { ServerConfigService } from '../../../modules/cloud';
|
||||
import type { AuthPanelProps } from './index';
|
||||
|
||||
const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
@@ -35,28 +35,6 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
return t['com.affine.settings.email.action.verify']();
|
||||
}
|
||||
};
|
||||
const useContent = (
|
||||
emailType: AuthPanelProps['emailType'],
|
||||
email: string,
|
||||
passwordLimits: PasswordLimitsFragment
|
||||
) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.set.password.message']({
|
||||
min: String(passwordLimits.minLength),
|
||||
max: String(passwordLimits.maxLength),
|
||||
});
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password.message']();
|
||||
case 'changeEmail':
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.verify.email.message']({
|
||||
email,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -161,12 +139,15 @@ export const SendEmail = ({
|
||||
emailType,
|
||||
}: AuthPanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { password: passwordLimits } = useCredentialsRequirement();
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
|
||||
const passwordLimits = useLiveData(
|
||||
serverConfig.credentialsRequirement$.map(r => r?.password)
|
||||
);
|
||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||
|
||||
const title = useEmailTitle(emailType);
|
||||
const hint = useNotificationHint(emailType);
|
||||
const content = useContent(emailType, email, passwordLimits);
|
||||
const buttonContent = useButtonContent(emailType);
|
||||
const { loading, sendEmail } = useSendEmail(emailType);
|
||||
|
||||
@@ -178,6 +159,27 @@ export const SendEmail = ({
|
||||
setHasSentEmail(true);
|
||||
}, [email, hint, sendEmail]);
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState]);
|
||||
|
||||
if (!passwordLimits) {
|
||||
// TODO: loading & error UI
|
||||
return null;
|
||||
}
|
||||
|
||||
const content =
|
||||
emailType === 'setPassword'
|
||||
? t['com.affine.auth.set.password.message']({
|
||||
min: String(passwordLimits.minLength),
|
||||
max: String(passwordLimits.maxLength),
|
||||
})
|
||||
: emailType === 'changePassword'
|
||||
? t['com.affine.auth.reset.password.message']()
|
||||
: emailType === 'changeEmail' || emailType === 'verifyEmail'
|
||||
? t['com.affine.auth.verify.email.message']({ email })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
@@ -210,11 +212,7 @@ export const SendEmail = ({
|
||||
>
|
||||
{hasSentEmail ? t['com.affine.auth.sent']() : buttonContent}
|
||||
</Button>
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
<BackButton onClick={onBack} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import { notify, Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { signInCloud } from '../../../utils/cloud-utils';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as styles from './style.css';
|
||||
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
|
||||
import { useCaptcha } from './use-captcha';
|
||||
|
||||
export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
@@ -24,57 +23,49 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { reload } = useSession();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
const {
|
||||
signIn,
|
||||
allowSendEmail,
|
||||
resetCountDown,
|
||||
isMutating: sendingEmail,
|
||||
} = useAuth();
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [sendingEmail, setSendingEmail] = useState(false);
|
||||
|
||||
const onSignIn = useAsyncCallback(async () => {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await signInCloud('credentials', {
|
||||
email,
|
||||
password,
|
||||
}).catch(console.error);
|
||||
|
||||
if (res?.ok) {
|
||||
await reload();
|
||||
try {
|
||||
await authService.signInPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
onSignedIn?.();
|
||||
} else {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPasswordError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [email, password, isLoading, onSignedIn, reload]);
|
||||
}, [isLoading, authService, email, password, onSignedIn]);
|
||||
|
||||
const sendMagicLink = useAsyncCallback(async () => {
|
||||
if (allowSendEmail && verifyToken && !sendingEmail) {
|
||||
const res = await signIn(email, verifyToken, challenge);
|
||||
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
|
||||
resetCountDown();
|
||||
return setAuthState('noAccess');
|
||||
if (sendingEmail) return;
|
||||
setSendingEmail(true);
|
||||
try {
|
||||
if (verifyToken) {
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
}
|
||||
setAuthState('afterSignInSendEmail');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notify.error({
|
||||
message: 'Failed to send email, please try again.',
|
||||
});
|
||||
// TODO: handle error better
|
||||
}
|
||||
}, [
|
||||
email,
|
||||
signIn,
|
||||
allowSendEmail,
|
||||
sendingEmail,
|
||||
setAuthState,
|
||||
verifyToken,
|
||||
challenge,
|
||||
resetCountDown,
|
||||
]);
|
||||
setSendingEmail(false);
|
||||
}, [sendingEmail, verifyToken, authService, email, challenge, setAuthState]);
|
||||
|
||||
const sendChangePasswordEmail = useCallback(() => {
|
||||
setEmailType('changePassword');
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import {
|
||||
AuthInput,
|
||||
CountDownRender,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { notify } from '@affine/component';
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { GetUserQuery } from '@affine/graphql';
|
||||
import { findGraphQLError, getUserQuery } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowDownBigIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { useMutation } from '../../../hooks/use-mutation';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { mixpanel } from '../../../utils';
|
||||
import { emailRegex } from '../../../utils/email-regex';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import { OAuth } from './oauth';
|
||||
import * as style from './style.css';
|
||||
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
import { useSubscriptionSearch } from './use-subscription';
|
||||
|
||||
function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
@@ -35,100 +28,74 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
const subscriptionData = useSubscriptionSearch();
|
||||
|
||||
const {
|
||||
isMutating: isSigningIn,
|
||||
resendCountDown,
|
||||
allowSendEmail,
|
||||
signIn,
|
||||
signUp,
|
||||
} = useAuth();
|
||||
|
||||
const { trigger: verifyUser, isMutating } = useMutation({
|
||||
mutation: getUserQuery,
|
||||
});
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setInterval(() => {
|
||||
// revalidate session to get the latest status
|
||||
authService.session.revalidate();
|
||||
}, 3000);
|
||||
return () => {
|
||||
clearInterval(timeout);
|
||||
};
|
||||
}, [authService]);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
if (loginStatus === 'authenticated') {
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onContinue = useAsyncCallback(async () => {
|
||||
if (!allowSendEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
// 0 for no access for internal beta
|
||||
const user: GetUserQuery['user'] | null | 0 = await verifyUser({ email })
|
||||
.then(({ user }) => user)
|
||||
.catch(err => {
|
||||
if (findGraphQLError(err, e => e.extensions.code === 402)) {
|
||||
setAuthState('noAccess');
|
||||
return 0;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
if (user === 0) {
|
||||
return;
|
||||
}
|
||||
setIsMutating(true);
|
||||
|
||||
setAuthEmail(email);
|
||||
try {
|
||||
const { hasPassword, isExist: isUserExist } =
|
||||
await authService.checkUserByEmail(email);
|
||||
|
||||
if (verifyToken) {
|
||||
if (user) {
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (user.hasPassword && !subscriptionData) {
|
||||
setAuthState('signInWithPassword');
|
||||
if (verifyToken) {
|
||||
if (isUserExist) {
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (hasPassword) {
|
||||
setAuthState('signInWithPassword');
|
||||
} else {
|
||||
mixpanel.track_forms('SignIn', 'Email', {
|
||||
email,
|
||||
});
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
}
|
||||
} else {
|
||||
mixpanel.track_forms('SignIn', 'Email', {
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
mixpanel.track_forms('SignUp', 'Email', {
|
||||
email,
|
||||
});
|
||||
const res = await signIn(email, verifyToken, challenge);
|
||||
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
|
||||
return setAuthState('noAccess');
|
||||
}
|
||||
// TODO, should always get id from user
|
||||
if ('id' in user) {
|
||||
mixpanel.identify(user.id);
|
||||
}
|
||||
setAuthState('afterSignInSendEmail');
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
} else {
|
||||
const res = await signUp(email, verifyToken, challenge);
|
||||
mixpanel.track_forms('SignUp', 'Email', {
|
||||
email,
|
||||
});
|
||||
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
|
||||
return setAuthState('noAccess');
|
||||
} else if (!res || res.status >= 400) {
|
||||
return;
|
||||
}
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// TODO: better error handling
|
||||
notify.error({
|
||||
message: 'Failed to send email. Please try again.',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
allowSendEmail,
|
||||
subscriptionData,
|
||||
challenge,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
signIn,
|
||||
signUp,
|
||||
verifyToken,
|
||||
verifyUser,
|
||||
]);
|
||||
|
||||
setIsMutating(false);
|
||||
}, [authService, challenge, email, setAuthEmail, setAuthState, verifyToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -164,24 +131,16 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
size="extraLarge"
|
||||
data-testid="continue-login-button"
|
||||
block
|
||||
loading={isMutating || isSigningIn}
|
||||
disabled={!allowSendEmail}
|
||||
loading={isMutating}
|
||||
icon={
|
||||
allowSendEmail || isMutating ? (
|
||||
<ArrowDownBigIcon
|
||||
width={20}
|
||||
height={20}
|
||||
style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
color: 'var(--affine-blue)',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CountDownRender
|
||||
className={style.resendCountdownInButton}
|
||||
timeLeft={resendCountDown}
|
||||
/>
|
||||
)
|
||||
<ArrowDownBigIcon
|
||||
width={20}
|
||||
height={20}
|
||||
style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
color: 'var(--affine-blue)',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
onClick={onContinue}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { SignUpPage } from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
||||
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import { SubscriptionPlan, type SubscriptionRecurring } from '@affine/graphql';
|
||||
import {
|
||||
changePasswordMutation,
|
||||
createCheckoutSessionMutation,
|
||||
subscriptionQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useMutation } from '../../../hooks/use-mutation';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../hooks/use-navigate-helper';
|
||||
import { useQuery } from '../../../hooks/use-query';
|
||||
import * as styles from './subscription-redirect.css';
|
||||
import { useSubscriptionSearch } from './use-subscription';
|
||||
|
||||
const usePaymentRedirect = () => {
|
||||
const searchData = useSubscriptionSearch();
|
||||
if (!searchData?.recurring) {
|
||||
throw new Error('Invalid recurring data.');
|
||||
}
|
||||
|
||||
const recurring = searchData.recurring as SubscriptionRecurring;
|
||||
const plan = searchData.plan as SubscriptionPlan;
|
||||
const coupon = searchData.coupon;
|
||||
const idempotencyKey = useMemo(() => nanoid(), []);
|
||||
const { trigger: checkoutSubscription } = useMutation({
|
||||
mutation: createCheckoutSessionMutation,
|
||||
});
|
||||
|
||||
return useAsyncCallback(async () => {
|
||||
const { createCheckoutSession: checkoutUrl } = await checkoutSubscription({
|
||||
input: {
|
||||
recurring,
|
||||
plan,
|
||||
coupon,
|
||||
idempotencyKey,
|
||||
successCallbackLink: null,
|
||||
},
|
||||
});
|
||||
popupWindow(checkoutUrl);
|
||||
}, [recurring, plan, coupon, idempotencyKey, checkoutSubscription]);
|
||||
};
|
||||
|
||||
const CenterLoading = () => {
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Loading size={40} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscriptionExisting = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
return (
|
||||
<div className={styles.subscriptionLayout}>
|
||||
<div className={styles.subscriptionBox}>
|
||||
<AffineShapeIcon width={180} height={180} />
|
||||
<p className={styles.subscriptionTips}>
|
||||
{t['com.affine.payment.subscription.exist']()}
|
||||
</p>
|
||||
<Button
|
||||
data-testid="upgrade-workspace-button"
|
||||
onClick={onButtonClick}
|
||||
size="extraLarge"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.auth.open.affine']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscriptionRedirection = ({ redirect }: { redirect: () => void }) => {
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
redirect();
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [redirect]);
|
||||
|
||||
return <CenterLoading />;
|
||||
};
|
||||
|
||||
const SubscriptionRedirectWithData = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const searchData = useSubscriptionSearch();
|
||||
const openPaymentUrl = usePaymentRedirect();
|
||||
const { password: passwordLimits } = useCredentialsRequirement();
|
||||
|
||||
const { trigger: changePassword } = useMutation({
|
||||
mutation: changePasswordMutation,
|
||||
});
|
||||
const { data: subscriptionData } = useQuery({
|
||||
query: subscriptionQuery,
|
||||
});
|
||||
|
||||
const onSetPassword = useCallback(
|
||||
async (password: string) => {
|
||||
await changePassword({
|
||||
token: searchData?.passwordToken ?? '',
|
||||
newPassword: password,
|
||||
});
|
||||
},
|
||||
[changePassword, searchData]
|
||||
);
|
||||
|
||||
if (searchData?.withSignUp) {
|
||||
return (
|
||||
<SignUpPage
|
||||
user={user}
|
||||
passwordLimits={passwordLimits}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={openPaymentUrl}
|
||||
openButtonText={t['com.affine.payment.subscription.go-to-subscribe']()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
subscriptionData.currentUser?.subscriptions?.some(
|
||||
sub => sub.plan === SubscriptionPlan.Pro
|
||||
)
|
||||
) {
|
||||
return <SubscriptionExisting />;
|
||||
}
|
||||
|
||||
return <SubscriptionRedirection redirect={openPaymentUrl} />;
|
||||
};
|
||||
|
||||
export const SubscriptionRedirect = () => {
|
||||
return (
|
||||
<Suspense fallback={<CenterLoading />}>
|
||||
<SubscriptionRedirectWithData />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { signInCloud } from '../../../utils/cloud-utils';
|
||||
import { useSubscriptionSearch } from './use-subscription';
|
||||
|
||||
const COUNT_DOWN_TIME = 60;
|
||||
export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
|
||||
|
||||
type AuthStoreAtom = {
|
||||
allowSendEmail: boolean;
|
||||
resendCountDown: number;
|
||||
isMutating: boolean;
|
||||
};
|
||||
|
||||
export const authStoreAtom = atom<AuthStoreAtom>({
|
||||
isMutating: false,
|
||||
allowSendEmail: true,
|
||||
resendCountDown: COUNT_DOWN_TIME,
|
||||
});
|
||||
|
||||
const countDownAtom = atom(
|
||||
null, // it's a convention to pass `null` for the first argument
|
||||
(get, set) => {
|
||||
const clearId = window.setInterval(() => {
|
||||
const countDown = get(authStoreAtom).resendCountDown;
|
||||
if (countDown === 0) {
|
||||
set(authStoreAtom, {
|
||||
isMutating: false,
|
||||
allowSendEmail: true,
|
||||
resendCountDown: COUNT_DOWN_TIME,
|
||||
});
|
||||
window.clearInterval(clearId);
|
||||
return;
|
||||
}
|
||||
set(authStoreAtom, {
|
||||
isMutating: false,
|
||||
resendCountDown: countDown - 1,
|
||||
allowSendEmail: false,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
);
|
||||
|
||||
export const useAuth = () => {
|
||||
const subscriptionData = useSubscriptionSearch();
|
||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
||||
const startResendCountDown = useSetAtom(countDownAtom);
|
||||
|
||||
const sendEmailMagicLink = useCallback(
|
||||
async (
|
||||
signUp: boolean,
|
||||
email: string,
|
||||
verifyToken: string,
|
||||
challenge?: string
|
||||
) => {
|
||||
setAuthStore(prev => {
|
||||
return {
|
||||
...prev,
|
||||
isMutating: true,
|
||||
};
|
||||
});
|
||||
|
||||
const res = await signInCloud(
|
||||
'email',
|
||||
{
|
||||
email,
|
||||
},
|
||||
{
|
||||
...(challenge
|
||||
? {
|
||||
challenge,
|
||||
token: verifyToken,
|
||||
}
|
||||
: { token: verifyToken }),
|
||||
callbackUrl: subscriptionData
|
||||
? subscriptionData.getRedirectUrl(signUp)
|
||||
: '/auth/signIn',
|
||||
}
|
||||
).catch(console.error);
|
||||
|
||||
if (!res?.ok) {
|
||||
// TODO: i18n
|
||||
notify.error({
|
||||
title: 'Send email error',
|
||||
message: 'Please back to home and try again',
|
||||
});
|
||||
}
|
||||
|
||||
setAuthStore({
|
||||
isMutating: false,
|
||||
allowSendEmail: false,
|
||||
resendCountDown: COUNT_DOWN_TIME,
|
||||
});
|
||||
|
||||
// TODO: when errored, should reset the count down
|
||||
startResendCountDown();
|
||||
|
||||
return res;
|
||||
},
|
||||
[setAuthStore, startResendCountDown, subscriptionData]
|
||||
);
|
||||
|
||||
const signUp = useCallback(
|
||||
async (email: string, verifyToken: string, challenge?: string) => {
|
||||
return sendEmailMagicLink(true, email, verifyToken, challenge).catch(
|
||||
console.error
|
||||
);
|
||||
},
|
||||
[sendEmailMagicLink]
|
||||
);
|
||||
|
||||
const signIn = useCallback(
|
||||
async (email: string, verifyToken: string, challenge?: string) => {
|
||||
return sendEmailMagicLink(false, email, verifyToken, challenge).catch(
|
||||
console.error
|
||||
);
|
||||
},
|
||||
[sendEmailMagicLink]
|
||||
);
|
||||
|
||||
const oauthSignIn = useCallback((provider: OAuthProviderType) => {
|
||||
signInCloud(provider).catch(console.error);
|
||||
}, []);
|
||||
|
||||
const resetCountDown = useCallback(() => {
|
||||
setAuthStore({
|
||||
isMutating: false,
|
||||
allowSendEmail: false,
|
||||
resendCountDown: 0,
|
||||
});
|
||||
}, [setAuthStore]);
|
||||
|
||||
return {
|
||||
allowSendEmail: authStore.allowSendEmail,
|
||||
resendCountDown: authStore.resendCountDown,
|
||||
resetCountDown,
|
||||
isMutating: authStore.isMutating,
|
||||
signUp,
|
||||
signIn,
|
||||
oauthSignIn,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { fetchWithTraceReport } from '@affine/graphql';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { useEffect, useRef } from 'react';
|
||||
@@ -17,7 +16,7 @@ const challengeFetcher = async (url: string) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await fetchWithTraceReport(url);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch challenge');
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
enum SubscriptionKey {
|
||||
Recurring = 'subscription_recurring',
|
||||
Plan = 'subscription_plan',
|
||||
Coupon = 'coupon',
|
||||
SignUp = 'sign_up', // A new user with subscription journey: signup > set password > pay in stripe > go to app
|
||||
Token = 'token', // When signup, there should have a token to set password
|
||||
}
|
||||
|
||||
export function useSubscriptionSearch() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return useMemo(() => {
|
||||
const withPayment =
|
||||
searchParams.has(SubscriptionKey.Recurring) &&
|
||||
searchParams.has(SubscriptionKey.Plan);
|
||||
|
||||
if (!withPayment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recurring = searchParams.get(SubscriptionKey.Recurring);
|
||||
const plan = searchParams.get(SubscriptionKey.Plan);
|
||||
const coupon = searchParams.get(SubscriptionKey.Coupon);
|
||||
const withSignUp = searchParams.get(SubscriptionKey.SignUp) === '1';
|
||||
const passwordToken = searchParams.get(SubscriptionKey.Token);
|
||||
return {
|
||||
recurring,
|
||||
plan,
|
||||
coupon,
|
||||
withSignUp,
|
||||
passwordToken,
|
||||
getRedirectUrl(signUp?: boolean) {
|
||||
const paymentParams = new URLSearchParams([
|
||||
[SubscriptionKey.Recurring, recurring ?? ''],
|
||||
[SubscriptionKey.Plan, plan ?? ''],
|
||||
]);
|
||||
|
||||
if (coupon) {
|
||||
paymentParams.set(SubscriptionKey.Coupon, coupon);
|
||||
}
|
||||
|
||||
if (signUp) {
|
||||
paymentParams.set(SubscriptionKey.SignUp, '1');
|
||||
}
|
||||
|
||||
return `/auth/subscription-redirect?${paymentParams.toString()}`;
|
||||
},
|
||||
};
|
||||
}, [searchParams]);
|
||||
}
|
||||
@@ -1,17 +1,36 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { withErrorBoundary } from 'react-error-boundary';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import { useUserSubscription } from '../../../hooks/use-subscription';
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
} from '../../../modules/cloud';
|
||||
import * as styles from './style.css';
|
||||
|
||||
const UserPlanButtonWithData = () => {
|
||||
const [subscription] = useUserSubscription();
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
export const UserPlanButton = () => {
|
||||
const { serverConfigService, subscriptionService } = useServices({
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
});
|
||||
|
||||
const hasPayment = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(r => r?.payment)
|
||||
);
|
||||
const plan = useLiveData(
|
||||
subscriptionService.subscription.primary$.map(subscription =>
|
||||
subscription !== null ? subscription?.plan : null
|
||||
)
|
||||
);
|
||||
const isLoading = plan === null;
|
||||
|
||||
useEffect(() => {
|
||||
// revalidate subscription to get the latest status
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const handleClick = useCallback(
|
||||
@@ -27,9 +46,19 @@ const UserPlanButtonWithData = () => {
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (plan === SubscriptionPlan.SelfHosted) {
|
||||
// Self hosted version doesn't have a payment apis.
|
||||
return <div className={styles.userPlanButton}>{plan}</div>;
|
||||
if (!hasPayment) {
|
||||
// no payment feature
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// loading, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
// no plan, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -40,8 +69,3 @@ const UserPlanButtonWithData = () => {
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
// If fetch user data failed, just render empty.
|
||||
export const UserPlanButton = withErrorBoundary(UserPlanButtonWithData, {
|
||||
fallbackRender: () => null,
|
||||
});
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { useSession } from '../../../hooks/affine/use-current-user';
|
||||
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
|
||||
const SyncAwarenessInnerLoggedIn = () => {
|
||||
const { user } = useSession();
|
||||
const currentWorkspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace$
|
||||
);
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
useEffect(() => {
|
||||
if (user && currentWorkspace) {
|
||||
if (account && currentWorkspace) {
|
||||
currentWorkspace.docCollection.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
{
|
||||
name: user.name,
|
||||
name: account.label,
|
||||
// todo: add avatar?
|
||||
}
|
||||
);
|
||||
@@ -29,13 +26,14 @@ const SyncAwarenessInnerLoggedIn = () => {
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [user, currentWorkspace]);
|
||||
}, [currentWorkspace, account]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const SyncAwarenessInner = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const session = useService(AuthService).session;
|
||||
const loginStatus = useLiveData(session.status$);
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <SyncAwarenessInnerLoggedIn />;
|
||||
|
||||
@@ -2,23 +2,24 @@ import { Avatar, Input, Switch, toast } from '@affine/component';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
||||
import { authAtom, openDisableCloudAlertModalAtom } from '@affine/core/atoms';
|
||||
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { _addLocalWorkspace } from '@affine/workspace-impl';
|
||||
import {
|
||||
initEmptyPage,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceManager,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { buildShowcaseWorkspace } from '../../../bootstrap/first-app-data';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { _addLocalWorkspace } from '../../../modules/workspace-engine';
|
||||
import { mixpanel } from '../../../utils';
|
||||
import { CloudSvg } from '../share-page-modal/cloud-svg';
|
||||
import * as styles from './index.css';
|
||||
@@ -47,8 +48,7 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const shouldEnableCloud =
|
||||
!runtimeConfig.allowLocalWorkspace && !environment.isDesktop;
|
||||
const shouldEnableCloud = !runtimeConfig.allowLocalWorkspace;
|
||||
|
||||
const NameWorkspaceContent = ({
|
||||
loading,
|
||||
@@ -58,7 +58,9 @@ const NameWorkspaceContent = ({
|
||||
const t = useAFFiNEI18N();
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const [enable, setEnable] = useState(shouldEnableCloud);
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const session = useService(AuthService).session;
|
||||
const loginStatus = useLiveData(session.status$);
|
||||
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
|
||||
const setOpenSignIn = useSetAtom(authAtom);
|
||||
@@ -181,7 +183,7 @@ export const CreateWorkspaceModal = ({
|
||||
}: ModalProps) => {
|
||||
const [step, setStep] = useState<CreateWorkspaceStep>();
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// todo: maybe refactor using xstate?
|
||||
@@ -202,9 +204,7 @@ export const CreateWorkspaceModal = ({
|
||||
const result = await apis.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
_addLocalWorkspace(result.workspaceId);
|
||||
workspaceManager.list.revalidate().catch(err => {
|
||||
logger.error("can't revalidate workspace list", err);
|
||||
});
|
||||
workspacesService.list.revalidate();
|
||||
onCreate(result.workspaceId);
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
@@ -223,7 +223,7 @@ export const CreateWorkspaceModal = ({
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [mode, onClose, onCreate, t, workspaceManager]);
|
||||
}, [mode, onClose, onCreate, t, workspacesService]);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
||||
@@ -237,13 +237,13 @@ export const CreateWorkspaceModal = ({
|
||||
// fix me later
|
||||
if (runtimeConfig.enablePreloading) {
|
||||
const { id } = await buildShowcaseWorkspace(
|
||||
workspaceManager,
|
||||
workspacesService,
|
||||
workspaceFlavour,
|
||||
name
|
||||
);
|
||||
onCreate(id);
|
||||
} else {
|
||||
const { id } = await workspaceManager.createWorkspace(
|
||||
const { id } = await workspacesService.create(
|
||||
workspaceFlavour,
|
||||
async workspace => {
|
||||
workspace.meta.setName(name);
|
||||
@@ -259,7 +259,7 @@ export const CreateWorkspaceModal = ({
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[loading, onCreate, workspaceManager]
|
||||
[loading, onCreate, workspacesService]
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@affine/core/atoms';
|
||||
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -13,7 +13,7 @@ import TopSvg from './top-svg';
|
||||
|
||||
export const HistoryTipsModal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const [open, setOpen] = useAtom(openHistoryTipsModalAtom);
|
||||
const setTempDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
@@ -3,12 +3,7 @@ import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspa
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { ListHistoryQuery } from '@affine/graphql';
|
||||
import {
|
||||
fetchWithTraceReport,
|
||||
listHistoryQuery,
|
||||
recoverDocMutation,
|
||||
} from '@affine/graphql';
|
||||
import { AffineCloudBlobStorage } from '@affine/workspace-impl';
|
||||
import { listHistoryQuery, recoverDocMutation } from '@affine/graphql';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { globalBlockSuiteSchema } from '@toeverything/infra';
|
||||
@@ -22,6 +17,7 @@ import {
|
||||
useMutation,
|
||||
} from '../../../hooks/use-mutation';
|
||||
import { useQueryInfinite } from '../../../hooks/use-query';
|
||||
import { CloudBlobStorage } from '../../../modules/workspace-engine/impls/engine/blob-cloud';
|
||||
|
||||
const logger = new DebugLogger('page-history');
|
||||
|
||||
@@ -76,11 +72,8 @@ const snapshotFetcher = async (
|
||||
if (!ts) {
|
||||
return null;
|
||||
}
|
||||
const res = await fetchWithTraceReport(
|
||||
`/api/workspaces/${workspaceId}/docs/${pageDocId}/histories/${ts}`,
|
||||
{
|
||||
priority: 'high',
|
||||
}
|
||||
const res = await fetch(
|
||||
`/api/workspaces/${workspaceId}/docs/${pageDocId}/histories/${ts}`
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -104,7 +97,7 @@ const docCollectionMap = new Map<string, DocCollection>();
|
||||
const getOrCreateShellWorkspace = (workspaceId: string) => {
|
||||
let docCollection = docCollectionMap.get(workspaceId);
|
||||
if (!docCollection) {
|
||||
const blobStorage = new AffineCloudBlobStorage(workspaceId);
|
||||
const blobStorage = new CloudBlobStorage(workspaceId);
|
||||
docCollection = new DocCollection({
|
||||
id: workspaceId,
|
||||
blobStorages: [
|
||||
|
||||
@@ -3,23 +3,29 @@ import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Modal, useConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
||||
import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
||||
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
|
||||
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import { Doc, useService, Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -90,8 +96,8 @@ interface HistoryEditorPreviewProps {
|
||||
ts?: string;
|
||||
historyList: HistoryList;
|
||||
snapshotPage?: BlockSuiteDoc;
|
||||
mode: PageMode;
|
||||
onModeChange: (mode: PageMode) => void;
|
||||
mode: DocMode;
|
||||
onModeChange: (mode: DocMode) => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -190,12 +196,22 @@ const HistoryEditorPreview = ({
|
||||
const planPromptClosedAtom = atom(false);
|
||||
|
||||
const PlanPrompt = () => {
|
||||
const workspace = useService(Workspace);
|
||||
const workspaceQuota = useWorkspaceQuota(workspace.id);
|
||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||
useEffect(() => {
|
||||
workspaceQuotaService.quota.revalidate();
|
||||
}, [workspaceQuotaService]);
|
||||
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
||||
const isProWorkspace = useMemo(() => {
|
||||
return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free';
|
||||
return workspaceQuota
|
||||
? workspaceQuota.humanReadable.name.toLowerCase() !== 'free'
|
||||
: null;
|
||||
}, [workspaceQuota]);
|
||||
const isOwner = useIsWorkspaceOwner(workspace.meta);
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
// revalidate permission
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
|
||||
@@ -216,11 +232,17 @@ const PlanPrompt = () => {
|
||||
const planTitle = useMemo(() => {
|
||||
return (
|
||||
<div className={styles.planPromptTitle}>
|
||||
{!isProWorkspace
|
||||
? t[
|
||||
'com.affine.history.confirm-restore-modal.plan-prompt.limited-title'
|
||||
]()
|
||||
: t['com.affine.history.confirm-restore-modal.plan-prompt.title']()}
|
||||
{
|
||||
isProWorkspace === null
|
||||
? !isProWorkspace
|
||||
? t[
|
||||
'com.affine.history.confirm-restore-modal.plan-prompt.limited-title'
|
||||
]()
|
||||
: t[
|
||||
'com.affine.history.confirm-restore-modal.plan-prompt.title'
|
||||
]()
|
||||
: '' /* TODO: loading UI */
|
||||
}
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -434,8 +456,8 @@ const PageHistoryManager = ({
|
||||
[activeVersion, onClose, onRestore, snapshotPage]
|
||||
);
|
||||
|
||||
const page = useService(Doc);
|
||||
const [mode, setMode] = useState<PageMode>(page.mode$.value);
|
||||
const doc = useService(DocService).doc;
|
||||
const [mode, setMode] = useState<DocMode>(doc.mode$.value);
|
||||
|
||||
const title = useDocCollectionPageTitle(docCollection, pageId);
|
||||
|
||||
@@ -531,7 +553,7 @@ export const PageHistoryModal = ({
|
||||
|
||||
export const GlobalPageHistoryModal = () => {
|
||||
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
mixpanel.track('Button', {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { ConfirmModal } from '@affine/component';
|
||||
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema';
|
||||
import { WorkspacePropertiesAdapter } from '@affine/core/modules/properties';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/properties/services/schema';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService } from '@toeverything/infra';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PagePropertyType } from '@affine/core/modules/workspace/properties/schema';
|
||||
import { PagePropertyType } from '@affine/core/modules/properties/services/schema';
|
||||
import * as icons from '@blocksuite/icons';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
MenuSeparator,
|
||||
Scrollable,
|
||||
} from '@affine/component';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/properties/services/schema';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { KeyboardEventHandler, MouseEventHandler } from 'react';
|
||||
import { cloneElement, isValidElement, useCallback } from 'react';
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
|
||||
import type { WorkspacePropertiesAdapter } from '@affine/core/modules/properties';
|
||||
import type {
|
||||
PageInfoCustomProperty,
|
||||
PageInfoCustomPropertyMeta,
|
||||
} from '@affine/core/modules/workspace/properties/schema';
|
||||
import { PagePropertyType } from '@affine/core/modules/workspace/properties/schema';
|
||||
} from '@affine/core/modules/properties/services/schema';
|
||||
import { PagePropertyType } from '@affine/core/modules/properties/services/schema';
|
||||
import { createFractionalIndexingSortableHelper } from '@affine/core/utils';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
+5
-12
@@ -1,14 +1,12 @@
|
||||
import { Checkbox, DatePicker, Menu } from '@affine/component';
|
||||
import { useAllBlockSuiteDocMeta } from '@affine/core/hooks/use-all-block-suite-page-meta';
|
||||
import type {
|
||||
PageInfoCustomProperty,
|
||||
PageInfoCustomPropertyMeta,
|
||||
PagePropertyType,
|
||||
} from '@affine/core/modules/workspace/properties/schema';
|
||||
} from '@affine/core/modules/properties/services/schema';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { Doc, useService, Workspace } from '@toeverything/infra';
|
||||
import { DocService, useService } from '@toeverything/infra';
|
||||
import { noop } from 'lodash-es';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
@@ -169,21 +167,16 @@ export const NumberValue = ({ property }: PropertyRowValueProps) => {
|
||||
};
|
||||
|
||||
export const TagsValue = () => {
|
||||
const workspace = useService(Workspace);
|
||||
const page = useService(Doc);
|
||||
const docCollection = workspace.docCollection;
|
||||
const pageMetas = useAllBlockSuiteDocMeta(docCollection);
|
||||
const doc = useService(DocService).doc;
|
||||
|
||||
const pageMeta = pageMetas.find(x => x.id === page.id);
|
||||
assertExists(pageMeta, 'pageMeta should exist');
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<TagsInlineEditor
|
||||
className={styles.propertyRowValueCell}
|
||||
placeholder={t['com.affine.page-properties.property-value-placeholder']()}
|
||||
pageId={page.id}
|
||||
readonly={page.blockSuiteDoc.readonly}
|
||||
pageId={doc.id}
|
||||
readonly={doc.blockSuiteDoc.readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
PageInfoCustomProperty,
|
||||
PageInfoCustomPropertyMeta,
|
||||
PagePropertyType,
|
||||
} from '@affine/core/modules/workspace/properties/schema';
|
||||
} from '@affine/core/modules/properties/services/schema';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
+15
-15
@@ -1,8 +1,8 @@
|
||||
import type { MenuProps } from '@affine/component';
|
||||
import { IconButton, Input, Menu, Scrollable } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/properties';
|
||||
import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
|
||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteIcon, MoreHorizontalIcon, TagsIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -30,9 +30,9 @@ const InlineTagsList = ({
|
||||
readonly,
|
||||
children,
|
||||
}: PropsWithChildren<InlineTagsListProps>) => {
|
||||
const tagService = useService(TagService);
|
||||
const tags = useLiveData(tagService.tags$);
|
||||
const tagIds = useLiveData(tagService.tagIdsByPageId$(pageId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
const tagIds = useLiveData(tagList.tagIdsByPageId$(pageId));
|
||||
|
||||
return (
|
||||
<div className={styles.inlineTagsContainer} data-testid="inline-tags-list">
|
||||
@@ -71,8 +71,8 @@ export const EditTagMenu = ({
|
||||
}>) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const legacyProperties = useService(WorkspaceLegacyProperties);
|
||||
const tagService = useService(TagService);
|
||||
const tag = useLiveData(tagService.tagByTagId$(tagId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tag = useLiveData(tagList.tagByTagId$(tagId));
|
||||
const tagColor = useLiveData(tag?.color$);
|
||||
const tagValue = useLiveData(tag?.value$);
|
||||
const navigate = useNavigateHelper();
|
||||
@@ -169,9 +169,9 @@ export const EditTagMenu = ({
|
||||
|
||||
export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const tagService = useService(TagService);
|
||||
const tags = useLiveData(tagService.tags$);
|
||||
const tagIds = useLiveData(tagService.tagIdsByPageId$(pageId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
const tagIds = useLiveData(tagList.tagIdsByPageId$(pageId));
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
@@ -192,10 +192,10 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
[setOpen, setSelectedTagIds]
|
||||
);
|
||||
|
||||
const exactMatch = useLiveData(tagService.tagByTagValue$(inputValue));
|
||||
const exactMatch = useLiveData(tagList.tagByTagValue$(inputValue));
|
||||
|
||||
const filteredTags = useLiveData(
|
||||
inputValue ? tagService.filterTagsByName$(inputValue) : tagService.tags$
|
||||
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
|
||||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
@@ -228,10 +228,10 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
return;
|
||||
}
|
||||
rotateNextColor();
|
||||
const newTag = tagService.createTag(name.trim(), nextColor);
|
||||
const newTag = tagList.createTag(name.trim(), nextColor);
|
||||
newTag.tag(pageId);
|
||||
},
|
||||
[nextColor, pageId, tagService]
|
||||
[nextColor, pageId, tagList]
|
||||
);
|
||||
|
||||
const onInputKeyDown = useCallback(
|
||||
@@ -335,8 +335,8 @@ export const TagsInlineEditor = ({
|
||||
placeholder,
|
||||
className,
|
||||
}: TagsInlineEditorProps) => {
|
||||
const tagService = useService(TagService);
|
||||
const tagIds = useLiveData(tagService.tagIdsByPageId$(pageId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tagIds = useLiveData(tagList.tagIdsByPageId$(pageId));
|
||||
const empty = !tagIds || tagIds.length === 0;
|
||||
return (
|
||||
<Menu
|
||||
|
||||
+57
-36
@@ -1,10 +1,10 @@
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
||||
import { useUserQuota } from '@affine/core/hooks/use-quota';
|
||||
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import bytes from 'bytes';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
@@ -13,53 +13,74 @@ import { mixpanel } from '../../../utils';
|
||||
|
||||
export const CloudQuotaModal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
||||
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
|
||||
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
||||
const userQuota = useUserQuota();
|
||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||
useEffect(() => {
|
||||
workspaceQuotaService.quota.revalidate();
|
||||
}, [workspaceQuotaService]);
|
||||
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
// revalidate permission
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const quotaService = useService(UserQuotaService);
|
||||
const userQuota = useLiveData(
|
||||
quotaService.quota.quota$.map(q =>
|
||||
q
|
||||
? {
|
||||
name: q.humanReadable.name,
|
||||
blobLimit: q.humanReadable.blobLimit,
|
||||
}
|
||||
: null
|
||||
)
|
||||
);
|
||||
|
||||
const isFreePlanOwner = useMemo(() => {
|
||||
return isOwner && userQuota?.humanReadable.name.toLowerCase() === 'free';
|
||||
}, [isOwner, userQuota?.humanReadable.name]);
|
||||
return isOwner && userQuota?.name === 'free';
|
||||
}, [isOwner, userQuota]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const handleUpgradeConfirm = useCallback(() => {
|
||||
if (isFreePlanOwner) {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
}
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
}, [isFreePlanOwner, setOpen, setSettingModalAtom]);
|
||||
}, [setOpen, setSettingModalAtom]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (userQuota && isFreePlanOwner) {
|
||||
return t['com.affine.payment.blob-limit.description.owner.free']({
|
||||
planName: userQuota.humanReadable.name,
|
||||
currentQuota: userQuota.humanReadable.blobLimit,
|
||||
planName: userQuota.name,
|
||||
currentQuota: userQuota.blobLimit,
|
||||
upgradeQuota: '100MB',
|
||||
});
|
||||
}
|
||||
if (isOwner && userQuota?.humanReadable.name.toLowerCase() === 'pro') {
|
||||
if (isOwner && userQuota && userQuota.name.toLowerCase() === 'pro') {
|
||||
return t['com.affine.payment.blob-limit.description.owner.pro']({
|
||||
planName: userQuota.humanReadable.name,
|
||||
quota: userQuota.humanReadable.blobLimit,
|
||||
planName: userQuota.name,
|
||||
quota: userQuota.blobLimit,
|
||||
});
|
||||
}
|
||||
return t['com.affine.payment.blob-limit.description.member']({
|
||||
quota: workspaceQuota.humanReadable.blobLimit,
|
||||
});
|
||||
}, [
|
||||
isFreePlanOwner,
|
||||
isOwner,
|
||||
t,
|
||||
userQuota,
|
||||
workspaceQuota.humanReadable.blobLimit,
|
||||
]);
|
||||
if (workspaceQuota) {
|
||||
return t['com.affine.payment.blob-limit.description.member']({
|
||||
quota: workspaceQuota.humanReadable.blobLimit,
|
||||
});
|
||||
} else {
|
||||
// loading
|
||||
return null;
|
||||
}
|
||||
}, [userQuota, isFreePlanOwner, isOwner, workspaceQuota, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceQuota) {
|
||||
return;
|
||||
}
|
||||
currentWorkspace.engine.blob.singleBlobSizeLimit = bytes.parse(
|
||||
workspaceQuota.blobLimit.toString()
|
||||
);
|
||||
@@ -70,15 +91,15 @@ export const CloudQuotaModal = () => {
|
||||
return () => {
|
||||
disposable?.dispose();
|
||||
};
|
||||
}, [currentWorkspace.engine.blob, setOpen, workspaceQuota.blobLimit]);
|
||||
}, [currentWorkspace.engine.blob, setOpen, workspaceQuota]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userQuota?.humanReadable) {
|
||||
if (userQuota?.name) {
|
||||
mixpanel.people.set({
|
||||
plan: userQuota.humanReadable.name,
|
||||
plan: userQuota.name,
|
||||
});
|
||||
}
|
||||
}, [userQuota]);
|
||||
}, [userQuota?.name]);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openQuotaModalAtom } from '@affine/core/atoms';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export const LocalQuotaModal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
|
||||
+87
-84
@@ -1,28 +1,37 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { Button, ErrorMessage, Skeleton } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useQuery } from '@affine/core/hooks/use-query';
|
||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||
import {
|
||||
getCopilotQuotaQuery,
|
||||
pricesQuery,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
UserQuotaService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useAffineAISubscription } from '../general-setting/plans/ai/use-affine-ai-subscription';
|
||||
import { AIResume, AISubscribe } from '../general-setting/plans/ai/actions';
|
||||
import * as styles from './storage-progress.css';
|
||||
|
||||
export const AIUsagePanel = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpenSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const [, mutateSubscription] = useUserSubscription();
|
||||
const { actionType, Action } = useAffineAISubscription();
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const quotaService = useService(UserQuotaService);
|
||||
useEffect(() => {
|
||||
quotaService.quota.revalidate();
|
||||
}, [quotaService]);
|
||||
const aiActionLimit = useLiveData(quotaService.quota.aiActionLimit$);
|
||||
const aiActionUsed = useLiveData(quotaService.quota.aiActionUsed$);
|
||||
const loading = aiActionLimit === null || aiActionUsed === null;
|
||||
const loadError = useLiveData(quotaService.quota.error$);
|
||||
|
||||
const openAiPricingPlan = useCallback(() => {
|
||||
setOpenSettingModal({
|
||||
@@ -32,95 +41,89 @@ export const AIUsagePanel = () => {
|
||||
});
|
||||
}, [setOpenSettingModal]);
|
||||
|
||||
if (actionType === 'cancel') {
|
||||
if (loading) {
|
||||
if (loadError) {
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
desc={''}
|
||||
spreadCol={false}
|
||||
>
|
||||
{/* TODO: i18n */}
|
||||
<ErrorMessage>Load error</ErrorMessage>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SettingRow
|
||||
desc={t['com.affine.payment.ai.usage-description-purchased']()}
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
desc={''}
|
||||
spreadCol={false}
|
||||
>
|
||||
<Button onClick={openAiPricingPlan}>
|
||||
{t['com.affine.payment.ai.usage.change-button-label']()}
|
||||
</Button>
|
||||
<Skeleton height={42} />
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
if (actionType === 'resume') {
|
||||
return (
|
||||
<SettingRow
|
||||
desc={t['com.affine.payment.ai.usage-description-purchased']()}
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
>
|
||||
<Action onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
return <AIUsagePanelNotSubscripted />;
|
||||
};
|
||||
|
||||
export const AIUsagePanelNotSubscripted = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, mutateSubscription] = useUserSubscription();
|
||||
const { actionType, Action } = useAffineAISubscription();
|
||||
|
||||
const {
|
||||
data: { prices },
|
||||
} = useQuery({ query: pricesQuery });
|
||||
const { data: quota } = useQuery({
|
||||
query: getCopilotQuotaQuery,
|
||||
});
|
||||
const { limit: nullableLimit, used = 0 } =
|
||||
quota.currentUser?.copilot.quota || {};
|
||||
const limit = nullableLimit || 10;
|
||||
const percent = Math.min(
|
||||
100,
|
||||
Math.max(0.5, Number(((used / limit) * 100).toFixed(4)))
|
||||
);
|
||||
|
||||
const price = prices.find(p => p.plan === SubscriptionPlan.AI);
|
||||
assertExists(price);
|
||||
const percent =
|
||||
aiActionLimit === 'unlimited'
|
||||
? 0
|
||||
: Math.min(
|
||||
100,
|
||||
Math.max(
|
||||
0.5,
|
||||
Number(((aiActionUsed / aiActionLimit) * 100).toFixed(4))
|
||||
)
|
||||
);
|
||||
|
||||
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
desc=""
|
||||
spreadCol={aiSubscription ? true : false}
|
||||
desc={
|
||||
aiSubscription
|
||||
? t['com.affine.payment.ai.usage-description-purchased']()
|
||||
: ''
|
||||
}
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
>
|
||||
<div className={styles.storageProgressContainer}>
|
||||
<div className={styles.storageProgressWrapper}>
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.payment.ai.usage.used-caption']()}</span>
|
||||
<span>
|
||||
{t['com.affine.payment.ai.usage.used-detail']({
|
||||
used: used.toString(),
|
||||
limit: limit.toString(),
|
||||
})}
|
||||
</span>
|
||||
{aiActionLimit === 'unlimited' ? (
|
||||
hasPaymentFeature && aiSubscription?.canceledAt ? (
|
||||
<AIResume />
|
||||
) : (
|
||||
<Button onClick={openAiPricingPlan}>
|
||||
{t['com.affine.payment.ai.usage.change-button-label']()}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<div className={styles.storageProgressContainer}>
|
||||
<div className={styles.storageProgressWrapper}>
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.payment.ai.usage.used-caption']()}</span>
|
||||
<span>
|
||||
{t['com.affine.payment.ai.usage.used-detail']({
|
||||
used: aiActionUsed.toString(),
|
||||
limit: aiActionLimit.toString(),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={styles.storageProgressBar}
|
||||
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={styles.storageProgressBar}
|
||||
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||
></div>
|
||||
</div>
|
||||
{hasPaymentFeature && (
|
||||
<AISubscribe type="primary" className={styles.storageButton}>
|
||||
{t['com.affine.payment.ai.usage.purchase-button-label']()}
|
||||
</AISubscribe>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Action
|
||||
recurring={SubscriptionRecurring.Yearly}
|
||||
onSubscriptionUpdate={mutateSubscription}
|
||||
price={price}
|
||||
type="primary"
|
||||
className={styles.storageButton}
|
||||
>
|
||||
{actionType === 'subscribe'
|
||||
? t['com.affine.payment.ai.usage.purchase-button-label']()
|
||||
: null}
|
||||
</Action>
|
||||
</div>
|
||||
)}
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
+42
-69
@@ -5,29 +5,21 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { SWRErrorBoundary } from '@affine/core/components/pure/swr-error-bundary';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import {
|
||||
removeAvatarMutation,
|
||||
updateUserProfileMutation,
|
||||
uploadAvatarMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||
import { useEnsureLiveData, useService } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
authAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../../../../atoms';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useServerFeatures } from '../../../../hooks/affine/use-server-config';
|
||||
import { useMutation } from '../../../../hooks/use-mutation';
|
||||
import { AuthService } from '../../../../modules/cloud';
|
||||
import { mixpanel } from '../../../../utils';
|
||||
import { validateAndReduceImage } from '../../../../utils/reduce-image';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import { AIUsagePanel } from './ai-usage-panel';
|
||||
import { StorageProgress } from './storage-progress';
|
||||
@@ -35,27 +27,16 @@ import * as styles from './style.css';
|
||||
|
||||
export const UserAvatar = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
const { trigger: avatarTrigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
const { trigger: removeAvatarTrigger } = useMutation({
|
||||
mutation: removeAvatarMutation,
|
||||
});
|
||||
const session = useService(AuthService).session;
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
|
||||
const handleUpdateUserAvatar = useAsyncCallback(
|
||||
async (file: File) => {
|
||||
try {
|
||||
mixpanel.track_forms('UpdateProfile', 'UploadAvatar', {
|
||||
userId: user.id,
|
||||
userId: account.id,
|
||||
});
|
||||
const reducedFile = await validateAndReduceImage(file);
|
||||
const data = await avatarTrigger({
|
||||
avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger
|
||||
});
|
||||
user.update({ avatarUrl: data.uploadAvatar.avatarUrl });
|
||||
// TODO: i18n
|
||||
await session.uploadAvatar(file);
|
||||
notify.success({ title: 'Update user avatar success' });
|
||||
} catch (e) {
|
||||
// TODO: i18n
|
||||
@@ -65,19 +46,18 @@ export const UserAvatar = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[avatarTrigger, user]
|
||||
[account, session]
|
||||
);
|
||||
|
||||
const handleRemoveUserAvatar = useAsyncCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
mixpanel.track('RemoveAvatar', {
|
||||
userId: user.id,
|
||||
userId: account.id,
|
||||
});
|
||||
e.stopPropagation();
|
||||
await removeAvatarTrigger();
|
||||
user.update({ avatarUrl: null });
|
||||
await session.removeAvatar();
|
||||
},
|
||||
[removeAvatarTrigger, user]
|
||||
[account, session]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -88,10 +68,10 @@ export const UserAvatar = () => {
|
||||
>
|
||||
<Avatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.avatarUrl}
|
||||
name={account.label}
|
||||
url={account.avatar}
|
||||
hoverIcon={<CameraIcon />}
|
||||
onRemove={user.avatarUrl ? handleRemoveUserAvatar : undefined}
|
||||
onRemove={account.avatar ? handleRemoveUserAvatar : undefined}
|
||||
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
|
||||
removeTooltipOptions={{ content: t['Remove photo']() }}
|
||||
data-testid="user-setting-avatar"
|
||||
@@ -105,33 +85,31 @@ export const UserAvatar = () => {
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
const session = useService(AuthService).session;
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const [input, setInput] = useState<string>(account.label);
|
||||
|
||||
const { trigger: updateProfile } = useMutation({
|
||||
mutation: updateUserProfileMutation,
|
||||
});
|
||||
const allowUpdate = !!input && input !== user.name;
|
||||
const allowUpdate = !!input && input !== account.label;
|
||||
const handleUpdateUserName = useAsyncCallback(async () => {
|
||||
if (account === null) {
|
||||
return;
|
||||
}
|
||||
if (!allowUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mixpanel.track_forms('UpdateProfile', 'UpdateUsername', {
|
||||
userId: user.id,
|
||||
userId: account.id,
|
||||
});
|
||||
const data = await updateProfile({
|
||||
input: { name: input },
|
||||
});
|
||||
user.update({ name: data.updateProfile.name });
|
||||
await session.updateLabel(input);
|
||||
} catch (e) {
|
||||
notify.error({
|
||||
title: 'Failed to update user name.',
|
||||
message: String(e),
|
||||
});
|
||||
}
|
||||
}, [allowUpdate, input, user, updateProfile]);
|
||||
}, [account, allowUpdate, session, input]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
@@ -140,9 +118,7 @@ export const AvatarAndName = () => {
|
||||
spreadCol={false}
|
||||
>
|
||||
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
|
||||
<Suspense>
|
||||
<UserAvatar />
|
||||
</Suspense>
|
||||
<UserAvatar />
|
||||
|
||||
<div className={styles.profileInputWrapper}>
|
||||
<label>{t['com.affine.settings.profile.name']()}</label>
|
||||
@@ -178,7 +154,6 @@ export const AvatarAndName = () => {
|
||||
|
||||
const StoragePanel = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { payment: hasPaymentFeature } = useServerFeatures();
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const onUpgrade = useCallback(() => {
|
||||
@@ -197,14 +172,18 @@ const StoragePanel = () => {
|
||||
desc=""
|
||||
spreadCol={false}
|
||||
>
|
||||
<StorageProgress onUpgrade={onUpgrade} upgradable={hasPaymentFeature} />
|
||||
<StorageProgress onUpgrade={onUpgrade} />
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const session = useService(AuthService).session;
|
||||
useEffect(() => {
|
||||
session.revalidate();
|
||||
}, [session]);
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const setSignOutModal = useSetAtom(openSignOutModalAtom);
|
||||
|
||||
@@ -212,19 +191,19 @@ export const AccountSetting: FC = () => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: user.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
email: account.email,
|
||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
});
|
||||
}, [setAuthModal, user.email, user.emailVerified]);
|
||||
}, [account.email, account.info?.emailVerified, setAuthModal]);
|
||||
|
||||
const onPasswordButtonClick = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: user.hasPassword ? 'changePassword' : 'setPassword',
|
||||
email: account.email,
|
||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
||||
});
|
||||
}, [setAuthModal, user.email, user.hasPassword]);
|
||||
}, [account.email, account.info?.hasPassword, setAuthModal]);
|
||||
|
||||
const onOpenSignOutModal = useCallback(() => {
|
||||
setSignOutModal(true);
|
||||
@@ -238,9 +217,9 @@ export const AccountSetting: FC = () => {
|
||||
data-testid="account-title"
|
||||
/>
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={user.email}>
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={account.email}>
|
||||
<Button onClick={onChangeEmail} className={styles.button}>
|
||||
{user.emailVerified
|
||||
{account.info?.emailVerified
|
||||
? t['com.affine.settings.email.action.change']()
|
||||
: t['com.affine.settings.email.action.verify']()}
|
||||
</Button>
|
||||
@@ -250,19 +229,13 @@ export const AccountSetting: FC = () => {
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onPasswordButtonClick} className={styles.button}>
|
||||
{user.hasPassword
|
||||
{account.info?.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<Suspense>
|
||||
<StoragePanel />
|
||||
</Suspense>
|
||||
<SWRErrorBoundary fallback={<div />}>
|
||||
<Suspense>
|
||||
<AIUsagePanel />
|
||||
</Suspense>
|
||||
</SWRErrorBoundary>
|
||||
<StoragePanel />
|
||||
<AIUsagePanel />
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
|
||||
+61
-17
@@ -1,9 +1,15 @@
|
||||
import { Button, Tooltip } from '@affine/component';
|
||||
import { useCloudStorageUsage } from '@affine/core/hooks/affine/use-cloud-storage-usage';
|
||||
import { Button, ErrorMessage, Skeleton, Tooltip } from '@affine/component';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
UserQuotaService,
|
||||
} from '../../../../modules/cloud';
|
||||
import * as styles from './storage-progress.css';
|
||||
|
||||
export interface StorageProgressProgress {
|
||||
@@ -16,20 +22,55 @@ enum ButtonType {
|
||||
Default = 'default',
|
||||
}
|
||||
|
||||
export const StorageProgress = ({
|
||||
upgradable = true,
|
||||
onUpgrade,
|
||||
}: StorageProgressProgress) => {
|
||||
export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { plan, usedText, color, percent, maxLimitText } =
|
||||
useCloudStorageUsage();
|
||||
const quota = useService(UserQuotaService).quota;
|
||||
|
||||
useEffect(() => {
|
||||
// revalidate quota to get the latest status
|
||||
quota.revalidate();
|
||||
}, [quota]);
|
||||
const color = useLiveData(quota.color$);
|
||||
const usedFormatted = useLiveData(quota.usedFormatted$);
|
||||
const maxFormatted = useLiveData(quota.maxFormatted$);
|
||||
const percent = useLiveData(quota.percent$);
|
||||
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
useEffect(() => {
|
||||
// revalidate subscription to get the latest status
|
||||
subscription.revalidate();
|
||||
}, [subscription]);
|
||||
|
||||
const primarySubscription = useLiveData(subscription.primary$);
|
||||
const isFreeUser =
|
||||
!primarySubscription || primarySubscription?.plan === SubscriptionPlan.Free;
|
||||
const quotaName = useLiveData(
|
||||
quota.quota$.map(q => (q !== null ? q?.humanReadable.name : null))
|
||||
);
|
||||
|
||||
const loading =
|
||||
primarySubscription === null || percent === null || quotaName === null;
|
||||
const loadError = useLiveData(quota.error$);
|
||||
|
||||
const buttonType = useMemo(() => {
|
||||
if (plan === SubscriptionPlan.Free) {
|
||||
if (isFreeUser) {
|
||||
return ButtonType.Primary;
|
||||
}
|
||||
return ButtonType.Default;
|
||||
}, [plan]);
|
||||
}, [isFreeUser]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) {
|
||||
// TODO: i18n
|
||||
return <ErrorMessage>Load error</ErrorMessage>;
|
||||
}
|
||||
// TODO: loading UI
|
||||
return <Skeleton height={42} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.storageProgressContainer}>
|
||||
@@ -37,24 +78,27 @@ export const StorageProgress = ({
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.storage.used.hint']()}</span>
|
||||
<span>
|
||||
{usedText}/{maxLimitText}
|
||||
{` (${plan} ${t['com.affine.storage.plan']()})`}
|
||||
{usedFormatted}/{maxFormatted}
|
||||
{` (${quotaName} ${t['com.affine.storage.plan']()})`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={styles.storageProgressBar}
|
||||
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color ?? cssVar('processingColor'),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{upgradable ? (
|
||||
{hasPaymentFeature ? (
|
||||
<Tooltip
|
||||
options={{ hidden: percent < 100 }}
|
||||
content={
|
||||
plan === 'Free'
|
||||
isFreeUser
|
||||
? t['com.affine.storage.maximum-tips']()
|
||||
: t['com.affine.storage.maximum-tips.pro']()
|
||||
}
|
||||
@@ -65,7 +109,7 @@ export const StorageProgress = ({
|
||||
onClick={onUpgrade}
|
||||
className={styles.storageButton}
|
||||
>
|
||||
{plan === 'Free'
|
||||
{isFreeUser
|
||||
? t['com.affine.storage.upgrade']()
|
||||
: t['com.affine.storage.change-plan']()}
|
||||
</Button>
|
||||
|
||||
+206
-180
@@ -14,29 +14,29 @@ import {
|
||||
getInvoicesCountQuery,
|
||||
invoicesQuery,
|
||||
InvoiceStatus,
|
||||
pricesQuery,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
} from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../atoms';
|
||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||
import { useQuery } from '../../../../../hooks/use-query';
|
||||
import type { SubscriptionMutator } from '../../../../../hooks/use-subscription';
|
||||
import { useUserSubscription } from '../../../../../hooks/use-subscription';
|
||||
import { mixpanel, popupWindow } from '../../../../../utils';
|
||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
||||
import {
|
||||
mixpanel,
|
||||
popupWindow,
|
||||
timestampToLocalDate,
|
||||
} from '../../../../../utils';
|
||||
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
|
||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||
import { useAffineAIPrice } from '../plans/ai/use-affine-ai-price';
|
||||
import { useAffineAISubscription } from '../plans/ai/use-affine-ai-subscription';
|
||||
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
|
||||
import * as styles from './style.css';
|
||||
|
||||
enum DescriptionI18NKey {
|
||||
@@ -58,13 +58,8 @@ const getMessageKey = (
|
||||
};
|
||||
|
||||
export const BillingSettings = () => {
|
||||
const status = useCurrentLoginStatus();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (status !== 'authenticated') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -94,181 +89,220 @@ export const BillingSettings = () => {
|
||||
};
|
||||
|
||||
const SubscriptionSettings = () => {
|
||||
const [subscription, mutateSubscription] = useUserSubscription();
|
||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||
const {
|
||||
isFree: isFreeAI,
|
||||
actionType: aiActionType,
|
||||
Action: AIAction,
|
||||
billingTip,
|
||||
} = useAffineAISubscription();
|
||||
|
||||
const { data: pricesQueryResult } = useQuery({
|
||||
query: pricesQuery,
|
||||
});
|
||||
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const recurring = subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const price = pricesQueryResult.prices.find(price => price.plan === plan);
|
||||
const aiPrice = pricesQueryResult.prices.find(
|
||||
price => price.plan === SubscriptionPlan.AI
|
||||
);
|
||||
assertExists(aiPrice);
|
||||
const amount =
|
||||
plan === SubscriptionPlan.Free
|
||||
? '0'
|
||||
: price
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String((price.amount ?? 0) / 100)
|
||||
: String((price.yearlyAmount ?? 0) / 100)
|
||||
: '?';
|
||||
|
||||
const { priceReadable: aiPriceReadable, priceFrequency: aiPriceFrequency } =
|
||||
useAffineAIPrice(aiPrice);
|
||||
const t = useAFFiNEI18N();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const primarySubscription = useLiveData(
|
||||
subscriptionService.subscription.primary$
|
||||
);
|
||||
const proPrice = useLiveData(subscriptionService.prices.proPrice$);
|
||||
|
||||
const currentPlan =
|
||||
primarySubscription === null
|
||||
? null
|
||||
: primarySubscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||
|
||||
const recurring =
|
||||
primarySubscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const gotoPlansSetting = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: 'ChangePlan',
|
||||
currentPlan: plan,
|
||||
currentPlan: currentPlan,
|
||||
});
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
}, [setOpenSettingModalAtom, plan]);
|
||||
}, [currentPlan, setOpenSettingModalAtom]);
|
||||
|
||||
const currentPlanDesc = useMemo(() => {
|
||||
const messageKey = getMessageKey(plan, recurring);
|
||||
return (
|
||||
<Trans
|
||||
i18nKey={messageKey}
|
||||
values={{
|
||||
planName: plan,
|
||||
}}
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
onClick={gotoPlansSetting}
|
||||
className={styles.currentPlanName}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [plan, recurring, gotoPlansSetting]);
|
||||
const amount = currentPlan
|
||||
? currentPlan === SubscriptionPlan.Free
|
||||
? '0'
|
||||
: proPrice
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String((proPrice.amount ?? 0) / 100)
|
||||
: String((proPrice.yearlyAmount ?? 0) / 100)
|
||||
: '?'
|
||||
: '?';
|
||||
|
||||
return (
|
||||
<div className={styles.subscription}>
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.current-plan']()}
|
||||
desc={currentPlanDesc}
|
||||
/>
|
||||
<PlanAction plan={plan} gotoPlansSetting={gotoPlansSetting} />
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
${amount}
|
||||
<span className={styles.billingFrequency}>
|
||||
/
|
||||
{recurring === SubscriptionRecurring.Monthly
|
||||
? t['com.affine.payment.billing-setting.month']()
|
||||
: t['com.affine.payment.billing-setting.year']()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.planCard} style={{ marginTop: 24 }}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.ai-plan']()}
|
||||
desc={
|
||||
isFreeAI
|
||||
? t['com.affine.payment.billing-setting.ai.free-desc']()
|
||||
: billingTip
|
||||
}
|
||||
/>
|
||||
{aiPrice?.yearlyAmount ? (
|
||||
<AIAction
|
||||
className={styles.planAction}
|
||||
price={aiPrice}
|
||||
recurring={SubscriptionRecurring.Yearly}
|
||||
onSubscriptionUpdate={mutateSubscription}
|
||||
>
|
||||
{aiActionType === 'subscribe'
|
||||
? t['com.affine.payment.billing-setting.ai.purchase']()
|
||||
: null}
|
||||
</AIAction>
|
||||
) : null}
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
{isFreeAI ? '$0' : aiPriceReadable}
|
||||
<span className={styles.billingFrequency}>/{aiPriceFrequency}</span>
|
||||
</p>
|
||||
</div>
|
||||
{subscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-setting.payment-method']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.payment-method.description'
|
||||
]()}
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{subscription.nextBillAt && (
|
||||
{currentPlan !== null ? (
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.renew-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.renew-date.description'
|
||||
]({
|
||||
renewDate: new Date(
|
||||
subscription.nextBillAt
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.current-plan']()}
|
||||
desc={
|
||||
<Trans
|
||||
i18nKey={getMessageKey(currentPlan, recurring)}
|
||||
values={{
|
||||
planName: currentPlan,
|
||||
}}
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
onClick={gotoPlansSetting}
|
||||
className={styles.currentPlanName}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{subscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.expiration-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.expiration-date.description'
|
||||
]({
|
||||
expirationDate: new Date(subscription.end).toLocaleDateString(),
|
||||
})}
|
||||
>
|
||||
<ResumeSubscription onSubscriptionUpdate={mutateSubscription} />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<CancelAction
|
||||
open={openCancelModal}
|
||||
onOpenChange={setOpenCancelModal}
|
||||
onSubscriptionUpdate={mutateSubscription}
|
||||
>
|
||||
<SettingRow
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setOpenCancelModal(true)}
|
||||
className="dangerous-setting"
|
||||
name={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription.description'
|
||||
]()}
|
||||
>
|
||||
<CancelSubscription />
|
||||
</SettingRow>
|
||||
</CancelAction>
|
||||
)}
|
||||
</>
|
||||
<PlanAction
|
||||
plan={currentPlan}
|
||||
gotoPlansSetting={gotoPlansSetting}
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
${amount}
|
||||
<span className={styles.billingFrequency}>
|
||||
/
|
||||
{recurring === SubscriptionRecurring.Monthly
|
||||
? t['com.affine.payment.billing-setting.month']()
|
||||
: t['com.affine.payment.billing-setting.year']()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<SubscriptionSettingSkeleton />
|
||||
)}
|
||||
|
||||
<AIPlanCard />
|
||||
{primarySubscription !== null ? (
|
||||
primarySubscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-setting.payment-method']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.payment-method.description'
|
||||
]()}
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{primarySubscription.nextBillAt && (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.renew-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.renew-date.description'
|
||||
]({
|
||||
renewDate: new Date(
|
||||
primarySubscription.nextBillAt
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{primarySubscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.expiration-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.expiration-date.description'
|
||||
]({
|
||||
expirationDate: new Date(
|
||||
primarySubscription.end
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
>
|
||||
<ResumeSubscription />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<CancelAction
|
||||
open={openCancelModal}
|
||||
onOpenChange={setOpenCancelModal}
|
||||
>
|
||||
<SettingRow
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setOpenCancelModal(true)}
|
||||
className="dangerous-setting"
|
||||
name={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription.description'
|
||||
]()}
|
||||
>
|
||||
<CancelSubscription />
|
||||
</SettingRow>
|
||||
</CancelAction>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<SubscriptionSettingSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AIPlanCard = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
|
||||
const priceReadable = price?.yearlyAmount
|
||||
? `$${(price.yearlyAmount / 100).toFixed(2)}`
|
||||
: '?';
|
||||
const priceFrequency = t['com.affine.payment.billing-setting.year']();
|
||||
|
||||
if (subscription === null) {
|
||||
return <Skeleton height={100} />;
|
||||
}
|
||||
|
||||
const billingTip =
|
||||
subscription === undefined
|
||||
? t['com.affine.payment.billing-setting.ai.free-desc']()
|
||||
: subscription?.nextBillAt
|
||||
? t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: timestampToLocalDate(subscription.nextBillAt),
|
||||
})
|
||||
: subscription?.canceledAt && subscription.end
|
||||
? t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: timestampToLocalDate(subscription.end),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={styles.planCard} style={{ marginTop: 24 }}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.ai-plan']()}
|
||||
desc={billingTip}
|
||||
/>
|
||||
{price?.yearlyAmount ? (
|
||||
subscription ? (
|
||||
subscription.canceledAt ? (
|
||||
<AIResume />
|
||||
) : (
|
||||
<AICancel />
|
||||
)
|
||||
) : (
|
||||
<AISubscribe>
|
||||
{t['com.affine.payment.billing-setting.ai.purchase']()}
|
||||
</AISubscribe>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
{subscription ? priceReadable : '$0'}
|
||||
<span className={styles.billingFrequency}>/{priceFrequency}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -322,20 +356,12 @@ const PaymentMethodUpdater = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeSubscription = ({
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const ResumeSubscription = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ResumeAction
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||
>
|
||||
<ResumeAction open={open} onOpenChange={setOpen}>
|
||||
<Button className={styles.button} onClick={() => setOpen(true)}>
|
||||
{t['com.affine.payment.billing-setting.resume-subscription']()}
|
||||
</Button>
|
||||
|
||||
+7
-4
@@ -4,10 +4,10 @@ import {
|
||||
InformationIcon,
|
||||
KeyboardIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { useServerFeatures } from '../../../../hooks/affine/use-server-config';
|
||||
import { AuthService, ServerConfigService } from '../../../../modules/cloud';
|
||||
import type { GeneralSettingKey } from '../types';
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
@@ -27,8 +27,11 @@ export type GeneralSettingList = GeneralSettingListItem[];
|
||||
|
||||
export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const t = useAFFiNEI18N();
|
||||
const status = useCurrentLoginStatus();
|
||||
const { payment: hasPaymentFeature } = useServerFeatures();
|
||||
const status = useLiveData(useService(AuthService).session.status$);
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
|
||||
const settings: GeneralSettingListItem[] = [
|
||||
{
|
||||
|
||||
+30
-40
@@ -1,14 +1,10 @@
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
|
||||
import {
|
||||
cancelSubscriptionMutation,
|
||||
resumeSubscriptionMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||
|
||||
/**
|
||||
@@ -20,30 +16,27 @@ export const CancelAction = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
} & PropsWithChildren) => {
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
});
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const downgrade = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onSubscriptionUpdate(data.cancelSubscription);
|
||||
onOpenChange(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [trigger, idempotencyKey, onSubscriptionUpdate, onOpenChange]);
|
||||
try {
|
||||
setIsMutating(true);
|
||||
await subscription.cancelSubscription(idempotencyKey);
|
||||
subscription.revalidate();
|
||||
await subscription.isRevalidating$.waitFor(v => !v);
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, onOpenChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -67,31 +60,28 @@ export const ResumeAction = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
} & PropsWithChildren) => {
|
||||
// allow replay request on network error until component unmount or success
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: resumeSubscriptionMutation,
|
||||
});
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onSubscriptionUpdate(data.resumeSubscription);
|
||||
onOpenChange(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [trigger, idempotencyKey, onSubscriptionUpdate, onOpenChange]);
|
||||
try {
|
||||
setIsMutating(true);
|
||||
await subscription.resumeSubscription(idempotencyKey);
|
||||
subscription.revalidate();
|
||||
await subscription.isRevalidating$.waitFor(v => !v);
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, onOpenChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+19
-23
@@ -1,23 +1,19 @@
|
||||
import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||
import { cancelSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { BaseActionProps } from '../types';
|
||||
|
||||
export interface AICancelProps extends BaseActionProps, ButtonProps {}
|
||||
export const AICancel = ({
|
||||
onSubscriptionUpdate,
|
||||
...btnProps
|
||||
}: AICancelProps) => {
|
||||
export interface AICancelProps extends ButtonProps {}
|
||||
export const AICancel = ({ ...btnProps }: AICancelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
});
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const cancel = useAsyncCallback(async () => {
|
||||
@@ -37,19 +33,19 @@ export const AICancel = ({
|
||||
type: 'primary',
|
||||
},
|
||||
onConfirm: async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey, plan: SubscriptionPlan.AI },
|
||||
{
|
||||
onSuccess: data => {
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onSubscriptionUpdate?.(data.cancelSubscription);
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
setMutating(true);
|
||||
await subscription.cancelSubscription(
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
setIdempotencyKey(nanoid());
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, t, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
}, [openConfirmModal, t, subscription, idempotencyKey]);
|
||||
|
||||
return (
|
||||
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
|
||||
|
||||
+1
-3
@@ -4,9 +4,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { BaseActionProps } from '../types';
|
||||
|
||||
export const AILogin = (btnProps: BaseActionProps & ButtonProps) => {
|
||||
export const AILogin = (btnProps: ButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
|
||||
+23
-35
@@ -5,28 +5,24 @@ import {
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||
import { resumeSubscriptionMutation, SubscriptionPlan } from '@affine/graphql';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { BaseActionProps } from '../types';
|
||||
export interface AIResumeProps extends ButtonProps {}
|
||||
|
||||
export interface AIResumeProps extends BaseActionProps, ButtonProps {}
|
||||
|
||||
export const AIResume = ({
|
||||
onSubscriptionUpdate,
|
||||
...btnProps
|
||||
}: AIResumeProps) => {
|
||||
export const AIResume = ({ ...btnProps }: AIResumeProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: resumeSubscriptionMutation,
|
||||
});
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
@@ -42,32 +38,24 @@ export const AIResume = ({
|
||||
cancelText:
|
||||
t['com.affine.payment.ai.action.resume.confirm.cancel-text'](),
|
||||
onConfirm: async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey, plan: SubscriptionPlan.AI },
|
||||
{
|
||||
onSuccess: data => {
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onSubscriptionUpdate?.(data.resumeSubscription);
|
||||
notify({
|
||||
icon: (
|
||||
<SingleSelectSelectSolidIcon
|
||||
color={cssVar('processingColor')}
|
||||
/>
|
||||
),
|
||||
title:
|
||||
t[
|
||||
'com.affine.payment.ai.action.resume.confirm.notify.title'
|
||||
](),
|
||||
message:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.msg'](),
|
||||
});
|
||||
},
|
||||
}
|
||||
setIsMutating(true);
|
||||
await subscription.resumeSubscription(
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
notify({
|
||||
icon: (
|
||||
<SingleSelectSelectSolidIcon color={cssVar('processingColor')} />
|
||||
),
|
||||
title:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.title'](),
|
||||
message:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.msg'](),
|
||||
});
|
||||
setIdempotencyKey(nanoid());
|
||||
},
|
||||
});
|
||||
}, [openConfirmModal, t, trigger, idempotencyKey, onSubscriptionUpdate]);
|
||||
}, [openConfirmModal, t, subscription, idempotencyKey]);
|
||||
|
||||
return (
|
||||
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>
|
||||
|
||||
+48
-59
@@ -1,75 +1,64 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/hooks/use-mutation';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import {
|
||||
createCheckoutSessionMutation,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { BaseActionProps } from '../types';
|
||||
import { useAffineAIPrice } from '../use-affine-ai-price';
|
||||
export interface AISubscribeProps extends ButtonProps {}
|
||||
|
||||
export interface AISubscribeProps extends BaseActionProps, ButtonProps {}
|
||||
export const AISubscribe = ({ ...btnProps }: AISubscribeProps) => {
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||
|
||||
export const AISubscribe = ({
|
||||
price,
|
||||
recurring = SubscriptionRecurring.Yearly,
|
||||
onSubscriptionUpdate,
|
||||
...btnProps
|
||||
}: AISubscribeProps) => {
|
||||
assertExists(price);
|
||||
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
|
||||
const { priceReadable, priceFrequency } = useAffineAIPrice(price);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
useEffect(() => {
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const newTabRef = useRef<Window | null>(null);
|
||||
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: createCheckoutSessionMutation,
|
||||
});
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
newTabRef.current = null;
|
||||
onSubscriptionUpdate?.();
|
||||
}, [onSubscriptionUpdate]);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (newTabRef.current) {
|
||||
newTabRef.current.removeEventListener('close', onClose);
|
||||
newTabRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [onClose]);
|
||||
if (isOpenedExternalWindow) {
|
||||
// when the external window is opened, revalidate the subscription status every 3 seconds
|
||||
const timer = setInterval(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
return;
|
||||
}, [isOpenedExternalWindow, subscriptionService]);
|
||||
|
||||
const subscribe = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{
|
||||
input: {
|
||||
recurring,
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.AI,
|
||||
coupon: null,
|
||||
successCallbackLink: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: data => {
|
||||
const newTab = popupWindow(data.createCheckoutSession);
|
||||
if (newTab) {
|
||||
newTabRef.current = newTab;
|
||||
newTab.addEventListener('close', onClose);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [idempotencyKey, onClose, recurring, trigger]);
|
||||
setMutating(true);
|
||||
try {
|
||||
const session = await subscriptionService.createCheckoutSession({
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.AI,
|
||||
coupon: null,
|
||||
successCallbackLink: null,
|
||||
});
|
||||
popupWindow(session);
|
||||
setOpenedExternalWindow(true);
|
||||
setIdempotencyKey(nanoid());
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
}, [idempotencyKey, subscriptionService]);
|
||||
|
||||
if (!price.yearlyAmount) return null;
|
||||
if (!price || !price.yearlyAmount) {
|
||||
// TODO: loading UI
|
||||
return null;
|
||||
}
|
||||
|
||||
const priceReadable = `$${(price.yearlyAmount / 100).toFixed(2)}`;
|
||||
const priceFrequency = t['com.affine.payment.billing-setting.year']();
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
+53
-32
@@ -1,40 +1,45 @@
|
||||
import { Button } from '@affine/component';
|
||||
import {
|
||||
type SubscriptionMutator,
|
||||
useUserSubscription,
|
||||
} from '@affine/core/hooks/use-subscription';
|
||||
import {
|
||||
type PricesQuery,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AIPlanLayout } from '../layout';
|
||||
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||
import * as styles from './ai-plan.css';
|
||||
import { AIBenefits } from './benefits';
|
||||
import type { BaseActionProps } from './types';
|
||||
import { useAffineAISubscription } from './use-affine-ai-subscription';
|
||||
|
||||
interface AIPlanProps {
|
||||
price?: PricesQuery['prices'][number];
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}
|
||||
export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
||||
export const AIPlan = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const recurring = SubscriptionRecurring.Yearly;
|
||||
|
||||
const { Action, billingTip } = useAffineAISubscription();
|
||||
const [subscription] = useUserSubscription(SubscriptionPlan.AI);
|
||||
const authService = useService(AuthService);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
const isLoggedIn =
|
||||
useLiveData(authService.session.status$) === 'authenticated';
|
||||
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
// yearly subscription should always be available
|
||||
if (!price?.yearlyAmount) return null;
|
||||
if (!price?.yearlyAmount || subscription === null) {
|
||||
// TODO: loading UI
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseActionProps: BaseActionProps = {
|
||||
price,
|
||||
recurring,
|
||||
onSubscriptionUpdate,
|
||||
};
|
||||
const billingTip = subscription?.nextBillAt
|
||||
? t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: timestampToLocalDate(subscription.nextBillAt),
|
||||
})
|
||||
: subscription?.canceledAt && subscription.end
|
||||
? t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: timestampToLocalDate(subscription.end),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AIPlanLayout
|
||||
@@ -60,13 +65,29 @@ export const AIPlan = ({ price, onSubscriptionUpdate }: AIPlanProps) => {
|
||||
|
||||
<div className={styles.actionBlock}>
|
||||
<div className={styles.actionButtons}>
|
||||
<Action {...baseActionProps} className={styles.purchaseButton} />
|
||||
{subscription ? null : (
|
||||
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
|
||||
<Button className={styles.learnAIButton}>
|
||||
{t['com.affine.payment.ai.pricing-plan.learn']()}
|
||||
</Button>
|
||||
</a>
|
||||
{isLoggedIn ? (
|
||||
subscription ? (
|
||||
subscription.canceledAt ? (
|
||||
<AIResume className={styles.purchaseButton} />
|
||||
) : (
|
||||
<AICancel className={styles.purchaseButton} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<AISubscribe className={styles.learnAIButton} />
|
||||
<a
|
||||
href="https://ai.affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Button className={styles.learnAIButton}>
|
||||
{t['com.affine.payment.ai.pricing-plan.learn']()}
|
||||
</Button>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<AILogin className={styles.purchaseButton} />
|
||||
)}
|
||||
</div>
|
||||
{billingTip ? (
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
|
||||
import type { PricesQuery, SubscriptionRecurring } from '@affine/graphql';
|
||||
|
||||
export interface BaseActionProps {
|
||||
price?: PricesQuery['prices'][number];
|
||||
recurring?: SubscriptionRecurring;
|
||||
onSubscriptionUpdate?: SubscriptionMutator;
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import type { PricesQuery } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
export const useAffineAIPrice = (price: PricesQuery['prices'][number]) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
assertExists(price.yearlyAmount, 'AFFiNE AI yearly price is missing');
|
||||
|
||||
const priceReadable = `$${(price.yearlyAmount / 100).toFixed(2)}`;
|
||||
const priceFrequency = t['com.affine.payment.billing-setting.year']();
|
||||
|
||||
return { priceReadable, priceFrequency };
|
||||
};
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||
|
||||
const plan = SubscriptionPlan.AI;
|
||||
|
||||
export type ActionType = 'login' | 'subscribe' | 'resume' | 'cancel';
|
||||
|
||||
export const useAffineAISubscription = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
|
||||
const [subscription] = useUserSubscription(plan);
|
||||
|
||||
const isCancelled = !!subscription?.canceledAt;
|
||||
const actionType: ActionType = !loggedIn
|
||||
? 'login'
|
||||
: !subscription
|
||||
? 'subscribe'
|
||||
: isCancelled
|
||||
? 'resume'
|
||||
: 'cancel';
|
||||
|
||||
const Action = {
|
||||
login: AILogin,
|
||||
subscribe: AISubscribe,
|
||||
resume: AIResume,
|
||||
cancel: AICancel,
|
||||
}[actionType];
|
||||
|
||||
const isFree = !subscription;
|
||||
|
||||
const billingTip = subscription?.nextBillAt
|
||||
? t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: timestampToLocalDate(subscription.nextBillAt),
|
||||
})
|
||||
: subscription?.canceledAt && subscription.end
|
||||
? t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: timestampToLocalDate(subscription.end),
|
||||
})
|
||||
: null;
|
||||
|
||||
return { actionType, Action, billingTip, isFree };
|
||||
};
|
||||
+29
-60
@@ -1,20 +1,13 @@
|
||||
import { notify, Switch } from '@affine/component';
|
||||
import {
|
||||
pricesQuery,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import { Switch } from '@affine/component';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
import { SWRErrorBoundary } from '../../../../../components/pure/swr-error-bundary';
|
||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||
import { useQuery } from '../../../../../hooks/use-query';
|
||||
import { useUserSubscription } from '../../../../../hooks/use-subscription';
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { AIPlan } from './ai/ai-plan';
|
||||
import { type FixedPrice, getPlanDetail } from './cloud-plans';
|
||||
import { CloudPlanLayout, PlanLayout } from './layout';
|
||||
@@ -36,19 +29,24 @@ const getRecurringLabel = ({
|
||||
|
||||
const Settings = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [subscription, mutateSubscription] = useUserSubscription();
|
||||
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
const loggedIn =
|
||||
useLiveData(useService(AuthService).session.status$) === 'authenticated';
|
||||
const planDetail = useMemo(() => getPlanDetail(t), [t]);
|
||||
const scrollWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
data: { prices },
|
||||
} = useQuery({
|
||||
query: pricesQuery,
|
||||
});
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const primarySubscription = useLiveData(
|
||||
subscriptionService.subscription.primary$
|
||||
);
|
||||
const prices = useLiveData(subscriptionService.prices.prices$);
|
||||
|
||||
prices.forEach(price => {
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
prices?.forEach(price => {
|
||||
const detail = planDetail.get(price.plan);
|
||||
|
||||
if (detail?.type === 'fixed') {
|
||||
@@ -64,13 +62,13 @@ const Settings = () => {
|
||||
});
|
||||
|
||||
const [recurring, setRecurring] = useState<SubscriptionRecurring>(
|
||||
subscription?.recurring ?? SubscriptionRecurring.Yearly
|
||||
primarySubscription?.recurring ?? SubscriptionRecurring.Yearly
|
||||
);
|
||||
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const isCanceled = !!subscription?.canceledAt;
|
||||
const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free;
|
||||
const isCanceled = !!primarySubscription?.canceledAt;
|
||||
const currentRecurring =
|
||||
subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
primarySubscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const yearlyDiscount = (
|
||||
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
|
||||
@@ -176,33 +174,7 @@ const Settings = () => {
|
||||
const cloudScroll = (
|
||||
<div className={styles.planCardsWrapper} ref={scrollWrapper}>
|
||||
{Array.from(planDetail.values()).map(detail => {
|
||||
return (
|
||||
<PlanCard
|
||||
key={detail.plan}
|
||||
onSubscriptionUpdate={mutateSubscription}
|
||||
onNotify={({ detail, recurring }) => {
|
||||
notify({
|
||||
style: 'normal',
|
||||
icon: (
|
||||
<SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />
|
||||
),
|
||||
title: t['com.affine.payment.updated-notify-title'](),
|
||||
message:
|
||||
detail.plan === SubscriptionPlan.Free
|
||||
? t[
|
||||
'com.affine.payment.updated-notify-msg.cancel-subscription'
|
||||
]()
|
||||
: t['com.affine.payment.updated-notify-msg']({
|
||||
plan: getRecurringLabel({
|
||||
recurring: recurring as SubscriptionRecurring,
|
||||
t,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}}
|
||||
{...{ detail, subscription, recurring }}
|
||||
/>
|
||||
);
|
||||
return <PlanCard key={detail.plan} {...{ detail, recurring }} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -214,6 +186,10 @@ const Settings = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (prices === null) {
|
||||
return <PlansSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PlanLayout
|
||||
cloud={
|
||||
@@ -225,12 +201,7 @@ const Settings = () => {
|
||||
scrollRef={scrollWrapper}
|
||||
/>
|
||||
}
|
||||
ai={
|
||||
<AIPlan
|
||||
price={prices.find(p => p.plan === SubscriptionPlan.AI)}
|
||||
onSubscriptionUpdate={mutateSubscription}
|
||||
/>
|
||||
}
|
||||
ai={<AIPlan />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -238,9 +209,7 @@ const Settings = () => {
|
||||
export const AFFiNEPricingPlans = () => {
|
||||
return (
|
||||
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
|
||||
<Suspense fallback={<PlansSkeleton />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
<Settings />
|
||||
</SWRErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
+67
-148
@@ -1,30 +1,21 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type {
|
||||
Subscription,
|
||||
SubscriptionMutator,
|
||||
} from '@affine/core/hooks/use-subscription';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import type { SubscriptionRecurring } from '@affine/graphql';
|
||||
import {
|
||||
createCheckoutSessionMutation,
|
||||
SubscriptionPlan,
|
||||
SubscriptionStatus,
|
||||
updateSubscriptionMutation,
|
||||
} from '@affine/graphql';
|
||||
import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DoneIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { openPaymentDisableAtom } from '../../../../../atoms';
|
||||
import { authAtom } from '../../../../../atoms/index';
|
||||
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
|
||||
import { useMutation } from '../../../../../hooks/use-mutation';
|
||||
import { mixpanel } from '../../../../../utils';
|
||||
import { CancelAction, ResumeAction } from './actions';
|
||||
import type { DynamicPrice, FixedPrice } from './cloud-plans';
|
||||
@@ -33,24 +24,23 @@ import * as styles from './style.css';
|
||||
|
||||
interface PlanCardProps {
|
||||
detail: FixedPrice | DynamicPrice;
|
||||
subscription?: Subscription | null;
|
||||
recurring: SubscriptionRecurring;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
onNotify: (info: {
|
||||
detail: FixedPrice | DynamicPrice;
|
||||
recurring: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const PlanCard = (props: PlanCardProps) => {
|
||||
const { detail, subscription, recurring } = props;
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const { detail, recurring } = props;
|
||||
const loggedIn =
|
||||
useLiveData(useService(AuthService).session.status$) === 'authenticated';
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const primarySubscription = useLiveData(
|
||||
subscriptionService.subscription.primary$
|
||||
);
|
||||
const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const isCurrent =
|
||||
loggedIn &&
|
||||
detail.plan === currentPlan &&
|
||||
recurring === subscription?.recurring;
|
||||
recurring === primarySubscription?.recurring;
|
||||
const isPro = detail.plan === SubscriptionPlan.Pro;
|
||||
|
||||
return (
|
||||
@@ -97,26 +87,16 @@ export const PlanCard = (props: PlanCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButton = ({
|
||||
detail,
|
||||
subscription,
|
||||
recurring,
|
||||
onSubscriptionUpdate,
|
||||
onNotify,
|
||||
}: PlanCardProps) => {
|
||||
const ActionButton = ({ detail, recurring }: PlanCardProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring = subscription?.recurring;
|
||||
|
||||
const mutateAndNotify = useCallback(
|
||||
(sub: Parameters<SubscriptionMutator>[0]) => {
|
||||
mixpanel.track_forms('Subscription', detail.plan, sub);
|
||||
onSubscriptionUpdate?.(sub);
|
||||
onNotify?.({ detail, recurring });
|
||||
},
|
||||
[onSubscriptionUpdate, onNotify, detail, recurring]
|
||||
const loggedIn =
|
||||
useLiveData(useService(AuthService).session.status$) === 'authenticated';
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const primarySubscription = useLiveData(
|
||||
subscriptionService.subscription.primary$
|
||||
);
|
||||
const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring = primarySubscription?.recurring;
|
||||
|
||||
// branches:
|
||||
// if contact => 'Contact Sales'
|
||||
@@ -148,43 +128,33 @@ const ActionButton = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isCanceled = !!subscription?.canceledAt;
|
||||
const isCanceled = !!primarySubscription?.canceledAt;
|
||||
const isFree = detail.plan === SubscriptionPlan.Free;
|
||||
const isCurrent =
|
||||
detail.plan === currentPlan &&
|
||||
(isFree
|
||||
? true
|
||||
: currentRecurring === recurring &&
|
||||
subscription?.status === SubscriptionStatus.Active);
|
||||
primarySubscription?.status === SubscriptionStatus.Active);
|
||||
|
||||
// is current
|
||||
if (isCurrent) {
|
||||
return isCanceled ? (
|
||||
<ResumeButton onSubscriptionUpdate={mutateAndNotify} />
|
||||
) : (
|
||||
<CurrentPlan />
|
||||
);
|
||||
return isCanceled ? <ResumeButton /> : <CurrentPlan />;
|
||||
}
|
||||
|
||||
if (isFree) {
|
||||
return (
|
||||
<Downgrade disabled={isCanceled} onSubscriptionUpdate={mutateAndNotify} />
|
||||
);
|
||||
return <Downgrade disabled={isCanceled} />;
|
||||
}
|
||||
|
||||
return currentPlan === detail.plan ? (
|
||||
<ChangeRecurring
|
||||
from={currentRecurring as SubscriptionRecurring}
|
||||
to={recurring as SubscriptionRecurring}
|
||||
due={subscription?.nextBillAt || ''}
|
||||
onSubscriptionUpdate={mutateAndNotify}
|
||||
due={primarySubscription?.nextBillAt || ''}
|
||||
disabled={isCanceled}
|
||||
/>
|
||||
) : (
|
||||
<Upgrade
|
||||
recurring={recurring as SubscriptionRecurring}
|
||||
onSubscriptionUpdate={mutateAndNotify}
|
||||
/>
|
||||
<Upgrade recurring={recurring as SubscriptionRecurring} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -197,13 +167,7 @@ const CurrentPlan = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Downgrade = ({
|
||||
disabled,
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const Downgrade = ({ disabled }: { disabled?: boolean }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -212,11 +176,7 @@ const Downgrade = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<CancelAction
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||
>
|
||||
<CancelAction open={open} onOpenChange={setOpen}>
|
||||
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
|
||||
<div className={styles.planAction}>
|
||||
<Button
|
||||
@@ -266,27 +226,25 @@ const BookDemo = ({ plan }: { plan: SubscriptionPlan }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Upgrade = ({
|
||||
recurring,
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
recurring: SubscriptionRecurring;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const Upgrade = ({ recurring }: { recurring: SubscriptionRecurring }) => {
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: createCheckoutSessionMutation,
|
||||
});
|
||||
|
||||
const newTabRef = useRef<Window | null>(null);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
|
||||
// allow replay request on network error until component unmount
|
||||
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
newTabRef.current = null;
|
||||
onSubscriptionUpdate();
|
||||
}, [onSubscriptionUpdate]);
|
||||
useEffect(() => {
|
||||
if (isOpenedExternalWindow) {
|
||||
// when the external window is opened, revalidate the subscription status every 3 seconds
|
||||
const timer = setInterval(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
return;
|
||||
}, [isOpenedExternalWindow, subscriptionService]);
|
||||
|
||||
const [, openPaymentDisableModal] = useAtom(openPaymentDisableAtom);
|
||||
const upgrade = useAsyncCallback(async () => {
|
||||
@@ -295,42 +253,20 @@ const Upgrade = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (newTabRef.current) {
|
||||
newTabRef.current.focus();
|
||||
} else {
|
||||
await trigger(
|
||||
{
|
||||
input: {
|
||||
recurring,
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
||||
coupon: null,
|
||||
successCallbackLink: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: data => {
|
||||
const newTab = popupWindow(data.createCheckoutSession);
|
||||
|
||||
if (newTab) {
|
||||
newTabRef.current = newTab;
|
||||
|
||||
newTab.addEventListener('close', onClose);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [openPaymentDisableModal, trigger, recurring, idempotencyKey, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (newTabRef.current) {
|
||||
newTabRef.current.removeEventListener('close', onClose);
|
||||
newTabRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [onClose]);
|
||||
setMutating(true);
|
||||
const link = await subscriptionService.createCheckoutSession({
|
||||
recurring,
|
||||
idempotencyKey,
|
||||
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
||||
coupon: null,
|
||||
successCallbackLink: null,
|
||||
});
|
||||
setMutating(false);
|
||||
setIdempotencyKey(nanoid());
|
||||
popupWindow(link);
|
||||
setOpenedExternalWindow(true);
|
||||
mixpanel.track_forms('Subscription', SubscriptionPlan.Pro);
|
||||
}, [openPaymentDisableModal, subscriptionService, recurring, idempotencyKey]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -350,34 +286,25 @@ const ChangeRecurring = ({
|
||||
to,
|
||||
disabled,
|
||||
due,
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
from: SubscriptionRecurring;
|
||||
to: SubscriptionRecurring;
|
||||
disabled?: boolean;
|
||||
due: string;
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
// allow replay request on network error until component unmount or success
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: updateSubscriptionMutation,
|
||||
});
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const change = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ recurring: to, idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onSubscriptionUpdate(data.updateSubscriptionRecurring);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [trigger, to, idempotencyKey, onSubscriptionUpdate]);
|
||||
setIsMutating(true);
|
||||
await subscription.setSubscriptionRecurring(idempotencyKey, to);
|
||||
setIdempotencyKey(nanoid());
|
||||
setIsMutating(false);
|
||||
}, [subscription, to, idempotencyKey]);
|
||||
|
||||
const changeCurringContent = (
|
||||
<Trans values={{ from, to, due }} className={styles.downgradeContent}>
|
||||
@@ -437,21 +364,13 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeButton = ({
|
||||
onSubscriptionUpdate,
|
||||
}: {
|
||||
onSubscriptionUpdate: SubscriptionMutator;
|
||||
}) => {
|
||||
const ResumeButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<ResumeAction
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSubscriptionUpdate={onSubscriptionUpdate}
|
||||
>
|
||||
<ResumeAction open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
|
||||
@@ -6,14 +6,18 @@ import {
|
||||
openIssueFeedbackModalAtom,
|
||||
openStarAFFiNEModalAtom,
|
||||
} from '@affine/core/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import { GeneralSetting } from './general-setting';
|
||||
import { SettingSidebar } from './setting-sidebar';
|
||||
@@ -48,7 +52,7 @@ const SettingModalInner = ({
|
||||
onSettingClick,
|
||||
...modalProps
|
||||
}: SettingProps) => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
|
||||
const modalContentRef = useRef<HTMLDivElement>(null);
|
||||
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
+34
-26
@@ -4,10 +4,10 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
||||
import { useIsEarlyAccess } from '@affine/core/hooks/affine/use-user-features';
|
||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
@@ -15,17 +15,15 @@ import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
Workspace,
|
||||
WorkspaceManager,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useMemo } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { mixpanel } from '../../../../utils';
|
||||
import { UserPlanButton } from '../../auth/user-plan-button';
|
||||
import { useGeneralSettingList } from '../general-setting';
|
||||
@@ -37,11 +35,12 @@ export type UserInfoProps = {
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export const UserInfo = ({
|
||||
onAccountSettingClick,
|
||||
active,
|
||||
}: UserInfoProps): ReactElement => {
|
||||
const user = useCurrentUser();
|
||||
export const UserInfo = ({ onAccountSettingClick, active }: UserInfoProps) => {
|
||||
const account = useLiveData(useService(AuthService).session.account$);
|
||||
if (!account) {
|
||||
// TODO: loading ui
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-testid="user-info-card"
|
||||
@@ -52,21 +51,21 @@ export const UserInfo = ({
|
||||
>
|
||||
<Avatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.avatarUrl}
|
||||
name={account.label}
|
||||
url={account.avatar}
|
||||
className="avatar"
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name-container">
|
||||
<div className="name" title={user.name}>
|
||||
{user.name}
|
||||
<div className="name" title={account.label}>
|
||||
{account.label}
|
||||
</div>
|
||||
<UserPlanButton />
|
||||
</div>
|
||||
|
||||
<div className="email" title={user.email}>
|
||||
{user.email}
|
||||
<div className="email" title={account.email}>
|
||||
{account.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +112,7 @@ export const SettingSidebar = ({
|
||||
selectedWorkspaceId: string | null;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const generalList = useGeneralSettingList();
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
@@ -207,7 +206,7 @@ export const WorkspaceList = ({
|
||||
activeSubTab: WorkspaceSubTab;
|
||||
}) => {
|
||||
const workspaces = useLiveData(
|
||||
useService(WorkspaceManager).list.workspaceList$
|
||||
useService(WorkspacesService).list.workspaces$
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -257,14 +256,23 @@ const WorkspaceListItem = ({
|
||||
activeSubTab?: WorkspaceSubTab;
|
||||
onClick: (subTab: WorkspaceSubTab) => void;
|
||||
}) => {
|
||||
const { workspaceService, userFeatureService } = useServices({
|
||||
WorkspaceService,
|
||||
UserFeatureService,
|
||||
});
|
||||
const information = useWorkspaceInfo(meta);
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const isCurrent = currentWorkspace.id === meta.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const isOwner = useIsWorkspaceOwner(meta);
|
||||
const isEarlyAccess = useIsEarlyAccess();
|
||||
const isEarlyAccess = useLiveData(
|
||||
userFeatureService.userFeature.isEarlyAccess$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
userFeatureService.userFeature.revalidate();
|
||||
}, [userFeatureService]);
|
||||
|
||||
const onClickPreference = useCallback(() => {
|
||||
onClick('preference');
|
||||
@@ -274,7 +282,7 @@ const WorkspaceListItem = ({
|
||||
return subTabConfigs
|
||||
.filter(({ key }) => {
|
||||
if (key === 'experimental-features') {
|
||||
return isOwner && isEarlyAccess;
|
||||
return information?.isOwner && isEarlyAccess;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
@@ -294,7 +302,7 @@ const WorkspaceListItem = ({
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, [activeSubTab, isEarlyAccess, isOwner, onClick, t]);
|
||||
}, [activeSubTab, information?.isOwner, isEarlyAccess, onClick, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+1
-9
@@ -1,6 +1,5 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
||||
import { ExperimentalFeatures } from './experimental-features';
|
||||
import { WorkspaceSettingDetail } from './new-workspace-setting-detail';
|
||||
import { WorkspaceSettingProperties } from './properties';
|
||||
@@ -12,16 +11,9 @@ export const WorkspaceSetting = ({
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
subTab: 'preference' | 'experimental-features' | 'properties';
|
||||
}) => {
|
||||
const isOwner = useIsWorkspaceOwner(workspaceMetadata);
|
||||
|
||||
switch (subTab) {
|
||||
case 'preference':
|
||||
return (
|
||||
<WorkspaceSettingDetail
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
return <WorkspaceSettingDetail workspaceMetadata={workspaceMetadata} />;
|
||||
case 'experimental-features':
|
||||
return <ExperimentalFeatures />;
|
||||
case 'properties':
|
||||
|
||||
+34
-27
@@ -2,16 +2,18 @@ import { notify } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
Workspace,
|
||||
WorkspaceManager,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../../atoms';
|
||||
import {
|
||||
@@ -19,41 +21,45 @@ import {
|
||||
useNavigateHelper,
|
||||
} from '../../../../../../hooks/use-navigate-helper';
|
||||
import { WorkspaceSubPath } from '../../../../../../shared';
|
||||
import type { WorkspaceSettingDetailProps } from '../types';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
|
||||
export interface DeleteLeaveWorkspaceProps
|
||||
extends WorkspaceSettingDetailProps {}
|
||||
|
||||
export const DeleteLeaveWorkspace = ({
|
||||
workspaceMetadata,
|
||||
isOwner,
|
||||
}: DeleteLeaveWorkspaceProps) => {
|
||||
export const DeleteLeaveWorkspace = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
// fixme: cloud regression
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspaceList = useLiveData(workspaceManager.list.workspaceList$);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaceList = useLiveData(workspacesService.list.workspaces$);
|
||||
const currentWorkspaceId = useLiveData(
|
||||
useService(GlobalContextService).globalContext.workspaceId.$
|
||||
);
|
||||
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const onLeaveOrDelete = useCallback(() => {
|
||||
if (isOwner) {
|
||||
setShowDelete(true);
|
||||
} else {
|
||||
setShowLeave(true);
|
||||
if (isOwner !== null) {
|
||||
if (isOwner) {
|
||||
setShowDelete(true);
|
||||
} else {
|
||||
setShowLeave(true);
|
||||
}
|
||||
}
|
||||
}, [isOwner]);
|
||||
|
||||
const onDeleteConfirm = useAsyncCallback(async () => {
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
|
||||
if (currentWorkspace?.id === workspaceMetadata.id) {
|
||||
if (currentWorkspaceId === workspace.id) {
|
||||
const backWorkspace = workspaceList.find(
|
||||
ws => ws.id !== workspaceMetadata.id
|
||||
ws => ws.id !== currentWorkspaceId
|
||||
);
|
||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||
if (backWorkspace) {
|
||||
@@ -67,17 +73,18 @@ export const DeleteLeaveWorkspace = ({
|
||||
}
|
||||
}
|
||||
|
||||
await workspaceManager.deleteWorkspace(workspaceMetadata);
|
||||
await workspacesService.deleteWorkspace(workspace.meta);
|
||||
notify.success({ title: t['Successfully deleted']() });
|
||||
}, [
|
||||
currentWorkspace?.id,
|
||||
jumpToIndex,
|
||||
jumpToSubPath,
|
||||
setSettingModal,
|
||||
currentWorkspaceId,
|
||||
workspace.id,
|
||||
workspace.meta,
|
||||
workspacesService,
|
||||
t,
|
||||
workspaceList,
|
||||
workspaceManager,
|
||||
workspaceMetadata,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -102,7 +109,7 @@ export const DeleteLeaveWorkspace = ({
|
||||
onConfirm={onDeleteConfirm}
|
||||
open={showDelete}
|
||||
onOpenChange={setShowDelete}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
workspaceMetadata={workspace.meta}
|
||||
/>
|
||||
) : (
|
||||
<ConfirmModal
|
||||
|
||||
+14
-11
@@ -1,30 +1,33 @@
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type Workspace,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../atoms';
|
||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface PublishPanelProps extends WorkspaceSettingDetailProps {
|
||||
export interface PublishPanelProps {
|
||||
workspace: Workspace | null;
|
||||
}
|
||||
|
||||
export const EnableCloudPanel = ({
|
||||
workspaceMetadata,
|
||||
workspace,
|
||||
}: PublishPanelProps) => {
|
||||
export const EnableCloudPanel = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const name = useLiveData(workspace.name$);
|
||||
const flavour = workspace.flavour;
|
||||
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -38,7 +41,7 @@ export const EnableCloudPanel = ({
|
||||
});
|
||||
}, [confirmEnableCloud, setSettingModal, workspace]);
|
||||
|
||||
if (workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
if (flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,7 +49,7 @@ export const EnableCloudPanel = ({
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['Workspace saved locally']({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
name: name ?? UNTITLED_WORKSPACE_NAME,
|
||||
})}
|
||||
desc={t['Enable cloud hint']()}
|
||||
spreadCol={false}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export const ExportPanel = ({
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isOnline) {
|
||||
await workspace.engine.waitForSynced();
|
||||
await workspace.engine.waitForDocSynced();
|
||||
await workspace.engine.blob.sync();
|
||||
}
|
||||
|
||||
|
||||
+15
-11
@@ -3,12 +3,12 @@ import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useServerFeatures } from '@affine/core/hooks/affine/use-server-config';
|
||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { FrameworkScope } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
@@ -20,10 +20,10 @@ import { ProfilePanel } from './profile';
|
||||
import { StoragePanel } from './storage';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
export const WorkspaceSettingDetail = ({
|
||||
workspaceMetadata,
|
||||
}: WorkspaceSettingDetailProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { payment: hasPaymentFeature } = useServerFeatures();
|
||||
const workspaceMetadata = props.workspaceMetadata;
|
||||
|
||||
// useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
|
||||
// we don't have a better way to do this now
|
||||
@@ -42,8 +42,12 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
});
|
||||
}, [workspace]);
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<SettingHeader
|
||||
title={t[`Workspace Settings with name`]({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
@@ -56,13 +60,13 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel workspace={workspace} {...props} />
|
||||
<LabelsPanel {...props} />
|
||||
<ProfilePanel />
|
||||
<LabelsPanel />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<EnableCloudPanel workspace={workspace} {...props} />
|
||||
<MembersPanel upgradable={hasPaymentFeature} {...props} />
|
||||
<EnableCloudPanel />
|
||||
<MembersPanel />
|
||||
</SettingWrapper>
|
||||
{environment.isDesktop && (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
@@ -76,7 +80,7 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
</SettingWrapper>
|
||||
)}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace {...props} />
|
||||
<DeleteLeaveWorkspace />
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: 'var(--affine-text-secondary-color)' }}>
|
||||
@@ -91,6 +95,6 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
</>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-10
@@ -1,9 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {}
|
||||
|
||||
type WorkspaceStatus =
|
||||
| 'local'
|
||||
@@ -35,10 +34,13 @@ const Label = ({ value, background }: LabelProps) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const LabelsPanel = ({
|
||||
workspaceMetadata,
|
||||
isOwner,
|
||||
}: LabelsPanelProps) => {
|
||||
export const LabelsPanel = () => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
const labelMap: LabelMap = useMemo(
|
||||
() => ({
|
||||
local: {
|
||||
@@ -74,9 +76,9 @@ export const LabelsPanel = ({
|
||||
);
|
||||
const labelConditions: labelConditionsProps[] = [
|
||||
{ condition: !isOwner, label: 'joinedWorkspace' },
|
||||
{ condition: workspaceMetadata.flavour === 'local', label: 'local' },
|
||||
{ condition: workspace.flavour === 'local', label: 'local' },
|
||||
{
|
||||
condition: workspaceMetadata.flavour === 'affine-cloud',
|
||||
condition: workspace.flavour === 'affine-cloud',
|
||||
label: 'syncCloud',
|
||||
},
|
||||
//TODO: add these labels
|
||||
|
||||
+67
-39
@@ -16,38 +16,45 @@ import { Menu, MenuItem } from '@affine/component/ui/menu';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import type { CheckedUser } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useCurrentUser } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useInviteMember } from '@affine/core/hooks/affine/use-invite-member';
|
||||
import { useMemberCount } from '@affine/core/hooks/affine/use-member-count';
|
||||
import type { Member } from '@affine/core/hooks/affine/use-members';
|
||||
import { useMembers } from '@affine/core/hooks/affine/use-members';
|
||||
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
||||
import { useWorkspaceQuota } from '@affine/core/hooks/use-quota';
|
||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission, SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
useEnsureLiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
type AuthAccountInfo,
|
||||
AuthService,
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
} from '../../../../../modules/cloud';
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
const COUNT_PER_PAGE = 8;
|
||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||
upgradable: boolean;
|
||||
}
|
||||
type OnRevoke = (memberId: string) => void;
|
||||
const MembersPanelLocal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -62,13 +69,19 @@ const MembersPanelLocal = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CloudWorkspaceMembersPanel = ({
|
||||
isOwner,
|
||||
upgradable,
|
||||
workspaceMetadata,
|
||||
}: MembersPanelProps) => {
|
||||
const workspaceId = workspaceMetadata.id;
|
||||
const memberCount = useMemberCount(workspaceId);
|
||||
export const CloudWorkspaceMembersPanel = () => {
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const memberCount = useMemberCount(workspace.id);
|
||||
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const checkMemberCountLimit = useCallback(
|
||||
(memberCount: number, memberLimit?: number) => {
|
||||
@@ -78,14 +91,22 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const quota = useWorkspaceQuota(workspaceId);
|
||||
const [subscription] = useUserSubscription();
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const isLimited = checkMemberCountLimit(memberCount, quota.memberLimit);
|
||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||
useEffect(() => {
|
||||
workspaceQuotaService.quota.revalidate();
|
||||
}, [workspaceQuotaService]);
|
||||
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const plan = useLiveData(
|
||||
subscriptionService.subscription.primary$.map(s => s?.plan)
|
||||
);
|
||||
const isLimited = workspaceQuota
|
||||
? checkMemberCountLimit(memberCount, workspaceQuota.memberLimit)
|
||||
: null;
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const { invite, isMutating } = useInviteMember(workspaceId);
|
||||
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
|
||||
const { invite, isMutating } = useInviteMember(workspace.id);
|
||||
const revokeMemberPermission = useRevokeMemberPermission(workspace.id);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [memberSkip, setMemberSkip] = useState(0);
|
||||
@@ -150,11 +171,11 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
);
|
||||
|
||||
const desc = useMemo(() => {
|
||||
if (!quota) return null;
|
||||
if (!workspaceQuota) return null;
|
||||
return (
|
||||
<span>
|
||||
{t['com.affine.payment.member.description2']()}
|
||||
{upgradable ? (
|
||||
{hasPaymentFeature ? (
|
||||
<div
|
||||
className={style.goUpgradeWrapper}
|
||||
onClick={handleUpgradeConfirm}
|
||||
@@ -166,14 +187,19 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}, [handleUpgradeConfirm, quota, t, upgradable]);
|
||||
}, [handleUpgradeConfirm, hasPaymentFeature, t, workspaceQuota]);
|
||||
|
||||
if (workspaceQuota === null) {
|
||||
// TODO: loading ui
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={`${t['Members']()} (${memberCount}/${quota.humanReadable.memberLimit})`}
|
||||
name={`${t['Members']()} (${memberCount}/${workspaceQuota.humanReadable.memberLimit})`}
|
||||
desc={desc}
|
||||
spreadCol={isOwner}
|
||||
spreadCol={!!isOwner}
|
||||
>
|
||||
{isOwner ? (
|
||||
<>
|
||||
@@ -182,8 +208,8 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
<MemberLimitModal
|
||||
isFreePlan={plan === SubscriptionPlan.Free}
|
||||
open={open}
|
||||
plan={quota.humanReadable.name}
|
||||
quota={quota.humanReadable.memberLimit}
|
||||
plan={workspaceQuota?.humanReadable.name ?? ''}
|
||||
quota={workspaceQuota?.humanReadable.memberLimit ?? ''}
|
||||
setOpen={setOpen}
|
||||
onConfirm={handleUpgradeConfirm}
|
||||
/>
|
||||
@@ -206,8 +232,8 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
>
|
||||
<Suspense fallback={<MemberListFallback memberCount={memberCount} />}>
|
||||
<MemberList
|
||||
workspaceId={workspaceId}
|
||||
isOwner={isOwner}
|
||||
workspaceId={workspace.id}
|
||||
isOwner={!!isOwner}
|
||||
skip={memberSkip}
|
||||
onRevoke={onRevoke}
|
||||
/>
|
||||
@@ -259,16 +285,17 @@ const MemberList = ({
|
||||
onRevoke: OnRevoke;
|
||||
}) => {
|
||||
const members = useMembers(workspaceId, skip, COUNT_PER_PAGE);
|
||||
const currentUser = useCurrentUser();
|
||||
const session = useService(AuthService).session;
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
|
||||
return (
|
||||
<div className={style.memberList}>
|
||||
{members.map(member => (
|
||||
<MemberItem
|
||||
currentAccount={account}
|
||||
key={member.id}
|
||||
member={member}
|
||||
isOwner={isOwner}
|
||||
currentUser={currentUser}
|
||||
onRevoke={onRevoke}
|
||||
/>
|
||||
))}
|
||||
@@ -279,12 +306,12 @@ const MemberList = ({
|
||||
const MemberItem = ({
|
||||
member,
|
||||
isOwner,
|
||||
currentUser,
|
||||
currentAccount,
|
||||
onRevoke,
|
||||
}: {
|
||||
member: Member;
|
||||
isOwner: boolean;
|
||||
currentUser: CheckedUser;
|
||||
currentAccount: AuthAccountInfo;
|
||||
onRevoke: OnRevoke;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -295,10 +322,10 @@ const MemberItem = ({
|
||||
|
||||
const operationButtonInfo = useMemo(() => {
|
||||
return {
|
||||
show: isOwner && currentUser.id !== member.id,
|
||||
show: isOwner && currentAccount.id !== member.id,
|
||||
leaveOrRevokeText: t['Remove from workspace'](),
|
||||
};
|
||||
}, [currentUser.id, isOwner, member.id, t]);
|
||||
}, [currentAccount.id, isOwner, member.id, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -354,14 +381,15 @@ const MemberItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
export const MembersPanel = (): ReactElement | null => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
<AffineErrorBoundary>
|
||||
<Suspense>
|
||||
<CloudWorkspaceMembersPanel {...props} />
|
||||
<CloudWorkspaceMembersPanel />
|
||||
</Suspense>
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
|
||||
+9
-8
@@ -4,25 +4,26 @@ import { Button } from '@affine/component/ui/button';
|
||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CameraIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import type { KeyboardEvent, MouseEvent } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: Workspace | null;
|
||||
}
|
||||
|
||||
export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => {
|
||||
export const ProfilePanel = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
useEffect(() => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready;
|
||||
|
||||
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
|
||||
export interface WorkspaceSettingDetailProps {
|
||||
isOwner: boolean;
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}
|
||||
|
||||
+16
-18
@@ -1,13 +1,11 @@
|
||||
import { Button, IconButton, Menu } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter';
|
||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema';
|
||||
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/properties/services/schema';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteIcon, FilterIcon, MoreHorizontalIcon } from '@blocksuite/icons';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { FrameworkScope, type WorkspaceMetadata } from '@toeverything/infra';
|
||||
import type { MouseEvent } from 'react';
|
||||
import {
|
||||
createContext,
|
||||
@@ -19,6 +17,8 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useCurrentWorkspacePropertiesAdapter } from '../../../../../hooks/use-affine-adapter';
|
||||
import { useWorkspace } from '../../../../../hooks/use-workspace';
|
||||
import type { PagePropertyIcon } from '../../../page-properties';
|
||||
import {
|
||||
nameToIcon,
|
||||
@@ -37,11 +37,11 @@ import * as styles from './styles.css';
|
||||
// @ts-expect-error this should always be set
|
||||
const managerContext = createContext<PagePropertiesMetaManager>();
|
||||
|
||||
const usePagePropertiesMetaManager = (workspace: Workspace) => {
|
||||
const usePagePropertiesMetaManager = () => {
|
||||
// the workspace properties adapter adapter is reactive,
|
||||
// which means it's reference will change when any of the properties change
|
||||
// also it will trigger a re-render of the component
|
||||
const adapter = useWorkspacePropertiesAdapter(workspace);
|
||||
const adapter = useCurrentWorkspacePropertiesAdapter();
|
||||
const manager = useMemo(() => {
|
||||
return new PagePropertiesMetaManager(adapter);
|
||||
}, [adapter]);
|
||||
@@ -372,12 +372,8 @@ const WorkspaceSettingPropertiesMain = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceSettingPropertiesInner = ({
|
||||
workspace,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
}) => {
|
||||
const manager = usePagePropertiesMetaManager(workspace);
|
||||
const WorkspaceSettingPropertiesInner = () => {
|
||||
const manager = usePagePropertiesMetaManager();
|
||||
return (
|
||||
<managerContext.Provider value={manager}>
|
||||
<WorkspaceSettingPropertiesMain />
|
||||
@@ -393,10 +389,14 @@ export const WorkspaceSettingProperties = ({
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useWorkspace(workspaceMetadata);
|
||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||
const title = workspaceInfo.name || 'untitled';
|
||||
const title = workspaceInfo?.name || 'untitled';
|
||||
|
||||
if (workspace === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<SettingHeader
|
||||
title={t['com.affine.settings.workspace.properties.header.title']()}
|
||||
subtitle={
|
||||
@@ -410,9 +410,7 @@ export const WorkspaceSettingProperties = ({
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{workspace ? (
|
||||
<WorkspaceSettingPropertiesInner workspace={workspace} />
|
||||
) : null}
|
||||
</>
|
||||
<WorkspaceSettingPropertiesInner />
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { ExportMenuItems } from '@affine/core/components/page-list';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { LinkIcon } from '@blocksuite/icons';
|
||||
import { Doc, useLiveData, useService } from '@toeverything/infra';
|
||||
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { useExportPage } from '../../../../hooks/affine/use-export-page';
|
||||
import * as styles from './index.css';
|
||||
@@ -16,7 +16,7 @@ export const ShareExport = ({
|
||||
currentPage,
|
||||
}: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const page = useService(Doc);
|
||||
const doc = useService(DocService).doc;
|
||||
const workspaceId = workspace.id;
|
||||
const pageId = currentPage.id;
|
||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||
@@ -25,7 +25,7 @@ export const ShareExport = ({
|
||||
urlType: 'workspace',
|
||||
});
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
const currentMode = useLiveData(page.mode$);
|
||||
const currentMode = useLiveData(doc.mode$);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+1
-9
@@ -8,7 +8,6 @@ import type { Doc } from '@blocksuite/store';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||
import * as styles from './index.css';
|
||||
import { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
@@ -65,11 +64,6 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
|
||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const {
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -87,9 +81,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
data-testid="cloud-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.sharedButton']()
|
||||
: t['com.affine.share-menu.shareButton']()}
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
+155
-51
@@ -1,21 +1,34 @@
|
||||
import {
|
||||
Input,
|
||||
notify,
|
||||
RadioButton,
|
||||
RadioButtonGroup,
|
||||
Skeleton,
|
||||
Switch,
|
||||
} from '@affine/component';
|
||||
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||
import { useIsSharedPage } from '@affine/core/hooks/affine/use-is-shared-page';
|
||||
import { useServerBaseUrl } from '@affine/core/hooks/affine/use-server-config';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { ShareService } from '@affine/core/modules/share-doc';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import { Doc, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
SingleSelectSelectSolidIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { ServerConfigService } from '../../../../modules/cloud';
|
||||
import { CloudSvg } from '../cloud-svg';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
@@ -50,63 +63,156 @@ export const LocalSharePage = (props: ShareMenuProps) => {
|
||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
const {
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const pageId = currentPage.id;
|
||||
const page = useService(Doc);
|
||||
const doc = useService(DocService).doc;
|
||||
const shareService = useService(ShareService);
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
useEffect(() => {
|
||||
shareService.share.revalidate();
|
||||
}, [shareService]);
|
||||
const isSharedPage = useLiveData(shareService.share.isShared$);
|
||||
const sharedMode = useLiveData(shareService.share.sharedMode$);
|
||||
const baseUrl = useLiveData(serverConfig.config$.map(c => c?.baseUrl));
|
||||
const isLoading =
|
||||
isSharedPage === null || sharedMode === null || baseUrl === null;
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
const {
|
||||
isSharedPage,
|
||||
enableShare,
|
||||
changeShare,
|
||||
currentShareMode,
|
||||
disableShare,
|
||||
} = useIsSharedPage(workspaceId, currentPage.id);
|
||||
|
||||
const currentPageMode = useLiveData(page.mode$);
|
||||
const currentDocMode = useLiveData(doc.mode$);
|
||||
|
||||
const defaultMode = useMemo(() => {
|
||||
if (isSharedPage) {
|
||||
const mode = useMemo(() => {
|
||||
if (isSharedPage && sharedMode) {
|
||||
// if it's a shared page, use the share mode
|
||||
return currentShareMode;
|
||||
return sharedMode.toLowerCase() as DocMode;
|
||||
}
|
||||
// default to page mode
|
||||
return currentPageMode;
|
||||
}, [currentPageMode, currentShareMode, isSharedPage]);
|
||||
const [mode, setMode] = useState<PageMode>(defaultMode);
|
||||
return currentDocMode;
|
||||
}, [currentDocMode, isSharedPage, sharedMode]);
|
||||
|
||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
pageId: doc.id,
|
||||
urlType: 'share',
|
||||
});
|
||||
const baseUrl = useServerBaseUrl();
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickCreateLink = useCallback(() => {
|
||||
enableShare(mode);
|
||||
if (sharingUrl) {
|
||||
navigator.clipboard.writeText(sharingUrl).catch(err => {
|
||||
console.error(err);
|
||||
const onClickCreateLink = useAsyncCallback(async () => {
|
||||
try {
|
||||
await shareService.share.enableShare(
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||
);
|
||||
notify.success({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.create-public-link.notification.success.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.create-public-link.notification.success.message'
|
||||
](),
|
||||
style: 'normal',
|
||||
icon: <SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />,
|
||||
});
|
||||
if (sharingUrl) {
|
||||
navigator.clipboard.writeText(sharingUrl).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
notify.error({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
|
||||
](),
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
}, [enableShare, mode, sharingUrl]);
|
||||
}, [mode, shareService.share, sharingUrl, t]);
|
||||
|
||||
const onDisablePublic = useCallback(() => {
|
||||
disableShare();
|
||||
const onDisablePublic = useAsyncCallback(async () => {
|
||||
try {
|
||||
await shareService.share.disableShare();
|
||||
notify.error({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.disable-publish-link.notification.success.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.disable-publish-link.notification.success.message'
|
||||
](),
|
||||
});
|
||||
} catch (err) {
|
||||
notify.error({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.disable-publish-link.notification.fail.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.disable-publish-link.notification.fail.message'
|
||||
](),
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
setShowDisable(false);
|
||||
}, [disableShare]);
|
||||
}, [shareService, t]);
|
||||
|
||||
const onShareModeChange = useCallback(
|
||||
(value: PageMode) => {
|
||||
setMode(value);
|
||||
if (isSharedPage) {
|
||||
changeShare(value);
|
||||
const onShareModeChange = useAsyncCallback(
|
||||
async (value: DocMode) => {
|
||||
try {
|
||||
if (isSharedPage) {
|
||||
await shareService.share.changeShare(
|
||||
value === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||
);
|
||||
notify.success({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.success.title'
|
||||
](),
|
||||
message: t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.success.message'
|
||||
]({
|
||||
preMode: value === 'edgeless' ? t['Page']() : t['Edgeless'](),
|
||||
currentMode: value === 'edgeless' ? t['Edgeless']() : t['Page'](),
|
||||
}),
|
||||
style: 'normal',
|
||||
icon: (
|
||||
<SingleSelectSelectSolidIcon color={cssVar('primaryColor')} />
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
notify.error({
|
||||
title:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.title'
|
||||
](),
|
||||
message:
|
||||
t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
|
||||
](),
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
[changeShare, isSharedPage]
|
||||
[isSharedPage, shareService.share, t]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
// TODO: loading and error UI
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={100} />
|
||||
<Skeleton height={40} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
@@ -124,15 +230,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
value={
|
||||
(isSharedPage && sharingUrl) ||
|
||||
`${
|
||||
baseUrl ||
|
||||
`${location.protocol}${
|
||||
location.port ? `:${location.port}` : ''
|
||||
}//${location.hostname}`
|
||||
}/...`
|
||||
}
|
||||
value={(isSharedPage && sharingUrl) || `${baseUrl}/...`}
|
||||
readOnly
|
||||
/>
|
||||
{isSharedPage ? (
|
||||
@@ -162,7 +260,6 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
<div>
|
||||
<RadioButtonGroup
|
||||
className={styles.radioButtonGroup}
|
||||
defaultValue={defaultMode}
|
||||
value={mode}
|
||||
onValueChange={onShareModeChange}
|
||||
>
|
||||
@@ -236,7 +333,14 @@ export const SharePage = (props: ShareMenuProps) => {
|
||||
} else if (
|
||||
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) {
|
||||
return <AffineSharePage {...props} />;
|
||||
return (
|
||||
// TODO: refactor this part
|
||||
<ErrorBoundary fallback={null}>
|
||||
<Suspense>
|
||||
<AffineSharePage {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useServerBaseUrl } from '@affine/core/hooks/affine/use-server-config';
|
||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -16,7 +16,7 @@ const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => {
|
||||
// to generate a public url like https://app.affine.app/share/123/456
|
||||
// or https://app.affine.app/share/123/456?mode=edgeless
|
||||
|
||||
const baseUrl = useServerBaseUrl();
|
||||
const baseUrl = getAffineCloudBaseUrl();
|
||||
|
||||
const url = useMemo(() => {
|
||||
// baseUrl is null when running in electron and without network
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { ResizePanel } from '@affine/component/resize-panel';
|
||||
import { useServiceOptional, Workspace } from '@toeverything/infra';
|
||||
import { useServiceOptional, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
@@ -121,7 +121,7 @@ export function AppSidebar({
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
const width = useAtomValue(appSidebarWidthAtom);
|
||||
|
||||
const currentWorkspace = useServiceOptional(Workspace);
|
||||
const currentWorkspace = useServiceOptional(WorkspaceService);
|
||||
return (
|
||||
<div
|
||||
style={{ width }}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@blocksuite/presets';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import { Slot } from '@blocksuite/store';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import type { DocMode } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
@@ -40,7 +40,7 @@ function forwardSlot<T extends Record<string, Slot<any>>>(
|
||||
|
||||
interface BlocksuiteEditorContainerProps {
|
||||
page: Doc;
|
||||
mode: PageMode;
|
||||
mode: DocMode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
defaultSelectedBlockId?: string;
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { toast } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export interface FavoriteButtonProps {
|
||||
@@ -12,7 +12,7 @@ export interface FavoriteButtonProps {
|
||||
|
||||
export const useFavorite = (pageId: string) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const currentPage = docCollection.getDoc(pageId);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
|
||||
+16
-20
@@ -11,10 +11,8 @@ import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
DuplicateIcon,
|
||||
EdgelessIcon,
|
||||
@@ -25,7 +23,12 @@ import {
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { Doc, useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -46,16 +49,12 @@ export const PageHeaderMenuButton = ({
|
||||
}: PageMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const currentPage = docCollection.getDoc(pageId);
|
||||
assertExists(currentPage);
|
||||
|
||||
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const page = useService(Doc);
|
||||
const currentMode = useLiveData(page.mode$);
|
||||
const doc = useService(DocService).doc;
|
||||
const isInTrash = useLiveData(doc.meta$.map(m => m.trash));
|
||||
const currentMode = useLiveData(doc.mode$);
|
||||
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
@@ -74,30 +73,27 @@ export const PageHeaderMenuButton = ({
|
||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||
|
||||
const handleOpenTrashModal = useCallback(() => {
|
||||
if (!pageMeta) {
|
||||
return;
|
||||
}
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [pageId],
|
||||
pageTitles: [pageMeta.title],
|
||||
pageTitles: [doc.meta$.value.title ?? ''],
|
||||
});
|
||||
}, [pageId, pageMeta, setTrashModal]);
|
||||
}, [doc.meta$.value.title, pageId, setTrashModal]);
|
||||
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
page.toggleMode();
|
||||
doc.toggleMode();
|
||||
toast(
|
||||
currentMode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
: t['com.affine.toastMessage.pageMode']()
|
||||
);
|
||||
}, [currentMode, page, t]);
|
||||
}, [currentMode, doc, t]);
|
||||
const menuItemStyle = {
|
||||
padding: '4px 12px',
|
||||
transition: 'all 0.3s',
|
||||
};
|
||||
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
const exportHandler = useExportPage(doc.blockSuiteDoc);
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
duplicate(pageId);
|
||||
@@ -212,7 +208,7 @@ export const PageHeaderMenuButton = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
if (pageMeta?.trash) {
|
||||
if (isInTrash) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
+17
-13
@@ -1,8 +1,12 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import { Doc, useLiveData, useService } from '@toeverything/infra';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
@@ -17,7 +21,7 @@ export type EditorModeSwitchProps = {
|
||||
pageId: string;
|
||||
style?: CSSProperties;
|
||||
isPublic?: boolean;
|
||||
publicMode?: PageMode;
|
||||
publicMode?: DocMode;
|
||||
};
|
||||
const TooltipContent = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -42,9 +46,9 @@ export const EditorModeSwitch = ({
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const trash = pageMeta?.trash ?? false;
|
||||
const page = useService(Doc);
|
||||
const doc = useService(DocService).doc;
|
||||
|
||||
const currentMode = useLiveData(page.mode$);
|
||||
const currentMode = useLiveData(doc.mode$);
|
||||
|
||||
useEffect(() => {
|
||||
if (trash || isPublic) {
|
||||
@@ -53,7 +57,7 @@ export const EditorModeSwitch = ({
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if (e.code === 'KeyS' && e.altKey) {
|
||||
e.preventDefault();
|
||||
page.toggleMode();
|
||||
doc.toggleMode();
|
||||
toast(
|
||||
currentMode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
@@ -64,7 +68,7 @@ export const EditorModeSwitch = ({
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [currentMode, isPublic, page, pageId, t, trash]);
|
||||
}, [currentMode, isPublic, doc, pageId, t, trash]);
|
||||
|
||||
const onSwitchToPageMode = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
@@ -73,9 +77,9 @@ export const EditorModeSwitch = ({
|
||||
if (currentMode === 'page' || isPublic) {
|
||||
return;
|
||||
}
|
||||
page.setMode('page');
|
||||
doc.setMode('page');
|
||||
toast(t['com.affine.toastMessage.pageMode']());
|
||||
}, [currentMode, isPublic, page, t]);
|
||||
}, [currentMode, isPublic, doc, t]);
|
||||
|
||||
const onSwitchToEdgelessMode = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
@@ -84,18 +88,18 @@ export const EditorModeSwitch = ({
|
||||
if (currentMode === 'edgeless' || isPublic) {
|
||||
return;
|
||||
}
|
||||
page.setMode('edgeless');
|
||||
doc.setMode('edgeless');
|
||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
||||
}, [currentMode, isPublic, page, t]);
|
||||
}, [currentMode, isPublic, doc, t]);
|
||||
|
||||
const shouldHide = useCallback(
|
||||
(mode: PageMode) =>
|
||||
(mode: DocMode) =>
|
||||
(trash && currentMode !== mode) || (isPublic && publicMode !== mode),
|
||||
[currentMode, isPublic, publicMode, trash]
|
||||
);
|
||||
|
||||
const shouldActive = useCallback(
|
||||
(mode: PageMode) => (isPublic ? false : currentMode === mode),
|
||||
(mode: DocMode) => (isPublic ? false : currentMode === mode),
|
||||
[currentMode, isPublic]
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import { initEmptyPage, PageRecordList, useService } from '@toeverything/infra';
|
||||
import { DocsService, initEmptyPage, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
@@ -13,23 +13,23 @@ export const usePageHelper = (docCollection: DocCollection) => {
|
||||
const { openPage, jumpToSubPath } = useNavigateHelper();
|
||||
const { createDoc } = useDocCollectionHelper(docCollection);
|
||||
const { setDocMeta } = useDocMetaHelper(docCollection);
|
||||
const pageRecordList = useService(PageRecordList);
|
||||
const docRecordList = useService(DocsService).list;
|
||||
|
||||
const isPreferredEdgeless = useCallback(
|
||||
(pageId: string) =>
|
||||
pageRecordList.record$(pageId).value?.mode$.value === 'edgeless',
|
||||
[pageRecordList]
|
||||
docRecordList.doc$(pageId).value?.mode$.value === 'edgeless',
|
||||
[docRecordList]
|
||||
);
|
||||
|
||||
const createPageAndOpen = useCallback(
|
||||
(mode?: 'page' | 'edgeless') => {
|
||||
const page = createDoc();
|
||||
initEmptyPage(page);
|
||||
pageRecordList.record$(page.id).value?.setMode(mode || 'page');
|
||||
docRecordList.doc$(page.id).value?.setMode(mode || 'page');
|
||||
openPage(docCollection.id, page.id);
|
||||
return page;
|
||||
},
|
||||
[docCollection.id, createDoc, openPage, pageRecordList]
|
||||
[docCollection.id, createDoc, openPage, docRecordList]
|
||||
);
|
||||
|
||||
const createEdgelessAndOpen = useCallback(() => {
|
||||
|
||||
+8
-6
@@ -1,7 +1,11 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService, WorkspaceManager } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { ShareHeaderRightItemProps } from './index';
|
||||
@@ -13,11 +17,9 @@ export const AuthenticatedItem = ({
|
||||
}: { setIsMember: (value: boolean) => void } & ShareHeaderRightItemProps) => {
|
||||
const { workspaceId, pageId } = props;
|
||||
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspaceList = useLiveData(workspaceManager.list.workspaceList$);
|
||||
const isMember = workspaceList?.some(
|
||||
workspace => workspace.id === workspaceId
|
||||
);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const isMember = workspaces?.some(workspace => workspace.id === workspaceId);
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { type DocMode, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { AuthenticatedItem } from './authenticated-item';
|
||||
import { PresentButton } from './present';
|
||||
import * as styles from './styles.css';
|
||||
@@ -10,11 +10,11 @@ import { PublishPageUserAvatar } from './user-avatar';
|
||||
export type ShareHeaderRightItemProps = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
publishMode: PageMode;
|
||||
publishMode: DocMode;
|
||||
};
|
||||
|
||||
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const { publishMode } = props;
|
||||
const [isMember, setIsMember] = useState(false);
|
||||
|
||||
|
||||
+30
-19
@@ -5,37 +5,44 @@ import {
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
} from '@affine/component/ui/menu';
|
||||
import { useCurrentUser } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||
import { signOutCloud } from '@affine/core/utils/cloud-utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SignOutIcon } from '@blocksuite/icons';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { AuthService, SubscriptionService } from '../../../modules/cloud';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const UserInfo = () => {
|
||||
const user = useCurrentUser();
|
||||
const [subscription] = useUserSubscription();
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const authService = useService(AuthService);
|
||||
const user = useLiveData(authService.session.account$);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
useEffect(() => {
|
||||
subscription.revalidate();
|
||||
}, [subscription]);
|
||||
const primary = useLiveData(subscription.primary$);
|
||||
const plan = primary?.plan;
|
||||
|
||||
if (!user) {
|
||||
// TODO: loading UI
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.accountCard}>
|
||||
<Avatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.avatarUrl}
|
||||
name={user.label}
|
||||
url={user.avatar}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.userName} title={user.name}>
|
||||
{user.name}
|
||||
<div className={styles.userName} title={user.label}>
|
||||
{user.label}
|
||||
</div>
|
||||
<div className={styles.userPlanButton}>{plan}</div>
|
||||
{plan && <div className={styles.userPlanButton}>{plan}</div>}
|
||||
</div>
|
||||
<div className={styles.userEmail} title={user.email}>
|
||||
{user.email}
|
||||
@@ -46,13 +53,13 @@ const UserInfo = () => {
|
||||
};
|
||||
|
||||
export const PublishPageUserAvatar = () => {
|
||||
const user = useCurrentUser();
|
||||
const authService = useService(AuthService);
|
||||
const user = useLiveData(authService.session.account$);
|
||||
const t = useAFFiNEI18N();
|
||||
const location = useLocation();
|
||||
|
||||
const handleSignOut = useAsyncCallback(async () => {
|
||||
await signOutCloud(location.pathname);
|
||||
}, [location.pathname]);
|
||||
await authService.signOut();
|
||||
}, [authService]);
|
||||
|
||||
const menuItem = useMemo(() => {
|
||||
return (
|
||||
@@ -74,6 +81,10 @@ export const PublishPageUserAvatar = () => {
|
||||
);
|
||||
}, [handleSignOut, t]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={menuItem}
|
||||
@@ -84,7 +95,7 @@ export const PublishPageUserAvatar = () => {
|
||||
}}
|
||||
>
|
||||
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
|
||||
<Avatar size={24} url={user.avatarUrl} name={user.name} />
|
||||
<Avatar size={24} url={user.avatar} name={user.label} />
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspa
|
||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import {
|
||||
Doc,
|
||||
type DocMode,
|
||||
DocService,
|
||||
fontStyleOptions,
|
||||
useLiveData,
|
||||
useService,
|
||||
@@ -32,7 +32,7 @@ export type OnLoadEditor = (
|
||||
|
||||
export interface PageDetailEditorProps {
|
||||
isPublic?: boolean;
|
||||
publishMode?: PageMode;
|
||||
publishMode?: DocMode;
|
||||
docCollection: DocCollection;
|
||||
pageId: string;
|
||||
onLoad?: OnLoadEditor;
|
||||
@@ -48,7 +48,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
isPublic,
|
||||
publishMode,
|
||||
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
|
||||
const currentMode = useLiveData(useService(Doc).mode$);
|
||||
const currentMode = useLiveData(useService(DocService).doc.mode$);
|
||||
const mode = useMemo(() => {
|
||||
const shareMode = publishMode || currentMode;
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
@@ -63,7 +63,7 @@ export const VirtualizedCollectionList = ({
|
||||
[]
|
||||
);
|
||||
const collectionService = useService(CollectionService);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const info = useDeleteCollectionInfo();
|
||||
|
||||
const collectionOperations = useCollectionOperationsRenderer({
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
SearchIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@@ -37,7 +37,7 @@ import { PageListNewPageButton } from './page-list-new-page-button';
|
||||
|
||||
export const PageListHeader = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { importFile, createEdgeless, createPage } = usePageHelper(
|
||||
workspace.docCollection
|
||||
);
|
||||
@@ -85,12 +85,12 @@ export const CollectionPageListHeader = ({
|
||||
collectionService.updateCollection(collection.id, () => ret);
|
||||
}, [collection, collectionService, open]);
|
||||
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const createAndAddDocument = useCallback(
|
||||
(createDocumentFn: () => Doc) => {
|
||||
(createDocumentFn: () => BlockSuiteDoc) => {
|
||||
const newDoc = createDocumentFn();
|
||||
collectionService.addPageToCollection(collection.id, newDoc.id);
|
||||
},
|
||||
@@ -98,7 +98,7 @@ export const CollectionPageListHeader = ({
|
||||
);
|
||||
|
||||
const onConfirmAddDocument = useCallback(
|
||||
(createDocumentFn: () => Doc) => {
|
||||
(createDocumentFn: () => BlockSuiteDoc) => {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.collection.add-doc.confirm.title'](),
|
||||
description: t['com.affine.collection.add-doc.confirm.description'](),
|
||||
@@ -248,9 +248,9 @@ interface SwitchTagProps {
|
||||
export const SwitchTag = ({ onClick }: SwitchTagProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const tagService = useService(TagService);
|
||||
const tagList = useService(TagService).tagList;
|
||||
const filteredTags = useLiveData(
|
||||
inputValue ? tagService.filterTagsByName$(inputValue) : tagService.tags$
|
||||
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
|
||||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
|
||||
@@ -73,8 +73,8 @@ const PageSelectionCell = ({
|
||||
};
|
||||
|
||||
export const PageTagsCell = ({ pageId }: Pick<PageListItemProps, 'pageId'>) => {
|
||||
const tagsService = useService(TagService);
|
||||
const tags = useLiveData(tagsService.tagsByPageId$(pageId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tagsByPageId$(pageId));
|
||||
|
||||
return (
|
||||
<div data-testid="page-list-item-tags" className={styles.tagsCell}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
@@ -70,13 +70,13 @@ export const VirtualizedPageList = ({
|
||||
const listRef = useRef<ItemListHandle>(null);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const pageOperations = usePageOperationsRenderer();
|
||||
const { isPreferredEdgeless } = usePageHelper(currentWorkspace.docCollection);
|
||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||
|
||||
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
filters,
|
||||
collection,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
@@ -127,8 +127,8 @@ const GroupTagLabel = ({ tag, count }: { tag: Tag; count: number }) => {
|
||||
);
|
||||
};
|
||||
export const useTagGroupDefinitions = (): ItemGroupDefinition<ListItem>[] => {
|
||||
const tagService = useService(TagService);
|
||||
const tags = useLiveData(tagService.tags$);
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
return useMemo(() => {
|
||||
return tags.map(tag => ({
|
||||
id: tag.id,
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
import { Workbench } from '@affine/core/modules/workbench';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -58,13 +58,13 @@ export const PageOperationCell = ({
|
||||
onRemoveFromAllowList,
|
||||
}: PageOperationCellProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||
const workbench = useService(Workbench);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
||||
|
||||
const onDisablePublicSharing = useCallback(() => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@blocksuite/icons';
|
||||
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
@@ -273,12 +273,8 @@ function tagIdToTagOption(
|
||||
}
|
||||
|
||||
const PageTitle = ({ id }: { id: string }) => {
|
||||
const page = useLiveData(
|
||||
useService(PageRecordList).records$.map(record => {
|
||||
return record.find(p => p.id === id);
|
||||
})
|
||||
);
|
||||
const title = useLiveData(page?.title$);
|
||||
const doc = useLiveData(useService(DocsService).list.doc$(id));
|
||||
const title = useLiveData(doc?.title$);
|
||||
const t = useAFFiNEI18N();
|
||||
return title || t['Untitled']();
|
||||
};
|
||||
|
||||
@@ -32,9 +32,9 @@ export const CreateOrEditTag = ({
|
||||
onOpenChange: (open: boolean) => void;
|
||||
tagMeta?: TagMeta;
|
||||
}) => {
|
||||
const tagService = useService(TagService);
|
||||
const tagOptions = useLiveData(tagService.tagMetas$);
|
||||
const tag = useLiveData(tagService.tagByTagId$(tagMeta?.id));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tagOptions = useLiveData(tagList.tagMetas$);
|
||||
const tag = useLiveData(tagList.tagByTagId$(tagMeta?.id));
|
||||
const t = useAFFiNEI18N();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
@@ -97,7 +97,7 @@ export const CreateOrEditTag = ({
|
||||
return toast(t['com.affine.tags.create-tag.toast.exist']());
|
||||
}
|
||||
if (!tagMeta) {
|
||||
tagService.createTag(tagName.trim(), tagIcon);
|
||||
tagList.createTag(tagName.trim(), tagIcon);
|
||||
toast(t['com.affine.tags.create-tag.toast.success']());
|
||||
onClose();
|
||||
return;
|
||||
@@ -108,7 +108,7 @@ export const CreateOrEditTag = ({
|
||||
toast(t['com.affine.tags.edit-tag.toast.success']());
|
||||
onClose();
|
||||
return;
|
||||
}, [onClose, t, tag, tagIcon, tagMeta, tagName, tagOptions, tagService]);
|
||||
}, [onClose, t, tag, tagIcon, tagMeta, tagName, tagOptions, tagList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||
@@ -26,7 +26,7 @@ export const VirtualizedTagList = ({
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [showCreateTagInput, setShowCreateTagInput] = useState(false);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const tagOperations = useCallback(
|
||||
(tag: TagMeta) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
@@ -30,7 +30,7 @@ export const useAllDocDisplayProperties = (): [
|
||||
value: PageGroupByType | PageDisplayProperties
|
||||
) => void,
|
||||
] => {
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const [properties, setProperties] = useAtom(displayPropertiesAtom);
|
||||
|
||||
const workspaceProperties = properties[workspace.id] || defaultProps;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { usePublicPages } from '../../hooks/affine/use-is-shared-page';
|
||||
import { filterPage, filterPageByRules } from './use-collection-manager';
|
||||
|
||||
export const useFilteredPageMetas = (
|
||||
workspace: Workspace,
|
||||
pageMetas: DocMeta[],
|
||||
options: {
|
||||
trash?: boolean;
|
||||
@@ -16,7 +16,26 @@ export const useFilteredPageMetas = (
|
||||
collection?: Collection;
|
||||
} = {}
|
||||
) => {
|
||||
const { getPublicMode } = usePublicPages(workspace);
|
||||
const shareDocsService = useService(ShareDocsService);
|
||||
const shareDocs = useLiveData(shareDocsService.shareDocs.list$);
|
||||
|
||||
const getPublicMode = useCallback(
|
||||
(id: string) => {
|
||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
||||
return mode
|
||||
? mode === PublicPageMode.Edgeless
|
||||
? ('edgeless' as const)
|
||||
: ('page' as const)
|
||||
: undefined;
|
||||
},
|
||||
[shareDocs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: loading & error UI
|
||||
shareDocsService.shareDocs.revalidate();
|
||||
}, [shareDocsService]);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { MenuItemProps } from '@affine/component';
|
||||
import { Menu, MenuIcon, MenuItem } from '@affine/component';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||
import { Workbench } from '@affine/core/modules/workbench';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ export const CollectionOperations = ({
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const service = useService(CollectionService);
|
||||
const workbench = useService(Workbench);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const { open: openEditCollectionModal, node: editModal } =
|
||||
useEditCollection(config);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Button, Menu } from '@affine/component';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
useBlockSuiteDocMeta,
|
||||
useDocMetaHelper,
|
||||
} from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
@@ -14,17 +11,20 @@ import {
|
||||
TodayIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import type { AffineCommand, CommandCategory } from '@toeverything/infra';
|
||||
import type {
|
||||
AffineCommand,
|
||||
CommandCategory,
|
||||
DocRecord,
|
||||
Workspace,
|
||||
} from '@toeverything/infra';
|
||||
import {
|
||||
AffineCommandRegistry,
|
||||
Doc,
|
||||
PageRecordList,
|
||||
DocsService,
|
||||
GlobalContextService,
|
||||
PreconditionStrategy,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
Workspace,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -51,13 +51,13 @@ function filterCommandByContext(
|
||||
return true;
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InEdgeless) {
|
||||
return context.pageMode === 'edgeless';
|
||||
return context.docMode === 'edgeless';
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InPaper) {
|
||||
return context.pageMode === 'page';
|
||||
return context.docMode === 'page';
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
|
||||
return !!context.pageMode;
|
||||
return !!context.docMode;
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.Never) {
|
||||
return false;
|
||||
@@ -75,28 +75,22 @@ function getAllCommand(context: CommandContext) {
|
||||
});
|
||||
}
|
||||
|
||||
const useWorkspacePages = () => {
|
||||
const workspace = useService(Workspace);
|
||||
const pages = useBlockSuiteDocMeta(workspace.docCollection);
|
||||
return pages;
|
||||
};
|
||||
|
||||
const useRecentPages = () => {
|
||||
const pages = useWorkspacePages();
|
||||
const useRecentDocs = () => {
|
||||
const docs = useLiveData(useService(DocsService).list.docs$);
|
||||
const recentPageIds = useAtomValue(recentPageIdsBaseAtom);
|
||||
return useMemo(() => {
|
||||
return recentPageIds
|
||||
.map(pageId => {
|
||||
const page = pages.find(page => page.id === pageId);
|
||||
const page = docs.find(page => page.id === pageId);
|
||||
return page;
|
||||
})
|
||||
.filter((p): p is DocMeta => !!p);
|
||||
}, [recentPageIds, pages]);
|
||||
.filter((p): p is DocRecord => !!p);
|
||||
}, [recentPageIds, docs]);
|
||||
};
|
||||
|
||||
export const pageToCommand = (
|
||||
export const docToCommand = (
|
||||
category: CommandCategory,
|
||||
page: DocMeta,
|
||||
doc: DocRecord,
|
||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||
getPageTitle: ReturnType<typeof useGetDocCollectionPageTitle>,
|
||||
isPageJournal: (pageId: string) => boolean,
|
||||
@@ -105,10 +99,9 @@ export const pageToCommand = (
|
||||
subTitle?: string,
|
||||
blockId?: string
|
||||
): CMDKCommand => {
|
||||
const pageMode = workspace.services.get(PageRecordList).record$(page.id).value
|
||||
?.mode$.value;
|
||||
const docMode = doc.mode$.value;
|
||||
|
||||
const title = getPageTitle(page.id) || t['Untitled']();
|
||||
const title = getPageTitle(doc.id) || t['Untitled']();
|
||||
const commandLabel = {
|
||||
title: title,
|
||||
subTitle: subTitle,
|
||||
@@ -116,11 +109,11 @@ export const pageToCommand = (
|
||||
|
||||
// hack: when comparing, the part between >>> and <<< will be ignored
|
||||
// adding this patch so that CMDK will not complain about duplicated commands
|
||||
const id = category + '.' + page.id;
|
||||
const id = category + '.' + doc.id;
|
||||
|
||||
const icon = isPageJournal(page.id) ? (
|
||||
const icon = isPageJournal(doc.id) ? (
|
||||
<TodayIcon />
|
||||
) : pageMode === 'edgeless' ? (
|
||||
) : docMode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PageIcon />
|
||||
@@ -136,19 +129,19 @@ export const pageToCommand = (
|
||||
return;
|
||||
}
|
||||
if (blockId) {
|
||||
return navigationHelper.jumpToPageBlock(workspace.id, page.id, blockId);
|
||||
return navigationHelper.jumpToPageBlock(workspace.id, doc.id, blockId);
|
||||
}
|
||||
return navigationHelper.jumpToPage(workspace.id, page.id);
|
||||
return navigationHelper.jumpToPage(workspace.id, doc.id);
|
||||
},
|
||||
icon: icon,
|
||||
timestamp: page.updatedDate,
|
||||
timestamp: doc.meta?.updatedDate,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePageCommands = () => {
|
||||
const recentPages = useRecentPages();
|
||||
const pages = useWorkspacePages();
|
||||
const workspace = useService(Workspace);
|
||||
const recentDocs = useRecentDocs();
|
||||
const docs = useLiveData(useService(DocsService).list.docs$);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const pageHelper = usePageHelper(workspace.docCollection);
|
||||
const pageMetaHelper = useDocMetaHelper(workspace.docCollection);
|
||||
const query = useAtomValue(cmdkQueryAtom);
|
||||
@@ -179,10 +172,10 @@ export const usePageCommands = () => {
|
||||
|
||||
let results: CMDKCommand[] = [];
|
||||
if (query.trim() === '') {
|
||||
results = recentPages.map(page => {
|
||||
return pageToCommand(
|
||||
results = recentDocs.map(doc => {
|
||||
return docToCommand(
|
||||
'affine:recent',
|
||||
page,
|
||||
doc,
|
||||
navigationHelper,
|
||||
getPageTitle,
|
||||
isPageJournal,
|
||||
@@ -203,18 +196,18 @@ export const usePageCommands = () => {
|
||||
reverseMapping.set(value.space, key);
|
||||
});
|
||||
|
||||
results = pages.map(page => {
|
||||
results = docs.map(doc => {
|
||||
const category = 'affine:pages';
|
||||
|
||||
const subTitle = resultValues.find(
|
||||
result => result.space === page.id
|
||||
result => result.space === doc.id
|
||||
)?.content;
|
||||
|
||||
const blockId = reverseMapping.get(page.id);
|
||||
const blockId = reverseMapping.get(doc.id);
|
||||
|
||||
const command = pageToCommand(
|
||||
const command = docToCommand(
|
||||
category,
|
||||
page,
|
||||
doc,
|
||||
navigationHelper,
|
||||
getPageTitle,
|
||||
isPageJournal,
|
||||
@@ -281,13 +274,13 @@ export const usePageCommands = () => {
|
||||
}, [
|
||||
searchTime,
|
||||
query,
|
||||
recentPages,
|
||||
recentDocs,
|
||||
navigationHelper,
|
||||
getPageTitle,
|
||||
isPageJournal,
|
||||
t,
|
||||
workspace,
|
||||
pages,
|
||||
docs,
|
||||
journalHelper,
|
||||
pageHelper,
|
||||
pageMetaHelper,
|
||||
@@ -322,7 +315,7 @@ export const useCollectionsCommands = () => {
|
||||
const query = useAtomValue(cmdkQueryAtom);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const selectCollection = useCallback(
|
||||
(id: string) => {
|
||||
navigationHelper.jumpToCollection(workspace.id, id);
|
||||
@@ -353,13 +346,14 @@ export const useCMDKCommandGroups = () => {
|
||||
const pageCommands = usePageCommands();
|
||||
const collectionCommands = useCollectionsCommands();
|
||||
|
||||
const currentPage = useServiceOptional(Doc);
|
||||
const currentPageMode = useLiveData(currentPage?.mode$);
|
||||
const currentDocMode =
|
||||
useLiveData(useService(GlobalContextService).globalContext.docMode.$) ??
|
||||
undefined;
|
||||
const affineCommands = useMemo(() => {
|
||||
return getAllCommand({
|
||||
pageMode: currentPageMode,
|
||||
docMode: currentDocMode,
|
||||
});
|
||||
}, [currentPageMode]);
|
||||
}, [currentDocMode]);
|
||||
const query = useAtomValue(cmdkQueryAtom).trim();
|
||||
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CommandCategory } from '@toeverything/infra';
|
||||
import type { CommandCategory, DocMode } from '@toeverything/infra';
|
||||
|
||||
export interface CommandContext {
|
||||
pageMode: 'page' | 'edgeless' | undefined;
|
||||
docMode: DocMode | undefined;
|
||||
}
|
||||
|
||||
// similar to AffineCommand, but for rendering into the UI
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties, FC } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { signInCloud } from '../../../utils/cloud-utils';
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
|
||||
export const Footer: FC = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
|
||||
// const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
{loginStatus === 'authenticated' ? null : <SignInButton />}
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
const SignInButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={useCallback(() => {
|
||||
signInCloud('email').catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name: string | undefined;
|
||||
avatar: string | undefined;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||
function WorkspaceAvatar(props, ref) {
|
||||
const size = props.size || 20;
|
||||
const sizeStr = size + 'px';
|
||||
|
||||
return props.avatar ? (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<picture>
|
||||
<img
|
||||
style={{ width: sizeStr, height: sizeStr }}
|
||||
src={props.avatar}
|
||||
alt=""
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||
background: stringToColour(props.name || 'AFFiNE'),
|
||||
borderRadius: '50%',
|
||||
textAlign: 'center',
|
||||
lineHeight: size + 'px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{(props.name || 'AFFiNE').substring(0, 1)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,148 +0,0 @@
|
||||
import {
|
||||
displayFlex,
|
||||
displayInlineFlex,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
maxWidth: '200px',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledFooter = styled('div')({
|
||||
padding: '20px 40px',
|
||||
flexShrink: 0,
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
|
||||
export const StyleUserInfo = styled('div')({
|
||||
textAlign: 'left',
|
||||
marginLeft: '16px',
|
||||
flex: 1,
|
||||
p: {
|
||||
lineHeight: '24px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
'p:first-of-type': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: 600,
|
||||
},
|
||||
});
|
||||
|
||||
export const StyledModalHeaderLeft = styled('div')(() => {
|
||||
return { ...displayFlex('flex-start', 'center') };
|
||||
});
|
||||
export const StyledModalTitle = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHelperContainer = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginLeft: '15px',
|
||||
fontWeight: 400,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalContent = styled('div')({
|
||||
height: '534px',
|
||||
padding: '8px 40px',
|
||||
marginTop: '72px',
|
||||
overflow: 'auto',
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
export const StyledOperationWrapper = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-end', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceAdd = styled('div')(() => {
|
||||
return {
|
||||
width: '58px',
|
||||
height: '58px',
|
||||
borderRadius: '100%',
|
||||
background: '#f4f5fa',
|
||||
border: '1.5px dashed #f4f5fa',
|
||||
transition: 'background .2s',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
export const StyledModalHeader = styled('div')({
|
||||
width: '100%',
|
||||
height: '72px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '0 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
|
||||
export const StyledSignInButton = styled('button')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
paddingLeft: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingRight: '15px',
|
||||
borderRadius: '8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
'.circle': {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '20px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontSize: '24px',
|
||||
flexShrink: 0,
|
||||
marginRight: '16px',
|
||||
...displayInlineFlex('center', 'center'),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -2,7 +2,12 @@ import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon } from '@blocksuite/icons';
|
||||
import { Doc, useLiveData, useServiceOptional } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -28,9 +33,12 @@ type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts';
|
||||
const showList = environment.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST;
|
||||
|
||||
export const HelpIsland = () => {
|
||||
const page = useServiceOptional(Doc);
|
||||
const pageId = page?.id;
|
||||
const mode = useLiveData(page?.mode$);
|
||||
const docId = useLiveData(
|
||||
useService(GlobalContextService).globalContext.docId.$
|
||||
);
|
||||
const docRecordList = useService(DocsService).list;
|
||||
const doc = useLiveData(docId ? docRecordList.doc$(docId) : undefined);
|
||||
const mode = useLiveData(doc?.mode$);
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -61,7 +69,7 @@ export const HelpIsland = () => {
|
||||
onClick={() => {
|
||||
setShowSpread(!spread);
|
||||
}}
|
||||
inEdgelessPage={!!pageId && mode === 'edgeless'}
|
||||
inEdgelessPage={!!docId && mode === 'edgeless'}
|
||||
>
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }}
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { DocService, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
|
||||
import { WorkspaceSubPath } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
||||
const workspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace$
|
||||
);
|
||||
assertExists(workspace);
|
||||
export const TrashPageFooter = () => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
assertExists(pageMeta);
|
||||
const doc = useService(DocService).doc;
|
||||
const t = useAFFiNEI18N();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
@@ -34,19 +25,19 @@ export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
||||
const hintText = t['com.affine.cmdk.affine.editor.trash-footer-hint']();
|
||||
|
||||
const onRestore = useCallback(() => {
|
||||
restoreFromTrash(pageId);
|
||||
restoreFromTrash(doc.id);
|
||||
toast(
|
||||
t['com.affine.toastMessage.restored']({
|
||||
title: pageMeta.title || 'Untitled',
|
||||
title: doc.meta$.value.title || 'Untitled',
|
||||
})
|
||||
);
|
||||
}, [pageId, pageMeta.title, restoreFromTrash, t]);
|
||||
}, [doc.id, doc.meta$.value.title, restoreFromTrash, t]);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||
docCollection.removeDoc(pageId);
|
||||
docCollection.removeDoc(doc.id);
|
||||
toast(t['com.affine.toastMessage.permanentlyDeleted']());
|
||||
}, [docCollection, jumpToSubPath, pageId, workspace.id, t]);
|
||||
}, [jumpToSubPath, workspace.id, docCollection, doc.id, t]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
setOpen(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { allPageFilterSelectAtom } from '@affine/core/atoms';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const WorkspaceModeFilterTab = ({
|
||||
}: {
|
||||
activeFilter: AllPageFilterOption;
|
||||
}) => {
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const [value, setValue] = useState(activeFilter);
|
||||
const [filterMode, setFilterMode] = useAtom(allPageFilterSelectAtom);
|
||||
|
||||
+10
-7
@@ -18,7 +18,7 @@ import {
|
||||
resolveDragEndIntent,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -35,13 +35,13 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
|
||||
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||
import { Workbench } from '../../../../modules/workbench';
|
||||
import { WorkbenchService } from '../../../../modules/workbench';
|
||||
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
||||
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
|
||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
||||
import type { CollectionsListProps } from '../index';
|
||||
import { Page } from './page';
|
||||
import { Doc } from './doc';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||
@@ -131,8 +131,11 @@ export const CollectionSidebarNavItem = ({
|
||||
};
|
||||
return filterPage(collection, pageData);
|
||||
});
|
||||
const location = useLiveData(useService(Workbench).location$);
|
||||
const currentPath = location.pathname;
|
||||
const currentPath = useLiveData(
|
||||
useService(WorkbenchService).workbench.location$.map(
|
||||
location => location.pathname
|
||||
)
|
||||
);
|
||||
const path = `/collection/${collection.id}`;
|
||||
|
||||
const onRename = useCallback(
|
||||
@@ -231,12 +234,12 @@ export const CollectionSidebarNavItem = ({
|
||||
<div style={{ marginLeft: 20, marginTop: -4 }}>
|
||||
{pagesToRender.map(page => {
|
||||
return (
|
||||
<Page
|
||||
<Doc
|
||||
parentId={dndId}
|
||||
inAllowList={allowList.has(page.id)}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
allPageMeta={allPagesMeta}
|
||||
page={page}
|
||||
doc={page}
|
||||
key={page.id}
|
||||
docCollection={docCollection}
|
||||
/>
|
||||
|
||||
+27
-27
@@ -1,11 +1,11 @@
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
import { Workbench } from '@affine/core/modules/workbench';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
@@ -19,8 +19,8 @@ import { PostfixItem } from '../components/postfix-item';
|
||||
import { ReferencePage } from '../components/reference-page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const Page = ({
|
||||
page,
|
||||
export const Doc = ({
|
||||
doc,
|
||||
parentId,
|
||||
docCollection,
|
||||
allPageMeta,
|
||||
@@ -28,47 +28,47 @@ export const Page = ({
|
||||
removeFromAllowList,
|
||||
}: {
|
||||
parentId: DNDIdentifier;
|
||||
page: DocMeta;
|
||||
doc: DocMeta;
|
||||
inAllowList: boolean;
|
||||
removeFromAllowList: (id: string) => void;
|
||||
docCollection: DocCollection;
|
||||
allPageMeta: Record<string, DocMeta>;
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = React.useState(true);
|
||||
const workbench = useService(Workbench);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const pageId = page.id;
|
||||
const active = location.pathname === '/' + pageId;
|
||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||
const pageMode = useLiveData(pageRecord?.mode$);
|
||||
const dragItemId = getDNDId('collection-list', 'doc', pageId, parentId);
|
||||
const docId = doc.id;
|
||||
const active = location.pathname === '/' + docId;
|
||||
const docRecord = useLiveData(useService(DocsService).list.doc$(docId));
|
||||
const docMode = useLiveData(docRecord?.mode$);
|
||||
const dragItemId = getDNDId('collection-list', 'doc', docId, parentId);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [pageMode]);
|
||||
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [docMode]);
|
||||
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const clickPage = useCallback(() => {
|
||||
jumpToPage(docCollection.id, page.id);
|
||||
}, [jumpToPage, page.id, docCollection.id]);
|
||||
const clickDoc = useCallback(() => {
|
||||
jumpToPage(docCollection.id, doc.id);
|
||||
}, [jumpToPage, doc.id, docCollection.id]);
|
||||
|
||||
const references = useBlockSuitePageReferences(docCollection, pageId);
|
||||
const references = useBlockSuitePageReferences(docCollection, docId);
|
||||
const referencesToRender = references.filter(
|
||||
id => allPageMeta[id] && !allPageMeta[id]?.trash
|
||||
);
|
||||
|
||||
const pageTitle = page.title || t['Untitled']();
|
||||
const pageTitleElement = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||
}, [icon, pageTitle]);
|
||||
const docTitle = doc.title || t['Untitled']();
|
||||
const docTitleElement = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} title={docTitle} />;
|
||||
}, [icon, docTitle]);
|
||||
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: dragItemId,
|
||||
data: {
|
||||
preview: pageTitleElement,
|
||||
preview: docTitleElement,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Page = ({
|
||||
data-testid="collection-page"
|
||||
data-type="collection-list-item"
|
||||
icon={icon}
|
||||
onClick={clickPage}
|
||||
onClick={clickDoc}
|
||||
className={styles.title}
|
||||
active={active}
|
||||
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
||||
@@ -90,8 +90,8 @@ export const Page = ({
|
||||
postfix={
|
||||
<PostfixItem
|
||||
docCollection={docCollection}
|
||||
pageId={pageId}
|
||||
pageTitle={pageTitle}
|
||||
pageId={docId}
|
||||
pageTitle={docTitle}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inAllowList={inAllowList}
|
||||
/>
|
||||
@@ -100,7 +100,7 @@ export const Page = ({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
{page.title || t['Untitled']()}
|
||||
{doc.title || t['Untitled']()}
|
||||
</CollectionItem>
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
{referencesToRender.map(id => {
|
||||
@@ -110,7 +110,7 @@ export const Page = ({
|
||||
docCollection={docCollection}
|
||||
pageId={id}
|
||||
metaMapping={allPageMeta}
|
||||
parentIds={new Set([pageId])}
|
||||
parentIds={new Set([docId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export * from './collections-list';
|
||||
export { Page } from './page';
|
||||
export { Doc } from './doc';
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { IconButton } from '@affine/component/ui/button';
|
||||
import { Menu } from '@affine/component/ui/menu';
|
||||
import { Workbench } from '@affine/core/modules/workbench';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
@@ -40,7 +40,7 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
||||
const { setTrashModal } = useTrashModalHelper(docCollection);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const workbench = useService(Workbench);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
setRenameModalOpen?.();
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
import { Workbench } from '@affine/core/modules/workbench';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { MenuLinkItem } from '../../../app-sidebar';
|
||||
@@ -24,11 +24,11 @@ export const ReferencePage = ({
|
||||
parentIds,
|
||||
}: ReferencePageProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workbench = useService(Workbench);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
const active = location.pathname === '/' + pageId;
|
||||
|
||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||
const pageRecord = useLiveData(useService(DocsService).list.doc$(pageId));
|
||||
const pageMode = useLiveData(pageRecord?.mode$);
|
||||
const icon = useMemo(() => {
|
||||
return pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user