mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 08:09:52 +08:00
feat(infra): framework
This commit is contained in:
@@ -2,14 +2,14 @@ import {
|
||||
NoPermissionOrNotFound,
|
||||
NotFoundPage,
|
||||
} from '@affine/component/not-found-page';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { SignOutModal } from '../components/affine/sign-out-modal';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { signOutCloud } from '../utils/cloud-utils';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
import { SignIn } from './sign-in';
|
||||
|
||||
export const PageNotFound = ({
|
||||
@@ -17,9 +17,9 @@ export const PageNotFound = ({
|
||||
}: {
|
||||
noPermission?: boolean;
|
||||
}): ReactElement => {
|
||||
const { user } = useSession();
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const { reload } = useSession();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleBackButtonClick = useCallback(
|
||||
@@ -33,21 +33,21 @@ export const PageNotFound = ({
|
||||
|
||||
const onConfirmSignOut = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
await signOutCloud();
|
||||
await reload();
|
||||
}, [reload]);
|
||||
await authService.signOut();
|
||||
}, [authService]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{noPermission ? (
|
||||
<NoPermissionOrNotFound
|
||||
user={user}
|
||||
user={account}
|
||||
onBack={handleBackButtonClick}
|
||||
onSignOut={handleOpenSignOutModal}
|
||||
signInComponent={<SignIn />}
|
||||
/>
|
||||
) : (
|
||||
<NotFoundPage
|
||||
user={user}
|
||||
user={account}
|
||||
onBack={handleBackButtonClick}
|
||||
onSignOut={handleOpenSignOutModal}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||
import {
|
||||
changeEmailMutation,
|
||||
changePasswordMutation,
|
||||
@@ -17,18 +16,17 @@ import {
|
||||
verifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SubscriptionRedirect } from '../components/affine/auth/subscription-redirect';
|
||||
import { WindowsAppControls } from '../components/pure/header/windows-app-controls';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { useMutation } from '../hooks/use-mutation';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService, ServerConfigService } from '../modules/cloud';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
@@ -43,9 +41,13 @@ const authTypeSchema = z.enum([
|
||||
]);
|
||||
|
||||
export const AuthPage = (): ReactElement | null => {
|
||||
const user = useCurrentUser();
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
const t = useAFFiNEI18N();
|
||||
const { password: passwordLimits } = useCredentialsRequirement();
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
const passwordLimits = useLiveData(
|
||||
serverConfig.credentialsRequirement$.map(r => r?.password)
|
||||
);
|
||||
|
||||
const { authType } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -97,11 +99,16 @@ export const AuthPage = (): ReactElement | null => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
if (!passwordLimits || !account) {
|
||||
// TODO: loading UI
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (authType) {
|
||||
case 'onboarding':
|
||||
return (
|
||||
<OnboardingPage
|
||||
user={user}
|
||||
user={account}
|
||||
onOpenAffine={onOpenAffine}
|
||||
windowControl={<WindowsAppControls />}
|
||||
/>
|
||||
@@ -109,7 +116,7 @@ export const AuthPage = (): ReactElement | null => {
|
||||
case 'signUp': {
|
||||
return (
|
||||
<SignUpPage
|
||||
user={user}
|
||||
user={account}
|
||||
passwordLimits={passwordLimits}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
@@ -122,7 +129,7 @@ export const AuthPage = (): ReactElement | null => {
|
||||
case 'changePassword': {
|
||||
return (
|
||||
<ChangePasswordPage
|
||||
user={user}
|
||||
user={account}
|
||||
passwordLimits={passwordLimits}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
@@ -132,7 +139,7 @@ export const AuthPage = (): ReactElement | null => {
|
||||
case 'setPassword': {
|
||||
return (
|
||||
<SetPasswordPage
|
||||
user={user}
|
||||
user={account}
|
||||
passwordLimits={passwordLimits}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
@@ -150,9 +157,6 @@ export const AuthPage = (): ReactElement | null => {
|
||||
case 'confirm-change-email': {
|
||||
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
case 'subscription-redirect': {
|
||||
return <SubscriptionRedirect />;
|
||||
}
|
||||
case 'verify-email': {
|
||||
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
@@ -203,10 +207,16 @@ export const loader: LoaderFunction = async args => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const authService = useService(AuthService);
|
||||
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
useEffect(() => {
|
||||
authService.session.revalidate();
|
||||
}, [authService]);
|
||||
|
||||
if (loginStatus === 'unauthenticated' && !isRevalidating) {
|
||||
jumpToExpired(RouteLogic.REPLACE);
|
||||
}
|
||||
|
||||
@@ -214,5 +224,6 @@ export const Component = () => {
|
||||
return <AuthPage />;
|
||||
}
|
||||
|
||||
// TODO: loading UI
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -2,9 +2,6 @@ import { OAuthProviderType } from '@affine/graphql';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSession } from '../hooks/affine/use-current-user';
|
||||
import { signInCloud, signOutCloud } from '../utils/cloud-utils';
|
||||
|
||||
const supportedProvider = z.enum([
|
||||
'google',
|
||||
...Object.values(OAuthProviderType),
|
||||
@@ -22,12 +19,8 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
if (session.user) {
|
||||
// already signed in, need to sign out first
|
||||
await signOutCloud(request.url);
|
||||
}
|
||||
// sign out first
|
||||
await fetch('/api/auth/sign-out');
|
||||
|
||||
const maybeProvider = supportedProvider.safeParse(provider);
|
||||
if (maybeProvider.success) {
|
||||
@@ -36,9 +29,11 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
if (provider === 'google') {
|
||||
provider = OAuthProviderType.Google;
|
||||
}
|
||||
await signInCloud(provider, undefined, {
|
||||
redirectUri,
|
||||
});
|
||||
location.href = `${
|
||||
runtimeConfig.serverUrlPrefix
|
||||
}/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
initEmptyPage,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceListService,
|
||||
WorkspaceManager,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import {
|
||||
lazy,
|
||||
@@ -20,8 +19,8 @@ import { type LoaderFunction, useSearchParams } from 'react-router-dom';
|
||||
import { createFirstAppData } from '../bootstrap/first-app-data';
|
||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||
import { WorkspaceFallback } from '../components/workspace';
|
||||
import { useSession } from '../hooks/affine/use-current-user';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
const AllWorkspaceModals = lazy(() =>
|
||||
@@ -36,14 +35,16 @@ export const loader: LoaderFunction = async () => {
|
||||
|
||||
export const Component = () => {
|
||||
// navigating and creating may be slow, to avoid flickering, we show workspace fallback
|
||||
const [navigating, setNavigating] = useState(false);
|
||||
const [navigating, setNavigating] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const { status } = useSession();
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const authService = useService(AuthService);
|
||||
const loggedIn = useLiveData(
|
||||
authService.session.status$.map(s => s === 'authenticated')
|
||||
);
|
||||
|
||||
const workspaceListService = useService(WorkspaceListService);
|
||||
const list = useLiveData(workspaceListService.workspaceList$);
|
||||
const workspaceListStatus = useLiveData(workspaceListService.status$);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const list = useLiveData(workspacesService.list.workspaces$);
|
||||
const listIsLoading = useLiveData(workspacesService.list.isLoading$);
|
||||
|
||||
const { openPage } = useNavigateHelper();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -53,26 +54,23 @@ export const Component = () => {
|
||||
const createCloudWorkspace = useCallback(() => {
|
||||
if (createOnceRef.current) return;
|
||||
createOnceRef.current = true;
|
||||
workspaceManager
|
||||
.createWorkspace(WorkspaceFlavour.AFFINE_CLOUD, async workspace => {
|
||||
workspacesService
|
||||
.create(WorkspaceFlavour.AFFINE_CLOUD, async workspace => {
|
||||
workspace.meta.setName('AFFiNE Cloud');
|
||||
const page = workspace.createDoc();
|
||||
initEmptyPage(page);
|
||||
})
|
||||
.then(workspace => openPage(workspace.id, WorkspaceSubPath.ALL))
|
||||
.catch(err => console.error('Failed to create cloud workspace', err));
|
||||
}, [openPage, workspaceManager]);
|
||||
}, [openPage, workspacesService]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (workspaceListStatus.loading) {
|
||||
if (listIsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check is user logged in && has cloud workspace
|
||||
if (
|
||||
searchParams.get('initCloud') === 'true' &&
|
||||
status === 'authenticated'
|
||||
) {
|
||||
if (searchParams.get('initCloud') === 'true' && loggedIn) {
|
||||
searchParams.delete('initCloud');
|
||||
if (list.every(w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD)) {
|
||||
createCloudWorkspace();
|
||||
@@ -81,6 +79,7 @@ export const Component = () => {
|
||||
}
|
||||
|
||||
if (list.length === 0) {
|
||||
setNavigating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,26 +88,31 @@ export const Component = () => {
|
||||
|
||||
const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
|
||||
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
|
||||
setNavigating(true);
|
||||
}, [
|
||||
createCloudWorkspace,
|
||||
list,
|
||||
openPage,
|
||||
searchParams,
|
||||
status,
|
||||
workspaceListStatus.loading,
|
||||
listIsLoading,
|
||||
loggedIn,
|
||||
navigating,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setCreating(true);
|
||||
createFirstAppData(workspaceManager)
|
||||
createFirstAppData(workspacesService)
|
||||
.then(workspaceMeta => {
|
||||
if (workspaceMeta) {
|
||||
openPage(workspaceMeta.id, WorkspaceSubPath.ALL);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to create first app data', err);
|
||||
})
|
||||
.finally(() => {
|
||||
setCreating(false);
|
||||
});
|
||||
}, [workspaceManager]);
|
||||
}, [openPage, workspacesService]);
|
||||
|
||||
if (navigating || creating) {
|
||||
return <WorkspaceFallback></WorkspaceFallback>;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
fetcher,
|
||||
getInviteInfoQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
@@ -13,8 +14,8 @@ import { redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { setOnceSignedInEventAtom } from '../atoms/event';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const inviteId = args.params.inviteId || '';
|
||||
@@ -47,7 +48,14 @@ export const loader: LoaderFunction = async args => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const authService = useService(AuthService);
|
||||
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
|
||||
useEffect(() => {
|
||||
authService.session.revalidate();
|
||||
}, [authService]);
|
||||
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
@@ -68,7 +76,7 @@ export const Component = () => {
|
||||
}, [inviteInfo.workspace.id, jumpToSubPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
if (loginStatus === 'unauthenticated' && !isRevalidating) {
|
||||
// We can not pass function to navigate state, so we need to save it in atom
|
||||
setOnceSignedInEvent(openWorkspace);
|
||||
jumpToSignIn(RouteLogic.REPLACE, {
|
||||
@@ -79,6 +87,7 @@ export const Component = () => {
|
||||
}
|
||||
}, [
|
||||
inviteInfo.workspace.id,
|
||||
isRevalidating,
|
||||
jumpToSignIn,
|
||||
loginStatus,
|
||||
openWorkspace,
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { fetchWithTraceReport } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
AffineCloudBlobStorage,
|
||||
StaticBlobStorage,
|
||||
} from '@affine/workspace-impl';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import type { Doc, PageMode } from '@toeverything/infra';
|
||||
import type { Doc, DocMode, Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
DocStorageImpl,
|
||||
DocsService,
|
||||
EmptyBlobStorage,
|
||||
LocalBlobStorage,
|
||||
PageManager,
|
||||
FrameworkScope,
|
||||
ReadonlyDocStorage,
|
||||
RemoteBlobStorage,
|
||||
ServiceProviderContext,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceIdContext,
|
||||
WorkspaceManager,
|
||||
WorkspaceScope,
|
||||
WorkspaceFlavourProvider,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -42,7 +33,7 @@ import { AppContainer } from '../../components/affine/app-container';
|
||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||
import { MainContainer } from '../../components/workspace';
|
||||
import { CurrentWorkspaceService } from '../../modules/workspace';
|
||||
import { CloudBlobStorage } from '../../modules/workspace-engine/impls/engine/blob-cloud';
|
||||
import * as styles from './share-detail-page.css';
|
||||
import { ShareFooter } from './share-footer';
|
||||
import { ShareHeader } from './share-header';
|
||||
@@ -58,12 +49,7 @@ export async function downloadBinaryFromCloud(
|
||||
rootGuid: string,
|
||||
pageGuid: string
|
||||
): Promise<CloudDoc | null> {
|
||||
const response = await fetchWithTraceReport(
|
||||
`/api/workspaces/${rootGuid}/docs/${pageGuid}`,
|
||||
{
|
||||
priority: 'high',
|
||||
}
|
||||
);
|
||||
const response = await fetch(`/api/workspaces/${rootGuid}/docs/${pageGuid}`);
|
||||
if (response.ok) {
|
||||
const publishMode = (response.headers.get('publish-mode') ||
|
||||
'page') as DocPublishMode;
|
||||
@@ -79,7 +65,7 @@ export async function downloadBinaryFromCloud(
|
||||
type LoaderData = {
|
||||
pageId: string;
|
||||
workspaceId: string;
|
||||
publishMode: PageMode;
|
||||
publishMode: DocMode;
|
||||
pageArrayBuffer: ArrayBuffer;
|
||||
workspaceArrayBuffer: ArrayBuffer;
|
||||
};
|
||||
@@ -124,72 +110,90 @@ export const loader: LoaderFunction = async ({ params }) => {
|
||||
export const Component = () => {
|
||||
const {
|
||||
workspaceId,
|
||||
pageId,
|
||||
pageId: docId,
|
||||
publishMode,
|
||||
workspaceArrayBuffer,
|
||||
pageArrayBuffer,
|
||||
} = useLoaderData() as LoaderData;
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const currentWorkspace = useService(CurrentWorkspaceService);
|
||||
const t = useAFFiNEI18N();
|
||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||
const [page, setPage] = useState<Doc | null>(null);
|
||||
const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const defaultCloudProvider = workspacesService.framework.get(
|
||||
WorkspaceFlavourProvider('CLOUD')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// create a workspace for share page
|
||||
const workspace = workspaceManager.instantiate(
|
||||
const { workspace } = workspacesService.open(
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
metadata: {
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
},
|
||||
isSharedMode: true,
|
||||
},
|
||||
services => {
|
||||
services
|
||||
.scope(WorkspaceScope)
|
||||
.addImpl(LocalBlobStorage, EmptyBlobStorage)
|
||||
.addImpl(RemoteBlobStorage('affine'), AffineCloudBlobStorage, [
|
||||
WorkspaceIdContext,
|
||||
])
|
||||
.addImpl(RemoteBlobStorage('static'), StaticBlobStorage)
|
||||
.addImpl(
|
||||
DocStorageImpl,
|
||||
new ReadonlyDocStorage({
|
||||
[workspaceId]: new Uint8Array(workspaceArrayBuffer),
|
||||
[pageId]: new Uint8Array(pageArrayBuffer),
|
||||
})
|
||||
);
|
||||
{
|
||||
...defaultCloudProvider,
|
||||
getEngineProvider(workspace) {
|
||||
return {
|
||||
getDocStorage() {
|
||||
return new ReadonlyDocStorage({
|
||||
[workspace.id]: new Uint8Array(workspaceArrayBuffer),
|
||||
[docId]: new Uint8Array(pageArrayBuffer),
|
||||
});
|
||||
},
|
||||
getAwarenessConnections() {
|
||||
return [];
|
||||
},
|
||||
getDocServer() {
|
||||
return null;
|
||||
},
|
||||
getLocalBlobStorage() {
|
||||
return EmptyBlobStorage;
|
||||
},
|
||||
getRemoteBlobStorages() {
|
||||
return [new CloudBlobStorage(workspace.id)];
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setWorkspace(workspace);
|
||||
|
||||
workspace.engine
|
||||
.waitForRootDocReady()
|
||||
.then(() => {
|
||||
const { page } = workspace.services.get(PageManager).open(pageId);
|
||||
const { doc } = workspace.scope.get(DocsService).open(docId);
|
||||
|
||||
workspace.docCollection.awarenessStore.setReadonly(
|
||||
page.blockSuiteDoc.blockCollection,
|
||||
doc.blockSuiteDoc.blockCollection,
|
||||
true
|
||||
);
|
||||
|
||||
currentWorkspace.openWorkspace(workspace);
|
||||
setPage(page);
|
||||
setPage(doc);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [
|
||||
currentWorkspace,
|
||||
defaultCloudProvider,
|
||||
pageArrayBuffer,
|
||||
pageId,
|
||||
docId,
|
||||
workspaceArrayBuffer,
|
||||
workspaceId,
|
||||
workspaceManager,
|
||||
workspacesService,
|
||||
]);
|
||||
|
||||
const pageTitle = useLiveData(page?.title$);
|
||||
|
||||
usePageDocumentTitle(pageTitle);
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const authService = useService(AuthService);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
|
||||
const onEditorLoad = useCallback(
|
||||
(_: BlockSuiteDoc, editor: AffineEditorContainer) => {
|
||||
@@ -199,57 +203,59 @@ export const Component = () => {
|
||||
[setActiveBlocksuiteEditor]
|
||||
);
|
||||
|
||||
if (!page) {
|
||||
if (!workspace || !page) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<ServiceProviderContext.Provider value={page.services}>
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.mainContainer}>
|
||||
<ShareHeader
|
||||
pageId={page.id}
|
||||
publishMode={publishMode}
|
||||
docCollection={page.blockSuiteDoc.collection}
|
||||
/>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={clsx(
|
||||
'affine-page-viewport',
|
||||
styles.editorContainer
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor
|
||||
isPublic
|
||||
publishMode={publishMode}
|
||||
docCollection={page.blockSuiteDoc.collection}
|
||||
pageId={page.id}
|
||||
onLoad={onEditorLoad}
|
||||
/>
|
||||
{publishMode === 'page' ? <ShareFooter /> : null}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
{loginStatus !== 'authenticated' ? (
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className={styles.linkText}>
|
||||
{t['com.affine.share-page.footer.built-with']()}
|
||||
</span>
|
||||
<Logo1Icon fontSize={20} />
|
||||
</a>
|
||||
) : null}
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<FrameworkScope scope={page.scope}>
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.mainContainer}>
|
||||
<ShareHeader
|
||||
pageId={page.id}
|
||||
publishMode={publishMode}
|
||||
docCollection={page.blockSuiteDoc.collection}
|
||||
/>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={clsx(
|
||||
'affine-page-viewport',
|
||||
styles.editorContainer
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor
|
||||
isPublic
|
||||
publishMode={publishMode}
|
||||
docCollection={page.blockSuiteDoc.collection}
|
||||
pageId={page.id}
|
||||
onLoad={onEditorLoad}
|
||||
/>
|
||||
{publishMode === 'page' ? <ShareFooter /> : null}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
{loginStatus !== 'authenticated' ? (
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className={styles.linkText}>
|
||||
{t['com.affine.share-page.footer.built-with']()}
|
||||
</span>
|
||||
<Logo1Icon fontSize={20} />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
</ServiceProviderContext.Provider>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import type { PageMode } from '@toeverything/infra';
|
||||
import type { DocMode } from '@toeverything/infra';
|
||||
|
||||
import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index';
|
||||
import * as styles from './share-header.css';
|
||||
@@ -12,7 +12,7 @@ export function ShareHeader({
|
||||
docCollection,
|
||||
}: {
|
||||
pageId: string;
|
||||
publishMode: PageMode;
|
||||
publishMode: DocMode;
|
||||
docCollection: DocCollection;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import type { AuthProps } from '../components/affine/auth';
|
||||
import { AuthPanel } from '../components/affine/auth';
|
||||
import { SubscriptionRedirect } from '../components/affine/auth/subscription-redirect';
|
||||
import { useSubscriptionSearch } from '../components/affine/auth/use-subscription';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
interface LocationState {
|
||||
@@ -19,31 +18,18 @@ interface LocationState {
|
||||
};
|
||||
}
|
||||
export const SignIn = () => {
|
||||
const paymentRedirectRef = useRef<'redirect' | 'ignore' | null>(null);
|
||||
const [{ state, email = '', emailType = 'changePassword' }, setAuthAtom] =
|
||||
useAtom(authAtom);
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
||||
const location = useLocation() as LocationState;
|
||||
const navigate = useNavigate();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const subscriptionData = useSubscriptionSearch();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const isLoggedIn = loginStatus === 'authenticated';
|
||||
|
||||
// Check payment redirect once after session loaded, to avoid unnecessary page rendering.
|
||||
if (loginStatus !== 'loading' && !paymentRedirectRef.current) {
|
||||
// If user is logged in and visit sign in page with subscription query, redirect to stripe payment page immediately.
|
||||
// Otherwise, user will login through email, and then redirect to payment page.
|
||||
paymentRedirectRef.current =
|
||||
subscriptionData && isLoggedIn ? 'redirect' : 'ignore';
|
||||
}
|
||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
||||
|
||||
useEffect(() => {
|
||||
if (paymentRedirectRef.current === 'redirect') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (location.state?.callbackURL) {
|
||||
navigate(location.state.callbackURL, {
|
||||
@@ -57,10 +43,9 @@ export const SignIn = () => {
|
||||
}
|
||||
}, [
|
||||
jumpToIndex,
|
||||
location.state?.callbackURL,
|
||||
location.state,
|
||||
navigate,
|
||||
setAuthAtom,
|
||||
subscriptionData,
|
||||
isLoggedIn,
|
||||
searchParams,
|
||||
]);
|
||||
@@ -86,10 +71,6 @@ export const SignIn = () => {
|
||||
[setAuthAtom]
|
||||
);
|
||||
|
||||
if (paymentRedirectRef.current === 'redirect') {
|
||||
return <SubscriptionRedirect />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SignInPageContainer>
|
||||
<div style={{ maxWidth: '400px', width: '100%' }}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -20,7 +20,7 @@ import * as styles from './index.css';
|
||||
|
||||
export const AllCollection = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
|
||||
const collectionService = useService(CollectionService);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { filterContainerStyle } from '../../../components/filter-container.css';
|
||||
@@ -17,7 +17,7 @@ export const FilterContainer = ({
|
||||
filters: Filter[];
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const collectionService = useService(CollectionService);
|
||||
const saveToCollection = useCallback(
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import * as styles from './all-page.css';
|
||||
@@ -22,10 +22,11 @@ export const AllPageHeader = ({
|
||||
filters: Filter[];
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { importFile, createEdgeless, createPage } = usePageHelper(
|
||||
workspace.docCollection
|
||||
);
|
||||
|
||||
return (
|
||||
<Header
|
||||
left={
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-me
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { performanceRenderLogger } from '@affine/core/shared';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
@@ -17,12 +17,12 @@ import { FilterContainer } from './all-page-filter';
|
||||
import { AllPageHeader } from './all-page-header';
|
||||
|
||||
export const AllPage = () => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
filters: filters,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ export const AllPage = () => {
|
||||
export const Component = () => {
|
||||
performanceRenderLogger.info('AllPage');
|
||||
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,12 +31,12 @@ const EmptyTagListHeader = () => {
|
||||
};
|
||||
|
||||
export const AllTag = () => {
|
||||
const tagService = useService(TagService);
|
||||
const tags = useLiveData(tagService.tags$);
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
|
||||
const tagMetas: TagMeta[] = useLiveData(tagService.tagMetas$);
|
||||
const tagMetas: TagMeta[] = useLiveData(tagList.tagMetas$);
|
||||
|
||||
const handleCloseModal = useCallback(
|
||||
(open: boolean) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
PageIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Component = function CollectionPage() {
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const navigate = useNavigateHelper();
|
||||
const params = useParams();
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const collection = collections.find(v => v.id === params.collectionId);
|
||||
|
||||
const notifyCollectionDeleted = useCallback(() => {
|
||||
@@ -103,7 +103,7 @@ export const Component = function CollectionPage() {
|
||||
};
|
||||
|
||||
const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const collectionService = useService(CollectionService);
|
||||
const { node, open } = useEditCollection(useAllPageListConfig());
|
||||
const { jumpToCollections } = useNavigateHelper();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
BookmarkService,
|
||||
@@ -14,29 +13,23 @@ import {
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { type AffineEditorContainer, AIProvider } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import {
|
||||
Doc,
|
||||
DocService,
|
||||
DocsService,
|
||||
FrameworkScope,
|
||||
globalBlockSuiteSchema,
|
||||
GlobalState,
|
||||
GlobalContextService,
|
||||
GlobalStateService,
|
||||
LiveData,
|
||||
PageManager,
|
||||
PageRecordList,
|
||||
ServiceProviderContext,
|
||||
useLiveData,
|
||||
useService,
|
||||
Workspace,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
|
||||
@@ -58,7 +51,7 @@ import {
|
||||
sidebarTabs,
|
||||
} from '../../../modules/multi-tab-sidebar';
|
||||
import {
|
||||
RightSidebar,
|
||||
RightSidebarService,
|
||||
RightSidebarViewIsland,
|
||||
} from '../../../modules/right-sidebar';
|
||||
import {
|
||||
@@ -74,8 +67,7 @@ import { DetailPageHeader } from './detail-page-header';
|
||||
const RIGHT_SIDEBAR_TABS_ACTIVE_KEY = 'app:settings:rightsidebar:tabs:active';
|
||||
|
||||
const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const globalState = useService(GlobalState);
|
||||
const rightSidebar = useService(RightSidebar);
|
||||
const globalState = useService(GlobalStateService).globalState;
|
||||
const activeTabName = useLiveData(
|
||||
LiveData.from(
|
||||
globalState.watch<SidebarTabName>(RIGHT_SIDEBAR_TABS_ACTIVE_KEY),
|
||||
@@ -89,13 +81,15 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
[globalState]
|
||||
);
|
||||
|
||||
const page = useService(Doc);
|
||||
const pageRecordList = useService(PageRecordList);
|
||||
const currentPageId = page.id;
|
||||
const doc = useService(DocService).doc;
|
||||
const docRecordList = useService(DocsService).list;
|
||||
const { openPage, jumpToTag } = useNavigateHelper();
|
||||
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||
const docCollection = workspace.docCollection;
|
||||
const mode = useLiveData(doc.mode$);
|
||||
|
||||
const isActiveView = useIsActiveView();
|
||||
// TODO: remove jotai here
|
||||
@@ -116,15 +110,32 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
});
|
||||
}, [activeTabName, rightSidebar, setActiveTabName]);
|
||||
|
||||
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
|
||||
meta => meta.id === page.id
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
globalContext.docId.set(doc.id);
|
||||
|
||||
const isInTrash = pageMeta?.trash;
|
||||
return () => {
|
||||
globalContext.docId.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [doc, globalContext, isActiveView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
globalContext.docMode.set(mode);
|
||||
|
||||
return () => {
|
||||
globalContext.docId.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [doc, globalContext, isActiveView, mode]);
|
||||
|
||||
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
|
||||
|
||||
const mode = useLiveData(page.mode$);
|
||||
useRegisterBlocksuiteEditorCommands();
|
||||
const title = useLiveData(page.title$);
|
||||
const title = useLiveData(doc.title$);
|
||||
usePageDocumentTitle(title);
|
||||
|
||||
const onLoad = useCallback(
|
||||
@@ -170,9 +181,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const disposable = new DisposableGroup();
|
||||
|
||||
pageService.getEditorMode = (pageId: string) =>
|
||||
pageRecordList.record$(pageId).value?.mode$.value ?? 'page';
|
||||
docRecordList.doc$(pageId).value?.mode$.value ?? 'page';
|
||||
pageService.getDocUpdatedAt = (pageId: string) => {
|
||||
const linkedPage = pageRecordList.record$(pageId).value;
|
||||
const linkedPage = docRecordList.doc$(pageId).value;
|
||||
if (!linkedPage) return new Date();
|
||||
|
||||
const updatedDate = linkedPage.meta$.value.updatedDate;
|
||||
@@ -180,7 +191,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
return new Date(updatedDate || createDate || Date.now());
|
||||
};
|
||||
|
||||
page.setMode(mode);
|
||||
doc.setMode(mode);
|
||||
// fixme: it seems pageLinkClicked is not triggered sometimes?
|
||||
disposable.add(
|
||||
pageService.slots.docLinkClicked.on(({ docId }) => {
|
||||
@@ -189,12 +200,12 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
);
|
||||
disposable.add(
|
||||
pageService.slots.tagClicked.on(({ tagId }) => {
|
||||
jumpToTag(currentWorkspace.id, tagId);
|
||||
jumpToTag(workspace.id, tagId);
|
||||
})
|
||||
);
|
||||
disposable.add(
|
||||
pageService.slots.editorModeSwitch.on(mode => {
|
||||
page.setMode(mode);
|
||||
doc.setMode(mode);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -205,13 +216,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
};
|
||||
},
|
||||
[
|
||||
docCollection.id,
|
||||
currentWorkspace.id,
|
||||
jumpToTag,
|
||||
doc,
|
||||
mode,
|
||||
docRecordList,
|
||||
openPage,
|
||||
page,
|
||||
pageRecordList,
|
||||
docCollection.id,
|
||||
jumpToTag,
|
||||
workspace.id,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -220,16 +231,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<DetailPageHeader
|
||||
page={page.blockSuiteDoc}
|
||||
workspace={currentWorkspace}
|
||||
/>
|
||||
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div className={styles.mainContainer}>
|
||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||
<AffineErrorBoundary key={currentPageId}>
|
||||
<TopTip pageId={currentPageId} workspace={currentWorkspace} />
|
||||
<AffineErrorBoundary key={doc.id}>
|
||||
<TopTip pageId={doc.id} workspace={workspace} />
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={clsx(
|
||||
@@ -239,7 +247,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
pageId={doc.id}
|
||||
onLoad={onLoad}
|
||||
docCollection={docCollection}
|
||||
/>
|
||||
@@ -247,7 +255,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</AffineErrorBoundary>
|
||||
{isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
|
||||
{isInTrash ? <TrashPageFooter /> : null}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
|
||||
@@ -282,7 +290,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
}
|
||||
/>
|
||||
|
||||
<ImagePreviewModal pageId={currentPageId} docCollection={docCollection} />
|
||||
<ImagePreviewModal pageId={doc.id} docCollection={docCollection} />
|
||||
<GlobalPageHistoryModal />
|
||||
<PageAIOnboarding />
|
||||
</>
|
||||
@@ -290,73 +298,65 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
});
|
||||
|
||||
export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const pageRecordList = useService(PageRecordList);
|
||||
const pageListReady = useLiveData(pageRecordList.isReady$);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const docRecordList = docsService.list;
|
||||
const docListReady = useLiveData(docRecordList.isReady$);
|
||||
const docRecord = docRecordList.doc$(pageId).value;
|
||||
|
||||
const pageRecords = useLiveData(pageRecordList.records$);
|
||||
|
||||
const pageRecord = useMemo(
|
||||
() => pageRecords.find(page => page.id === pageId),
|
||||
[pageRecords, pageId]
|
||||
);
|
||||
const pageManager = useService(PageManager);
|
||||
|
||||
const [page, setPage] = useState<Doc | null>(null);
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!pageRecord) {
|
||||
if (!docRecord) {
|
||||
return;
|
||||
}
|
||||
const { page, release } = pageManager.open(pageRecord.id);
|
||||
setPage(page);
|
||||
const { doc: opened, release } = docsService.open(pageId);
|
||||
setDoc(opened);
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [pageManager, pageRecord]);
|
||||
}, [docRecord, docsService, pageId]);
|
||||
|
||||
// set sync engine priority target
|
||||
useEffect(() => {
|
||||
currentWorkspace.setPriorityLoad(pageId, 10);
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 10);
|
||||
return () => {
|
||||
currentWorkspace.setPriorityLoad(pageId, 5);
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 5);
|
||||
};
|
||||
}, [currentWorkspace, pageId]);
|
||||
|
||||
const jumpOnce = useLiveData(pageRecord?.meta$.map(meta => meta.jumpOnce));
|
||||
const jumpOnce = useLiveData(doc?.meta$.map(meta => meta.jumpOnce));
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpOnce) {
|
||||
pageRecord?.setMeta({ jumpOnce: false });
|
||||
doc?.record.setMeta({ jumpOnce: false });
|
||||
}
|
||||
}, [jumpOnce, pageRecord]);
|
||||
}, [doc?.record, jumpOnce]);
|
||||
|
||||
const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
|
||||
|
||||
useEffect(() => {
|
||||
if (page && pageRecord?.meta?.trash) {
|
||||
if (doc && isInTrash) {
|
||||
currentWorkspace.docCollection.awarenessStore.setReadonly(
|
||||
page.blockSuiteDoc.blockCollection,
|
||||
doc.blockSuiteDoc.blockCollection,
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [
|
||||
currentWorkspace.docCollection.awarenessStore,
|
||||
page,
|
||||
pageRecord?.meta?.trash,
|
||||
]);
|
||||
}, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
|
||||
|
||||
// if sync engine has been synced and the page is null, show 404 page.
|
||||
if (pageListReady && !page) {
|
||||
if (docListReady && !doc) {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
if (!doc) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ServiceProviderContext.Provider value={page.services}>
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<DetailPageImpl />
|
||||
</ServiceProviderContext.Provider>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||
import { ZipTransformer } from '@blocksuite/blocks';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
ServiceProviderContext,
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceListService,
|
||||
WorkspaceManager,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useEffect, useMemo } from 'react';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
@@ -17,7 +17,6 @@ import { WorkspaceFallback } from '../../components/workspace';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
||||
import { WorkbenchRoot } from '../../modules/workbench';
|
||||
import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace';
|
||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
||||
import { performanceRenderLogger } from '../../shared';
|
||||
import { PageNotFound } from '../404';
|
||||
@@ -38,62 +37,87 @@ declare global {
|
||||
export const Component = (): ReactElement => {
|
||||
performanceRenderLogger.info('WorkspaceLayout');
|
||||
|
||||
const currentWorkspaceService = useService(CurrentWorkspaceService);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const { workspaceList, loading: listLoading } = useLiveData(
|
||||
useService(WorkspaceListService).status$
|
||||
);
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const [showNotFound, setShowNotFound] = useState(false);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const listLoading = useLiveData(workspacesService.list.isLoading$);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
|
||||
const meta = useMemo(() => {
|
||||
return workspaceList.find(({ id }) => id === params.workspaceId);
|
||||
}, [workspaceList, params.workspaceId]);
|
||||
return workspaces.find(({ id }) => id === params.workspaceId);
|
||||
}, [workspaces, params.workspaceId]);
|
||||
|
||||
const workspace = useWorkspace(meta);
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) {
|
||||
currentWorkspaceService.closeWorkspace();
|
||||
return undefined;
|
||||
}
|
||||
currentWorkspaceService.openWorkspace(workspace ?? null);
|
||||
workspacesService.list.revalidate();
|
||||
}, [workspacesService]);
|
||||
|
||||
// for debug purpose
|
||||
window.currentWorkspace = workspace;
|
||||
window.exportWorkspaceSnapshot = async () => {
|
||||
const zip = await ZipTransformer.exportDocs(
|
||||
workspace.docCollection,
|
||||
Array.from(workspace.docCollection.docs.values()).map(collection =>
|
||||
collection.getDoc()
|
||||
)
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
// for debug purpose
|
||||
window.currentWorkspace = workspace ?? undefined;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine:workspace:change', {
|
||||
detail: {
|
||||
id: workspace.id,
|
||||
},
|
||||
})
|
||||
);
|
||||
const url = URL.createObjectURL(zip);
|
||||
// download url
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${workspace.docCollection.meta.name}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine:workspace:change', {
|
||||
detail: {
|
||||
id: workspace.id,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
}, [meta, workspaceManager, workspace, currentWorkspaceService]);
|
||||
window.exportWorkspaceSnapshot = async () => {
|
||||
const zip = await ZipTransformer.exportDocs(
|
||||
workspace.docCollection,
|
||||
Array.from(workspace.docCollection.docs.values()).map(collection =>
|
||||
collection.getDoc()
|
||||
)
|
||||
);
|
||||
const url = URL.createObjectURL(zip);
|
||||
// download url
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${workspace.docCollection.meta.name}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
globalContext.workspaceId.set(workspace.id);
|
||||
return () => {
|
||||
window.currentWorkspace = undefined;
|
||||
globalContext.workspaceId.set(null);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [globalContext, meta, workspace]);
|
||||
|
||||
// avoid doing operation, before workspace is loaded
|
||||
const isRootDocReady =
|
||||
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
||||
|
||||
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
|
||||
if (listLoading === false && meta === undefined) {
|
||||
useEffect(() => {
|
||||
if (listLoading === false && meta === undefined) {
|
||||
setShowNotFound(true);
|
||||
}
|
||||
if (meta) {
|
||||
setShowNotFound(false);
|
||||
}
|
||||
}, [listLoading, meta, workspacesService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showNotFound) {
|
||||
const timer = setInterval(() => {
|
||||
workspacesService.list.revalidate();
|
||||
}, 3000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [showNotFound, workspacesService]);
|
||||
|
||||
if (showNotFound) {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
if (!workspace) {
|
||||
@@ -102,15 +126,15 @@ export const Component = (): ReactElement => {
|
||||
|
||||
if (!isRootDocReady) {
|
||||
return (
|
||||
<ServiceProviderContext.Provider value={workspace.services}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<WorkspaceFallback key="workspaceLoading" />
|
||||
<AllWorkspaceModals />
|
||||
</ServiceProviderContext.Provider>
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ServiceProviderContext.Provider value={workspace.services}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
@@ -119,6 +143,6 @@ export const Component = (): ReactElement => {
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</Suspense>
|
||||
</ServiceProviderContext.Provider>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ViewBodyIsland,
|
||||
ViewHeaderIsland,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
@@ -18,11 +18,11 @@ import { TagDetailHeader } from './header';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
|
||||
const tagService = useService(TagService);
|
||||
const currentTag = useLiveData(tagService.tagByTagId$(tagId));
|
||||
const tagList = useService(TagService).tagList;
|
||||
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
||||
|
||||
const pageIds = useLiveData(currentTag?.pageIds$);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DeleteIcon } from '@blocksuite/icons';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench';
|
||||
@@ -38,12 +38,12 @@ const TrashHeader = () => {
|
||||
};
|
||||
|
||||
export const TrashPage = () => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
assertExists(docCollection);
|
||||
|
||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
trash: true,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user