mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
refactor(infra): directory structure (#4615)
This commit is contained in:
53
packages/frontend/core/src/pages/404.tsx
Normal file
53
packages/frontend/core/src/pages/404.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NotFoundPage } from '@affine/component/not-found-page';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
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';
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
const { data: session } = useSession();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleBackButtonClick = useCallback(
|
||||
() => jumpToIndex(RouteLogic.REPLACE),
|
||||
[jumpToIndex]
|
||||
);
|
||||
|
||||
const handleOpenSignOutModal = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const onConfirmSignOut = useCallback(async () => {
|
||||
setOpen(false);
|
||||
await signOutCloud({
|
||||
callbackUrl: '/signIn',
|
||||
});
|
||||
}, [setOpen]);
|
||||
return (
|
||||
<>
|
||||
<NotFoundPage
|
||||
user={
|
||||
session?.user
|
||||
? {
|
||||
name: session.user.name || '',
|
||||
email: session.user.email || '',
|
||||
avatar: session.user.image || '',
|
||||
}
|
||||
: null
|
||||
}
|
||||
onBack={handleBackButtonClick}
|
||||
onSignOut={handleOpenSignOutModal}
|
||||
/>
|
||||
<SignOutModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={onConfirmSignOut}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
177
packages/frontend/core/src/pages/auth.tsx
Normal file
177
packages/frontend/core/src/pages/auth.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
ChangeEmailPage,
|
||||
ChangePasswordPage,
|
||||
ConfirmChangeEmail,
|
||||
SetPasswordPage,
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import {
|
||||
changeEmailMutation,
|
||||
changePasswordMutation,
|
||||
sendVerifyChangeEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { fetcher, useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
type LoaderFunction,
|
||||
redirect,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
'confirm-change-email',
|
||||
]);
|
||||
|
||||
export const AuthPage = (): ReactElement | null => {
|
||||
const user = useCurrentUser();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { authType } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const { trigger: changePassword } = useMutation({
|
||||
mutation: changePasswordMutation,
|
||||
});
|
||||
|
||||
const { trigger: sendVerifyChangeEmail } = useMutation({
|
||||
mutation: sendVerifyChangeEmailMutation,
|
||||
});
|
||||
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onSendVerifyChangeEmail = useCallback(
|
||||
async (email: string) => {
|
||||
const res = await sendVerifyChangeEmail({
|
||||
token: searchParams.get('token') || '',
|
||||
email,
|
||||
callbackUrl: `/auth/confirm-change-email`,
|
||||
}).catch(console.error);
|
||||
|
||||
// FIXME: There is not notification
|
||||
if (res?.sendVerifyChangeEmail) {
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.sent.change.email.hint'](),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
return !!res?.sendVerifyChangeEmail;
|
||||
},
|
||||
[pushNotification, searchParams, sendVerifyChangeEmail, t]
|
||||
);
|
||||
|
||||
const onSetPassword = useCallback(
|
||||
(password: string) => {
|
||||
changePassword({
|
||||
token: searchParams.get('token') || '',
|
||||
newPassword: password,
|
||||
}).catch(console.error);
|
||||
},
|
||||
[changePassword, searchParams]
|
||||
);
|
||||
const onOpenAffine = useCallback(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
switch (authType) {
|
||||
case 'signUp': {
|
||||
return (
|
||||
<SignUpPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'signIn': {
|
||||
return <SignInSuccessPage onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
case 'changePassword': {
|
||||
return (
|
||||
<ChangePasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'setPassword': {
|
||||
return (
|
||||
<SetPasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'changeEmail': {
|
||||
return (
|
||||
<ChangeEmailPage
|
||||
onChangeEmail={onSendVerifyChangeEmail}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'confirm-change-email': {
|
||||
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
if (args.params.authType === 'confirm-change-email') {
|
||||
const url = new URL(args.request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const token = searchParams.get('token');
|
||||
const res = await fetcher({
|
||||
query: changeEmailMutation,
|
||||
variables: {
|
||||
token: token || '',
|
||||
},
|
||||
}).catch(console.error);
|
||||
// TODO: Add error handling
|
||||
if (!res?.changeEmail) {
|
||||
return redirect('/expired');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
jumpToExpired(RouteLogic.REPLACE);
|
||||
}
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <AuthPage />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
39
packages/frontend/core/src/pages/desktop-signin.tsx
Normal file
39
packages/frontend/core/src/pages/desktop-signin.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { type LoaderFunction } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { signInCloud, signOutCloud } from '../utils/cloud-utils';
|
||||
|
||||
const supportedProvider = z.enum(['google']);
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const provider = searchParams.get('provider');
|
||||
const callback_url = searchParams.get('callback_url');
|
||||
if (!callback_url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
if (session) {
|
||||
// already signed in, need to sign out first
|
||||
await signOutCloud({
|
||||
callbackUrl: request.url, // retry
|
||||
});
|
||||
}
|
||||
|
||||
const maybeProvider = supportedProvider.safeParse(provider);
|
||||
if (maybeProvider.success) {
|
||||
const provider = maybeProvider.data;
|
||||
await signInCloud(provider, {
|
||||
callbackUrl: callback_url,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return null;
|
||||
};
|
||||
25
packages/frontend/core/src/pages/expired.tsx
Normal file
25
packages/frontend/core/src/pages/expired.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
export const Component = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const onOpenAffine = useCallback(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
return (
|
||||
<AuthPageContainer
|
||||
title={t['com.affine.expired.page.title']()}
|
||||
subtitle={t['com.affine.expired.page.subtitle']()}
|
||||
>
|
||||
<Button type="primary" size="large" onClick={onOpenAffine}>
|
||||
{t['com.affine.auth.open.affine']()}
|
||||
</Button>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
};
|
||||
88
packages/frontend/core/src/pages/index.tsx
Normal file
88
packages/frontend/core/src/pages/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { lazy } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||
|
||||
const AllWorkspaceModals = lazy(() =>
|
||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
||||
default: AllWorkspaceModals,
|
||||
}))
|
||||
);
|
||||
|
||||
const logger = new DebugLogger('index-page');
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
const rootStore = getCurrentStore();
|
||||
const { createFirstAppData } = await import('../bootstrap/setup');
|
||||
createFirstAppData(rootStore);
|
||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
const lastId = localStorage.getItem('last_workspace_id');
|
||||
const lastPageId = localStorage.getItem('last_page_id');
|
||||
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
|
||||
if (target) {
|
||||
const targetWorkspace = getWorkspace(target.id);
|
||||
|
||||
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
|
||||
({ trash }) => !trash
|
||||
);
|
||||
const helloWorldPage = nonTrashPages.find(({ jumpOnce }) => jumpOnce)?.id;
|
||||
const pageId =
|
||||
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
|
||||
nonTrashPages.at(0)?.id;
|
||||
if (helloWorldPage) {
|
||||
logger.debug(
|
||||
'Found target workspace. Jump to hello world page',
|
||||
helloWorldPage
|
||||
);
|
||||
return redirect(`/workspace/${targetWorkspace.id}/${helloWorldPage}`);
|
||||
} else if (pageId) {
|
||||
logger.debug('Found target workspace. Jump to page', pageId);
|
||||
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
|
||||
} else {
|
||||
logger.debug('Found target workspace. Jump to all page');
|
||||
return redirect(`/workspace/${targetWorkspace.id}/all`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
// TODO: We need a no workspace page
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: true,
|
||||
}}
|
||||
items={<UserWithWorkspaceList />}
|
||||
contentOptions={{
|
||||
style: {
|
||||
width: 300,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: '16px 12px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
</Menu>
|
||||
</div>
|
||||
<AllWorkspaceModals />
|
||||
</>
|
||||
);
|
||||
};
|
||||
101
packages/frontend/core/src/pages/invite.tsx
Normal file
101
packages/frontend/core/src/pages/invite.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { AcceptInvitePage } from '@affine/component/member-components';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
type GetInviteInfoQuery,
|
||||
getInviteInfoQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { type LoaderFunction, 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 { useAppHelper } from '../hooks/use-workspaces';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const inviteId = args.params.inviteId || '';
|
||||
const res = await fetcher({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
}).catch(console.error);
|
||||
|
||||
// If the inviteId is invalid, redirect to 404 page
|
||||
if (!res || !res?.getInviteInfo) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
// No mater sign in or not, we need to accept the invite
|
||||
await fetcher({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: res.getInviteInfo.workspace.id,
|
||||
inviteId,
|
||||
sendAcceptMail: true,
|
||||
},
|
||||
}).catch(console.error);
|
||||
|
||||
return {
|
||||
inviteId,
|
||||
inviteInfo: res.getInviteInfo,
|
||||
};
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
const { addCloudWorkspace } = useAppHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
|
||||
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
const { inviteInfo } = useLoaderData() as {
|
||||
inviteId: string;
|
||||
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||
};
|
||||
|
||||
const openWorkspace = useCallback(() => {
|
||||
addCloudWorkspace(inviteInfo.workspace.id);
|
||||
jumpToSubPath(
|
||||
inviteInfo.workspace.id,
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
}, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
// We can not pass function to navigate state, so we need to save it in atom
|
||||
setOnceSignedInEvent(openWorkspace);
|
||||
jumpToSignIn(RouteLogic.REPLACE, {
|
||||
state: {
|
||||
callbackURL: `/workspace/${inviteInfo.workspace.id}/all`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
inviteInfo.workspace.id,
|
||||
jumpToSignIn,
|
||||
loginStatus,
|
||||
openWorkspace,
|
||||
setAuthAtom,
|
||||
setOnceSignedInEvent,
|
||||
]);
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return (
|
||||
<AcceptInvitePage
|
||||
inviteInfo={inviteInfo}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
58
packages/frontend/core/src/pages/open-app.css.ts
Normal file
58
packages/frontend/core/src/pages/open-app.css.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const affineLogo = style({
|
||||
color: 'inherit',
|
||||
});
|
||||
|
||||
export const topNav = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 120px',
|
||||
});
|
||||
|
||||
export const topNavLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
});
|
||||
|
||||
export const topNavLink = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
|
||||
export const tryAgainLink = style({
|
||||
color: 'var(--affine-link-color)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
|
||||
export const centerContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 40,
|
||||
});
|
||||
|
||||
export const prompt = style({
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
});
|
||||
204
packages/frontend/core/src/pages/open-app.tsx
Normal file
204
packages/frontend/core/src/pages/open-app.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { type GetCurrentUserQuery, getCurrentUserQuery } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type LoaderFunction,
|
||||
useLoaderData,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import * as styles from './open-app.css';
|
||||
|
||||
let lastOpened = '';
|
||||
|
||||
const appSchemas = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
type Schema = z.infer<typeof appSchemas>;
|
||||
type Channel = z.infer<typeof appChannelSchema>;
|
||||
|
||||
const schemaToChanel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Schema, Channel>;
|
||||
|
||||
const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
interface OpenAppProps {
|
||||
urlToOpen?: string | null;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
interface LoaderData {
|
||||
action: 'url' | 'signin-redirect';
|
||||
currentUser?: GetCurrentUserQuery['currentUser'];
|
||||
}
|
||||
|
||||
const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const openDownloadLink = useCallback(() => {
|
||||
const url = `https://affine.pro/download?channel=${channel}`;
|
||||
open(url, '_blank');
|
||||
}, [channel]);
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
const [params] = useSearchParams();
|
||||
const autoOpen = useMemo(() => params.get('open') !== 'false', [params]);
|
||||
|
||||
if (urlToOpen && lastOpened !== urlToOpen && autoOpen) {
|
||||
lastOpened = urlToOpen;
|
||||
open(urlToOpen, '_blank');
|
||||
}
|
||||
|
||||
if (!urlToOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.topNav}>
|
||||
<a href="/" rel="noreferrer" className={styles.affineLogo}>
|
||||
<Logo1Icon width={24} height={24} />
|
||||
</a>
|
||||
|
||||
<div className={styles.topNavLinks}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Official Website
|
||||
</a>
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
AFFiNE Community
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/blog"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/about-us"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button onClick={openDownloadLink}>
|
||||
{t['com.affine.auth.open.affine.download-app']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.centerContent}>
|
||||
<img src={appIcon} alt={appName} width={120} height={120} />
|
||||
|
||||
<div className={styles.prompt}>
|
||||
<Trans i18nKey="com.affine.auth.open.affine.prompt">
|
||||
Open {appName} app now
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className={styles.tryAgainLink}
|
||||
href={urlToOpen}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.try-again']()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenUrl = () => {
|
||||
const [params] = useSearchParams();
|
||||
const urlToOpen = useMemo(() => params.get('url'), [params]);
|
||||
const channel = useMemo(() => {
|
||||
const urlObj = new URL(urlToOpen || '');
|
||||
const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', ''));
|
||||
return schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine'];
|
||||
}, [urlToOpen]);
|
||||
|
||||
return <OpenAppImpl urlToOpen={urlToOpen} channel={channel} />;
|
||||
};
|
||||
|
||||
const OpenOAuthJwt = () => {
|
||||
const { currentUser } = useLoaderData() as LoaderData;
|
||||
const [params] = useSearchParams();
|
||||
const schema = useMemo(() => {
|
||||
const maybeSchema = appSchemas.safeParse(params.get('schema'));
|
||||
return maybeSchema.success ? maybeSchema.data : 'affine';
|
||||
}, [params]);
|
||||
const channel = schemaToChanel[schema as Schema];
|
||||
|
||||
if (!currentUser || !currentUser?.token?.sessionToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlToOpen = `${schema}://signin-redirect?token=${currentUser.token.sessionToken}`;
|
||||
|
||||
return <OpenAppImpl urlToOpen={urlToOpen} channel={channel} />;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const { action } = useLoaderData() as LoaderData;
|
||||
|
||||
if (action === 'url') {
|
||||
return <OpenUrl />;
|
||||
} else if (action === 'signin-redirect') {
|
||||
return <OpenOAuthJwt />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const action = args.params.action || '';
|
||||
const res = await fetcher({
|
||||
query: getCurrentUserQuery,
|
||||
}).catch(console.error);
|
||||
|
||||
return {
|
||||
action,
|
||||
currentUser: res?.currentUser || null,
|
||||
};
|
||||
};
|
||||
90
packages/frontend/core/src/pages/share/detail-page.tsx
Normal file
90
packages/frontend/core/src/pages/share/detail-page.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { noop } from 'foxact/noop';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
useRouteError,
|
||||
} from 'react-router-dom';
|
||||
import { applyUpdate } from 'yjs';
|
||||
|
||||
import { PageDetailEditor } from '../../adapters/shared';
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||
|
||||
function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
|
||||
if (!(value instanceof ArrayBuffer)) {
|
||||
throw new Error('value is not ArrayBuffer');
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new DebugLogger('public:share-page');
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
const workspaceId = params?.workspaceId;
|
||||
const pageId = params?.pageId;
|
||||
if (!workspaceId || !pageId) {
|
||||
return redirect('/404');
|
||||
}
|
||||
const workspace = getOrCreateWorkspace(
|
||||
workspaceId,
|
||||
WorkspaceFlavour.AFFINE_PUBLIC
|
||||
);
|
||||
// download root workspace
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(workspace.doc, new Uint8Array(buffer));
|
||||
}
|
||||
const page = workspace.getPage(pageId);
|
||||
assertExists(page, 'cannot find page');
|
||||
// download page
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(
|
||||
workspaceId,
|
||||
page.spaceDoc.guid
|
||||
);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(page.spaceDoc, new Uint8Array(buffer));
|
||||
}
|
||||
logger.info('workspace', workspace);
|
||||
workspace.awarenessStore.setReadonly(page, true);
|
||||
return page;
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
const page = useLoaderData() as Page;
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<PageDetailEditor
|
||||
isPublic
|
||||
workspace={page.workspace}
|
||||
pageId={page.id}
|
||||
onInit={noop}
|
||||
onLoad={useCallback(() => noop, [])}
|
||||
/>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
return isRouteErrorResponse(error) ? (
|
||||
<h1>
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
) : (
|
||||
<SharePageNotFoundError />
|
||||
);
|
||||
}
|
||||
70
packages/frontend/core/src/pages/sign-in.tsx
Normal file
70
packages/frontend/core/src/pages/sign-in.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { AuthPanel } from '../components/affine/auth';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
interface LocationState {
|
||||
state?: {
|
||||
callbackURL?: string;
|
||||
};
|
||||
}
|
||||
export const Component = () => {
|
||||
const [{ state, email = '', emailType = 'changePassword' }, setAuthAtom] =
|
||||
useAtom(authAtom);
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const location = useLocation() as LocationState;
|
||||
const navigate = useNavigate();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'authenticated') {
|
||||
if (location.state?.callbackURL) {
|
||||
navigate(location.state.callbackURL, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
jumpToIndex,
|
||||
location.state?.callbackURL,
|
||||
loginStatus,
|
||||
navigate,
|
||||
setAuthAtom,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SignInPageContainer>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
emailType={emailType}
|
||||
setEmailType={useCallback(
|
||||
emailType => {
|
||||
setAuthAtom(prev => ({ ...prev, emailType }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
setAuthState={useCallback(
|
||||
state => {
|
||||
setAuthAtom(prev => ({ ...prev, state }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
setAuthEmail={useCallback(
|
||||
email => {
|
||||
setAuthAtom(prev => ({ ...prev, email }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
/>
|
||||
</SignInPageContainer>
|
||||
);
|
||||
};
|
||||
68
packages/frontend/core/src/pages/workspace/all-page.tsx
Normal file
68
packages/frontend/core/src/pages/workspace/all-page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useCollectionManager } from '@affine/component/page-list';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { currentCollectionsAtom } from '../../utils/user-setting';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const rootStore = getCurrentStore();
|
||||
const workspaceId = args.params.workspaceId;
|
||||
assertExists(workspaceId);
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
||||
const workspace = await rootStore.get(workspaceAtom);
|
||||
for (const pageId of workspace.pages.keys()) {
|
||||
const page = workspace.getPage(pageId);
|
||||
if (page && page.meta.jumpOnce) {
|
||||
workspace.meta.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
return redirect(`/workspace/${workspace.id}/${page.id}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const AllPage = () => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const setting = useCollectionManager(currentCollectionsAtom);
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
if (newTab) {
|
||||
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
|
||||
} else {
|
||||
jumpToPage(currentWorkspace.id, pageId);
|
||||
}
|
||||
},
|
||||
[currentWorkspace, jumpToPage]
|
||||
);
|
||||
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
currentWorkspaceId={currentWorkspace.id}
|
||||
currentEntry={{
|
||||
subPath: WorkspaceSubPath.ALL,
|
||||
}}
|
||||
/>
|
||||
<PageList
|
||||
collection={setting.currentCollection}
|
||||
onOpenPage={onClickPage}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <AllPage />;
|
||||
};
|
||||
149
packages/frontend/core/src/pages/workspace/detail-page.tsx
Normal file
149
packages/frontend/core/src/pages/workspace/detail-page.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import {
|
||||
createTagFilter,
|
||||
useCollectionManager,
|
||||
} from '@affine/component/page-list';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { setPageModeAtom } from '../../atoms';
|
||||
import { currentModeAtom } from '../../atoms/mode';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { currentCollectionsAtom } from '../../utils/user-setting';
|
||||
|
||||
const DetailPageImpl = (): ReactElement => {
|
||||
const { openPage, jumpToSubPath } = useNavigateHelper();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
assertExists(currentWorkspace);
|
||||
assertExists(currentPageId);
|
||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||
const collectionManager = useCollectionManager(currentCollectionsAtom);
|
||||
const mode = useAtomValue(currentModeAtom);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
|
||||
const onLoad = useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
try {
|
||||
const surfaceBlock = page.getBlockByFlavour('affine:surface')[0];
|
||||
// hotfix for old page
|
||||
if (
|
||||
surfaceBlock &&
|
||||
(surfaceBlock.yBlock.get('prop:elements') as YMap<any>).get(
|
||||
'type'
|
||||
) !== '$blocksuite:internal:native$'
|
||||
) {
|
||||
globalBlockSuiteSchema.upgradePage(
|
||||
0,
|
||||
{
|
||||
'affine:surface': 3,
|
||||
},
|
||||
page.spaceDoc
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
setPageMode(currentPageId, mode);
|
||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
});
|
||||
const disposeTagClick = editor.slots.tagClicked.on(async ({ tagId }) => {
|
||||
jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
|
||||
collectionManager.backToAll();
|
||||
collectionManager.setTemporaryFilter([createTagFilter(tagId)]);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
disposeTagClick.dispose();
|
||||
};
|
||||
},
|
||||
[
|
||||
blockSuiteWorkspace.id,
|
||||
collectionManager,
|
||||
currentPageId,
|
||||
currentWorkspace.id,
|
||||
jumpToSubPath,
|
||||
mode,
|
||||
openPage,
|
||||
setPageMode,
|
||||
]
|
||||
);
|
||||
|
||||
const { PageDetail, Header } = getUIAdapter(currentWorkspace.flavour);
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
currentWorkspaceId={currentWorkspace.id}
|
||||
currentEntry={{
|
||||
pageId: currentPageId,
|
||||
}}
|
||||
/>
|
||||
<PageDetail
|
||||
currentWorkspaceId={currentWorkspace.id}
|
||||
currentPageId={currentPageId}
|
||||
onLoadEditor={onLoad}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailPage = (): ReactElement => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const page = currentPageId
|
||||
? currentWorkspace.blockSuiteWorkspace.getPage(currentPageId)
|
||||
: null;
|
||||
|
||||
if (!currentPageId || !page) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
return <DetailPageImpl />;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const rootStore = getCurrentStore();
|
||||
rootStore.set(contentLayoutAtom, 'editor');
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
const currentWorkspace = await rootStore.get(currentWorkspaceAtom);
|
||||
if (args.params.pageId) {
|
||||
const pageId = args.params.pageId;
|
||||
localStorage.setItem('last_page_id', pageId);
|
||||
const page = currentWorkspace.getPage(pageId);
|
||||
if (!page) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (page.meta.jumpOnce) {
|
||||
currentWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
}
|
||||
rootStore.set(currentPageIdAtom, pageId);
|
||||
} else {
|
||||
return redirect('/404');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <DetailPage />;
|
||||
};
|
||||
59
packages/frontend/core/src/pages/workspace/index.tsx
Normal file
59
packages/frontend/core/src/pages/workspace/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
type LoaderFunction,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const rootStore = getCurrentStore();
|
||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
const currentMetadata = meta.find(({ id }) => id === args.params.workspaceId);
|
||||
if (!currentMetadata) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
if (!args.params.pageId) {
|
||||
rootStore.set(currentPageIdAtom, null);
|
||||
}
|
||||
if (currentMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(currentMetadata.id);
|
||||
const workspace = await rootStore.get(workspaceAtom);
|
||||
return (() => {
|
||||
const blockVersions = workspace.meta.blockVersions;
|
||||
if (!blockVersions) {
|
||||
return true;
|
||||
}
|
||||
for (const [flavour, schema] of workspace.schema.flavourSchemaMap) {
|
||||
if (blockVersions[flavour] !== schema.version) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
const incompatible = useLoaderData();
|
||||
return (
|
||||
<WorkspaceLayout incompatible={!!incompatible}>
|
||||
<Outlet />
|
||||
</WorkspaceLayout>
|
||||
);
|
||||
};
|
||||
47
packages/frontend/core/src/pages/workspace/trash-page.tsx
Normal file
47
packages/frontend/core/src/pages/workspace/trash-page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
|
||||
export const TrashPage = () => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
if (newTab) {
|
||||
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
|
||||
} else {
|
||||
jumpToPage(currentWorkspace.id, pageId);
|
||||
}
|
||||
},
|
||||
[currentWorkspace, jumpToPage]
|
||||
);
|
||||
// todo(himself65): refactor to plugin
|
||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||
assertExists(blockSuiteWorkspace);
|
||||
const { Header } = getUIAdapter(currentWorkspace.flavour);
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
currentWorkspaceId={currentWorkspace.id}
|
||||
currentEntry={{
|
||||
subPath: WorkspaceSubPath.TRASH,
|
||||
}}
|
||||
/>
|
||||
<BlockSuitePageList
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
onOpenPage={onClickPage}
|
||||
listType="trash"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <TrashPage />;
|
||||
};
|
||||
Reference in New Issue
Block a user