refactor!: next generation AFFiNE code structure (#1176)

This commit is contained in:
Himself65
2023-03-01 01:40:01 -06:00
committed by GitHub
parent 2dcccc772c
commit e0481d29ad
270 changed files with 8308 additions and 6829 deletions

View File

@@ -1,13 +1,56 @@
import Head from 'next/head';
import { Button, displayFlex, styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import Image from 'next/image';
import { useRouter } from 'next/router';
import React from 'react';
import { Helmet } from 'react-helmet-async';
import NotfoundPage from '@/components/404';
import ErrorImg from '../../public/imgs/invite-error.svg';
export const StyledContainer = styled.div(() => {
return {
...displayFlex('center', 'center'),
flexDirection: 'column',
height: '100vh',
img: {
width: '360px',
height: '270px',
},
p: {
fontSize: '22px',
fontWeight: 600,
margin: '24px 0',
},
};
});
export const NotfoundPage = () => {
const { t } = useTranslation();
const router = useRouter();
return (
<StyledContainer data-testid="notFound">
<Image alt="404" src={ErrorImg}></Image>
<p>{t('404 - Page Not Found')}</p>
<Button
shape="round"
onClick={() => {
router.push('/');
}}
>
{t('Back Home')}
</Button>
</StyledContainer>
);
};
export default function Custom404() {
return (
<>
<Head>
<title>404 - AFFiNE</title>
</Head>
<Helmet>
s<title>404 - AFFiNE</title>
</Helmet>
<NotfoundPage></NotfoundPage>
</>
);

View File

@@ -1,107 +1,96 @@
import '../../public/globals.css';
import './temporary.css';
import '@fontsource/space-mono';
import '@fontsource/poppins';
import '../utils/print-build-info';
import '@affine/i18n';
import '@blocksuite/editor/themes/affine.css';
import '../styles/globals.css';
import { useTranslation } from '@affine/i18n';
import { Logger } from '@toeverything/pathfinder-logger';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import Head from 'next/head';
// import AppStateProvider2 from '@/providers/app-state-provider2/provider';
import { appWithTranslation, createI18n, I18nextProvider } from '@affine/i18n';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { Provider } from 'jotai';
import { useAtomsDebugValue } from 'jotai-devtools';
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import { Suspense, useEffect } from 'react';
import React from 'react';
import React, { memo, ReactElement, Suspense, useEffect, useMemo } from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig, SWRConfiguration } from 'swr';
import { PageLoading } from '@/components/loading';
import { MessageCenterHandler } from '@/components/message-center-handler';
import ProviderComposer from '@/components/provider-composer';
import { AppStateProvider } from '@/providers/app-state-provider';
import ConfirmProvider from '@/providers/ConfirmProvider';
import { ThemeProvider } from '@/providers/ThemeProvider';
import { GlobalAppProvider } from '@/store/app';
import { DataCenterPreloader } from '@/store/app/datacenter';
import { ModalProvider } from '@/store/globalModal';
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
P,
IP
> & {
getLayout?: (page: ReactElement) => ReactNode;
};
import { jotaiStore } from '../atoms';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
import { ProviderComposer } from '../components/provider-composer';
import { PageLoading } from '../components/pure/loading';
import { AffineSWRConfigProvider } from '../providers/AffineSWRConfigProvider';
import { ModalProvider } from '../providers/ModalProvider';
import { ThemeProvider } from '../providers/ThemeProvider';
import { NextPageWithLayout } from '../shared';
import { config } from '../shared/env';
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
// Page list which do not rely on app state
const NoNeedAppStatePageList = [
'/404',
'/public-workspace/[workspaceId]',
'/public-workspace/[workspaceId]/[pageId]',
];
const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const getLayout = Component.getLayout || (page => page);
const { i18n } = useTranslation();
const router = useRouter();
const EmptyLayout = (page: ReactElement) => page;
React.useEffect(() => {
document.documentElement.lang = i18n.language;
}, [i18n.language]);
const DebugAtoms = memo(function DebugAtoms() {
useAtomsDebugValue();
return null;
});
const helmetContext = {};
const defaultSWRConfig: SWRConfiguration = {
suspense: true,
fetcher: () => {
const error = new Error(
'you might forget to warp your page with AffineSWRConfigProvider'
);
console.log(error);
throw error;
},
};
const cache = createCache({ key: 'affine' });
const App = function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || EmptyLayout;
const i18n = useMemo(() => createI18n(), []);
if (process.env.NODE_ENV === 'development') {
// I know what I'm doing
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
console.log('Runtime Preset', config);
}, []);
}
return (
<>
<Head>
<meta name="theme-color" content="#fafafa" />
<link rel="manifest" href="/manifest.json" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/apple-touch-icon.png"
/>
<title>AFFiNE</title>
</Head>
<Logger />
<GlobalAppProvider key="BlockSuiteProvider">
<ProviderComposer
contexts={[
<ThemeProvider key="ThemeProvider" />,
<AppStateProvider key="appStateProvider" />,
<ModalProvider key="ModalProvider" />,
<ConfirmProvider key="ConfirmProvider" />,
]}
>
{NoNeedAppStatePageList.includes(router.route) ? (
getLayout(<Component {...pageProps} />)
) : (
<Suspense fallback={<PageLoading />}>
<DataCenterPreloader>
<MessageCenterHandler>
<AppDefender>
{getLayout(<Component {...pageProps} />)}
</AppDefender>
</MessageCenterHandler>
</DataCenterPreloader>
<I18nextProvider i18n={i18n}>
<CacheProvider value={cache}>
<DebugAtoms />
<SWRConfig value={defaultSWRConfig}>
<AffineErrorBoundary router={useRouter()}>
<Suspense fallback={<PageLoading key="RootPageLoading" />}>
<ProviderComposer
contexts={useMemo(
() => [
<AffineSWRConfigProvider key="AffineSWRConfigProvider" />,
<Provider key="JotaiProvider" store={jotaiStore} />,
<ThemeProvider key="ThemeProvider" />,
<ModalProvider key="ModalProvider" />,
],
[]
)}
>
<HelmetProvider key="HelmetProvider" context={helmetContext}>
<Helmet>
<title>AFFiNE</title>
</Helmet>
{getLayout(<Component {...pageProps} />)}
</HelmetProvider>
</ProviderComposer>
</Suspense>
)}
</ProviderComposer>
</GlobalAppProvider>
</>
</AffineErrorBoundary>
</SWRConfig>
</CacheProvider>
</I18nextProvider>
);
};
const AppDefender = ({ children }: PropsWithChildren) => {
const router = useRouter();
useEffect(() => {
if (['/index.html', '/'].includes(router.asPath)) {
router.replace('/workspace');
}
}, [router]);
return <>{children}</>;
};
export default App;
export default appWithTranslation(App as any);

View File

@@ -1,47 +1,20 @@
import { cache } from '@emotion/css';
import createEmotionServer from '@emotion/server/create-instance';
import Document, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document';
import Document, { Head, Html, Main, NextScript } from 'next/document';
import * as React from 'react';
export const renderStatic = async (html: string) => {
if (html === undefined) {
throw new Error('did you forget to return html from renderToString?');
}
const { extractCritical } = createEmotionServer(cache);
const { ids, css } = extractCritical(html);
return { html, ids, css };
};
export default class AppDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const page = await ctx.renderPage();
const { css, ids } = await renderStatic(page.html);
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<React.Fragment>
{initialProps.styles}
<style
data-emotion={`css ${ids.join(' ')}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
</React.Fragment>
),
};
}
render() {
return (
<Html>
<Head />
<Head>
<meta name="theme-color" content="#fafafa" />
<link rel="manifest" href="/manifest.json" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link rel="icon" sizes="192x192" href="/chrome-192x192.png" />
</Head>
<body>
<Main />
<NextScript />

View File

@@ -1,7 +1,75 @@
import type { NextPage } from 'next';
import { useAtom } from 'jotai';
import { NextPage } from 'next';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
const Home: NextPage = () => {
return <div title="Home Page"></div>;
import { currentWorkspaceIdAtom } from '../atoms';
import { PageLoading } from '../components/pure/loading';
import { refreshDataCenter, useWorkspaces } from '../hooks/use-workspaces';
const IndexPage: NextPage = () => {
const router = useRouter();
useEffect(() => {
const controller = new AbortController();
refreshDataCenter(controller.signal);
return () => {
controller.abort();
};
}, []);
const [workspaceId] = useAtom(currentWorkspaceIdAtom);
const workspaces = useWorkspaces();
useEffect(() => {
if (!router.isReady) {
return;
}
const targetWorkspace = workspaces.find(w => w.id === workspaceId);
if (workspaceId && targetWorkspace) {
const pageId =
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (pageId) {
router.replace({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId,
pageId,
},
});
return;
} else {
router.replace({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId,
},
});
return;
}
}
const firstWorkspace = workspaces.at(0);
if (firstWorkspace) {
const pageId =
firstWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (pageId) {
router.replace({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: firstWorkspace.id,
pageId,
},
});
return;
} else {
router.replace({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: firstWorkspace.id,
},
});
return;
}
}
}, [router, workspaceId, workspaces]);
return <PageLoading />;
};
export default Home;
export default IndexPage;

View File

@@ -1,104 +0,0 @@
import { displayFlex, styled } from '@affine/component';
import { Button } from '@affine/component';
import { Permission } from '@affine/datacenter';
import {
SucessfulDuotoneIcon,
UnsucessfulDuotoneIcon,
} from '@blocksuite/icons';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { PageLoading } from '@/components/loading';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useGlobalState } from '@/store/app';
import inviteError from '../../../public/imgs/invite-error.svg';
import inviteSuccess from '../../../public/imgs/invite-success.svg';
export default function DevPage() {
const [loading, setLoading] = useState(true);
const router = useRouter();
const [inviteData, setInviteData] = useState<Permission | null>(null);
const { acceptInvite } = useWorkspaceHelper();
const dataCenter = useGlobalState(store => store.dataCenter);
useEffect(() => {
const init = async () => {
const data = await dataCenter.acceptInvitation(
router.query.invite_code as string
);
setInviteData(data as Permission);
setLoading(false);
};
init();
}, [router, acceptInvite, dataCenter]);
if (loading) {
return <PageLoading />;
}
if (inviteData?.accepted) {
return (
<StyledContainer>
<Image src={inviteSuccess} alt="" />
<Button
type="primary"
shape="round"
onClick={() => {
router.push(`/workspace/${inviteData?.workspace_id}/all`);
}}
>
Go to Workspace
</Button>
<p>
<SucessfulDuotoneIcon />
Successfully joined
</p>
</StyledContainer>
);
}
if (inviteData?.accepted === false) {
return (
<StyledContainer>
<Image src={inviteError} alt="" />
<Button
shape="round"
onClick={() => {
router.push(`/`);
}}
>
Back to Home
</Button>
<p>
<UnsucessfulDuotoneIcon />
The link has expired
</p>
</StyledContainer>
);
}
}
const StyledContainer = styled('div')(({ theme }) => {
return {
height: '100vh',
...displayFlex('center', 'center'),
flexDirection: 'column',
backgroundColor: theme.colors.pageBackground,
img: {
width: '300px',
height: '300px',
},
p: {
...displayFlex('center', 'center'),
marginTop: '24px',
svg: {
color: theme.colors.primaryColor,
fontSize: '24px',
marginRight: '12px',
},
},
};
});

View File

@@ -0,0 +1,125 @@
import {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
NextPage,
} from 'next';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { PageDetailEditor } from '../../components/page-detail-editor';
import { PageLoading } from '../../components/pure/loading';
import { StyledPage, StyledWrapper } from '../../layouts/styles';
import { BlockSuiteWorkspace } from '../../shared';
import { createEmptyBlockSuiteWorkspace } from '../../utils';
export type PreviewPageProps = {
text: string;
title: string;
};
export type PreviewPageParams = {
previewId: string;
};
const PreviewPage: NextPage<PreviewPageProps> = ({
text,
title,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
const [blockSuiteWorkspace, setBlockSuiteWorkspace] =
useState<BlockSuiteWorkspace | null>(null);
useEffect(() => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace('preview');
blockSuiteWorkspace.signals.pageAdded.once(() => {
setBlockSuiteWorkspace(blockSuiteWorkspace);
});
blockSuiteWorkspace.createPage('preview');
return () => {
blockSuiteWorkspace.removePage('preview');
};
}, []);
if (!blockSuiteWorkspace || !blockSuiteWorkspace.getPage('preview')) {
return <PageLoading />;
}
return (
<>
<Head>
<title>{title}</title>
</Head>
<StyledPage>
<StyledWrapper>
<PageDetailEditor
blockSuiteWorkspace={blockSuiteWorkspace}
pageId="preview"
onInit={(page, editor) => {
blockSuiteWorkspace.setPageMeta(page.id, { title });
const pageBlockId = page.addBlockByFlavour('affine:page', {
title,
});
page.addBlockByFlavour('affine:surface', {}, null);
const frameId = page.addBlockByFlavour(
'affine:frame',
{},
pageBlockId
);
page.addBlockByFlavour('affine:paragraph', {}, frameId);
editor.clipboard.importMarkdown(text, frameId).then(() => {
page.resetHistory();
});
}}
/>
</StyledWrapper>
</StyledPage>
</>
);
};
export default PreviewPage;
export const getStaticProps: GetStaticProps<
PreviewPageProps,
PreviewPageParams
> = async context => {
const name = context.params?.previewId;
const fs = await import('node:fs/promises');
const path = await import('node:path');
const markdown: string = await fs.readFile(
path.resolve(process.cwd(), 'src', 'templates', `${name}.md`),
'utf8'
);
const title = markdown
.split('\n')
.splice(0, 1)
.join('')
.replaceAll('#', '')
.trim();
if (!name) {
return {
redirect: {
destination: '/404',
},
props: {
text: '',
title: '',
},
};
}
return {
props: {
text: markdown.split('\n').slice(1).join('\n'),
title,
},
};
};
export const getStaticPaths: GetStaticPaths<PreviewPageParams> = () => {
return {
paths: [
{ params: { previewId: 'AFFiNE-Docs' } },
{ params: { previewId: 'Welcome-to-AFFiNE-Abbey-Alpha-Wood' } },
{ params: { previewId: 'Welcome-to-AFFiNE-Alpha-Downhills' } },
{ params: { previewId: 'Welcome-to-the-AFFiNE-Alpha' } },
],
fallback: false,
};
};

View File

@@ -0,0 +1,77 @@
import { useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import React, { Suspense, useCallback, useEffect } from 'react';
import { currentWorkspaceIdAtom } from '../../atoms';
import {
publicBlockSuiteAtom,
publicWorkspaceIdAtom,
} from '../../atoms/public-workspace';
import { QueryParamError } from '../../components/affine/affine-error-eoundary';
import { BlockSuitePublicPageList } from '../../components/blocksuite/block-suite-page-list';
import { PageLoading } from '../../components/pure/loading';
import { WorkspaceLayout } from '../../layouts';
import { NextPageWithLayout } from '../../shared';
const ListPageInner: React.FC<{
workspaceId: string;
}> = ({ workspaceId }) => {
const router = useRouter();
const blockSuiteWorkspace = useAtomValue(publicBlockSuiteAtom);
const handleClickPage = useCallback(
(pageId: string) => {
return router.push({
pathname: `/public-workspace/[workspaceId]/[pageId]`,
query: {
workspaceId,
pageId,
},
});
},
[router, workspaceId]
);
if (!blockSuiteWorkspace) {
return <PageLoading />;
}
return (
<BlockSuitePublicPageList
onOpenPage={handleClickPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
};
// This is affine only page, so we don't need to dynamic use WorkspacePlugin
const ListPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceId = router.query.workspaceId;
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (typeof workspaceId === 'string') {
setWorkspaceId(workspaceId);
setCurrentWorkspaceId(workspaceId);
}
}, [router.isReady, setCurrentWorkspaceId, setWorkspaceId, workspaceId]);
const value = useAtomValue(publicWorkspaceIdAtom);
if (!router.isReady || !value) {
return <PageLoading />;
}
if (typeof workspaceId !== 'string') {
throw new QueryParamError('workspaceId', workspaceId);
}
return (
<Suspense fallback={<PageLoading />}>
<ListPageInner workspaceId={workspaceId} />
</Suspense>
);
};
export default ListPage;
ListPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -1,117 +1,27 @@
import { displayFlex, styled } from '@affine/component';
import { Breadcrumbs } from '@affine/component';
import { IconButton } from '@affine/component';
import {
Breadcrumbs,
displayFlex,
IconButton,
styled,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { PaperIcon, SearchIcon } from '@blocksuite/icons';
import dynamic from 'next/dynamic';
import NextLink from 'next/link';
import { PaperIcon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ReactElement, useEffect, useMemo } from 'react';
import React, { Suspense, useEffect } from 'react';
import { PageLoading } from '@/components/loading';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useLoadPublicWorkspace } from '@/hooks/use-load-public-workspace';
import { useModal } from '@/store/globalModal';
import {
publicBlockSuiteAtom,
publicWorkspaceIdAtom,
} from '../../../atoms/public-workspace';
import { QueryParamError } from '../../../components/affine/affine-error-eoundary';
import { PageDetailEditor } from '../../../components/page-detail-editor';
import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceLayout } from '../../../layouts';
import { NextPageWithLayout } from '../../../shared';
import type { NextPageWithLayout } from '../..//_app';
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
ssr: false,
});
const Page: NextPageWithLayout = () => {
const router = useRouter();
const { workspaceId, pageId } = router.query as Record<string, string>;
const { status, workspace: workspaceUnit } =
useLoadPublicWorkspace(workspaceId);
const { triggerQuickSearchModal } = useModal();
const { t } = useTranslation();
const page = useMemo(() => {
if (workspaceUnit?.blocksuiteWorkspace) {
return workspaceUnit.blocksuiteWorkspace.getPage(pageId);
}
return null;
}, [workspaceUnit, pageId]);
const workspace = workspaceUnit?.blocksuiteWorkspace;
const pageTitle = page?.meta.title;
const workspaceName = workspace?.meta.name;
useEffect(() => {
const pageNotFound = workspace?.meta.pageMetas.every(p => p.id !== pageId);
if (workspace && pageNotFound) {
router.push('/404');
}
}, [workspace, router, pageId]);
useEffect(() => {
if (status === 'error') {
router.push('/404');
}
}, [router, status]);
if (status === 'loading') {
return <PageLoading />;
}
if (status === 'error') {
return null;
}
return (
<PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs href={`/public-workspace/${workspaceId}`}>
<WorkspaceUnitAvatar
size={24}
name={workspaceName}
workspaceUnit={workspaceUnit}
/>
<span>{workspaceName}</span>
</StyledBreadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${workspaceId}/${pageId}`}
>
<PaperIcon fontSize={24} />
<span>{pageTitle ? pageTitle : t('Untitled')}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
{workspace && page && (
<DynamicBlocksuite
page={page}
workspace={workspace}
setEditor={editor => {
editor.readonly = true;
}}
/>
)}
</PageContainer>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
return <div>{page}</div>;
};
export default Page;
export const PageContainer = styled.div(({ theme }) => {
return {
height: '100vh',
overflowY: 'auto',
backgroundColor: theme.colors.pageBackground,
};
});
export const NavContainer = styled.div(({ theme }) => {
return {
width: '100vw',
@@ -121,7 +31,8 @@ export const NavContainer = styled.div(({ theme }) => {
backgroundColor: theme.colors.pageBackground,
};
});
export const StyledBreadcrumbs = styled(NextLink)(({ theme }) => {
export const StyledBreadcrumbs = styled(Link)(({ theme }) => {
return {
flex: 1,
...displayFlex('center', 'center'),
@@ -139,6 +50,7 @@ export const StyledBreadcrumbs = styled(NextLink)(({ theme }) => {
},
};
});
export const SearchButton = styled(IconButton)(({ theme }) => {
return {
color: theme.colors.iconColor,
@@ -147,3 +59,89 @@ export const SearchButton = styled(IconButton)(({ theme }) => {
padding: '0 24px',
};
});
const PublicWorkspaceDetailPageInner: React.FC<{
pageId: string;
}> = ({ pageId }) => {
const blockSuiteWorkspace = useAtomValue(publicBlockSuiteAtom);
if (!blockSuiteWorkspace) {
throw new Error('cannot find workspace');
}
const { t } = useTranslation();
const name = blockSuiteWorkspace.meta.name;
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
return (
<>
<PageDetailEditor
pageId={pageId}
blockSuiteWorkspace={blockSuiteWorkspace}
onLoad={(_, editor) => {
editor.readonly = true;
}}
header={
<NavContainer
// fixme(himself65): this is a hack to make the breadcrumbs work
style={{
position: 'absolute',
left: '0',
}}
>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${blockSuiteWorkspace.room}`}
>
<WorkspaceAvatar
size={24}
name={name}
avatar={blockSuiteWorkspace.meta.avatar}
/>
<span>{name}</span>
</StyledBreadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${
blockSuiteWorkspace.room as string
}/${pageId}`}
>
<PaperIcon fontSize={24} />
<span>{pageTitle ? pageTitle : t('Untitled')}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
</NavContainer>
}
/>
</>
);
};
export const PublicWorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceId = router.query.workspaceId;
const pageId = router.query.pageId;
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (typeof workspaceId === 'string') {
setWorkspaceId(workspaceId);
}
}, [router.isReady, setWorkspaceId, workspaceId]);
const value = useAtomValue(publicWorkspaceIdAtom);
if (!router.isReady || !value) {
return <PageLoading />;
}
if (typeof workspaceId !== 'string' || typeof pageId !== 'string') {
throw new QueryParamError('workspaceId, pageId', workspaceId);
}
return (
<Suspense fallback={<PageLoading />}>
<PublicWorkspaceDetailPageInner pageId={pageId} />
</Suspense>
);
};
export default PublicWorkspaceDetailPage;
PublicWorkspaceDetailPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -1,83 +0,0 @@
import { Breadcrumbs } from '@affine/component';
import { SearchIcon } from '@blocksuite/icons';
import { useRouter } from 'next/router';
import { ReactElement, useEffect, useMemo } from 'react';
import { PageLoading } from '@/components/loading';
import { PageList } from '@/components/page-list';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useLoadPublicWorkspace } from '@/hooks/use-load-public-workspace';
import { PageMeta } from '@/providers/app-state-provider';
import { useModal } from '@/store/globalModal';
import {
NavContainer,
PageContainer,
SearchButton,
StyledBreadcrumbs,
} from './[pageId]';
const All = () => {
const router = useRouter();
const { triggerQuickSearchModal } = useModal();
const { status, workspace } = useLoadPublicWorkspace(
router.query.workspaceId as string
);
const pageList = useMemo(() => {
return (workspace?.blocksuiteWorkspace?.meta.pageMetas ?? []) as PageMeta[];
}, [workspace]);
const workspaceName = workspace?.blocksuiteWorkspace?.meta.name;
useEffect(() => {
if (status === 'error') {
router.push('/404');
}
}, [router, status]);
if (status === 'loading') {
return <PageLoading />;
}
if (status === 'error') {
return null;
}
return (
<PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}`}
>
<WorkspaceUnitAvatar
size={24}
name={workspaceName}
workspaceUnit={workspace}
/>
<span>{workspaceName}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
<PageList
pageList={pageList.filter(p => !p.trash)}
showFavoriteTag={false}
isPublic={true}
/>
</PageContainer>
);
};
All.getLayout = function getLayout(page: ReactElement) {
return <div>{page}</div>;
};
export default All;

View File

@@ -1,15 +0,0 @@
.affine-default-page-block-title-container {
margin-top: 78px;
margin-bottom: 40px;
}
.affine-default-page-block-container {
max-width: 686px;
}
affine-block-hub {
position: unset !important;
}
.block-hub-menu-container {
position: unset !important;
}

View File

@@ -1,152 +1,73 @@
import { useTranslation } from '@affine/i18n';
import { assertEquals } from '@blocksuite/store';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useRouter } from 'next/router';
import {
PropsWithChildren,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import React, { useEffect, useState } from 'react';
import { EditorHeader } from '@/components/header';
import MobileModal from '@/components/mobile-modal';
import WorkspaceLayout from '@/components/workspace-layout';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useGlobalState, useGlobalStateApi } from '@/store/app';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useLoadWorkspace } from '../../../hooks/use-load-workspace';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins';
import { NextPageWithLayout, RemWorkspaceFlavour } from '../../../shared';
import type { NextPageWithLayout } from '../..//_app';
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
ssr: false,
});
const BlockHubAppender = () => {
const setBlockHub = useGlobalState(store => store.setBlockHub);
const editor = useGlobalState(store => store.editor);
const WorkspaceDetail: React.FC = () => {
const [pageId] = useCurrentPageId();
const [currentWorkspace] = useCurrentWorkspace();
const [, rerender] = useState(false);
// fixme(himself65): this is a hack
useEffect(() => {
// todo(himself65): refactor with `useRef`
const abortController = new AbortController();
let blockHubElement: HTMLElement | null = null;
abortController.signal.addEventListener('abort', () => {
blockHubElement?.remove();
const toolWrapper = document.querySelector('#toolWrapper');
if (toolWrapper) {
assertEquals(toolWrapper.childNodes.length, 0);
}
});
editor?.createBlockHub().then(blockHub => {
const toolWrapper = document.querySelector('#toolWrapper');
if (!toolWrapper) {
// In an invitation page there is no toolWrapper, which contains helper icon and blockHub icon
return;
}
if (abortController.signal.aborted) {
return;
}
blockHubElement = blockHub;
setBlockHub(blockHub);
toolWrapper.appendChild(blockHub);
});
return () => {
abortController.abort();
};
}, [editor, setBlockHub]);
return null;
};
const Page: NextPageWithLayout = () => {
const currentPage = useGlobalState(store => store.currentPage);
const setEditor = useGlobalState(store => store.setEditor);
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const { t } = useTranslation();
// Only first workspace and first page will have template markdown
const shouldInitTemplateContent =
currentPage?.isEmpty &&
currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas.length === 1;
return (
<>
<Head>
<title>{currentPage?.meta?.title || t('Untitled')} - AFFiNE</title>
</Head>
<EditorHeader />
<MobileModal />
{currentPage && currentWorkspace?.blocksuiteWorkspace && (
<>
<DynamicBlocksuite
page={currentPage}
workspace={currentWorkspace.blocksuiteWorkspace}
setEditor={setEditor}
templateMarkdown={
shouldInitTemplateContent ? exampleMarkdown : undefined
}
templateTitle={
shouldInitTemplateContent
? 'Welcome to AFFiNE Alpha "Downhills"'
: undefined
}
/>
<BlockHubAppender key={currentWorkspace.id + currentPage.id} />
</>
)}
</>
);
};
const PageDefender = ({ children }: PropsWithChildren) => {
const router = useRouter();
const [pageLoaded, setPageLoaded] = useState(false);
const loadPage = useGlobalState(store => store.loadPage);
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const dataCenter = useGlobalState(store => store.dataCenter);
const { createPage } = usePageHelper();
useEffect(() => {
const initPage = async () => {
const pageId = router.query.pageId as string;
const isPageExist =
currentWorkspace?.blocksuiteWorkspace?.meta?.pageMetas.find(
p => p.id === pageId
);
if (!isPageExist) {
await createPage({ pageId });
}
await loadPage(pageId);
setPageLoaded(true);
};
initPage();
}, [createPage, currentWorkspace, loadPage, router.query.pageId]);
const api = useGlobalStateApi();
useEffect(
() =>
dataCenter.onWorkspacesChange(({ deleted }) => {
if (deleted?.some(workspace => workspace.id === currentWorkspace?.id)) {
router.replace('/404?code=kicked');
const dispose = currentWorkspace?.blockSuiteWorkspace.signals.pageAdded.on(
id => {
if (pageId === id) {
rerender(prev => !prev);
}
}),
[api, currentWorkspace?.id, dataCenter, router]
);
return <>{pageLoaded ? children : null}</>;
}
);
return () => {
dispose?.dispose();
};
}, [currentWorkspace?.blockSuiteWorkspace.signals.pageAdded, pageId]);
if (currentWorkspace === null) {
return <PageLoading />;
}
if (!pageId) {
return <PageLoading />;
}
if (!currentWorkspace.blockSuiteWorkspace.getPage(pageId)) {
return <PageLoading />;
}
if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].PageDetail;
return (
<PageDetail currentWorkspace={currentWorkspace} currentPageId={pageId} />
);
} else if (currentWorkspace.flavour === RemWorkspaceFlavour.LOCAL) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].PageDetail;
return (
<PageDetail currentWorkspace={currentWorkspace} currentPageId={pageId} />
);
}
throw new Unreachable();
};
Page.getLayout = function getLayout(page: ReactElement) {
return (
<WorkspaceLayout>
<PageDefender>{page}</PageDefender>
</WorkspaceLayout>
);
const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
useLoadWorkspace(useCurrentWorkspace()[0]);
useSyncRouterWithCurrentWorkspaceAndPage(router);
if (!router.isReady) {
return <PageLoading />;
} else if (
typeof router.query.pageId !== 'string' ||
typeof router.query.workspaceId !== 'string'
) {
throw new Error('Invalid router query');
}
return <WorkspaceDetail />;
};
export default Page;
export default WorkspaceDetailPage;
WorkspaceDetailPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -1,35 +1,87 @@
import { useTranslation } from '@affine/i18n';
import { FolderIcon } from '@blocksuite/icons';
import Head from 'next/head';
import { ReactElement, useCallback } from 'react';
import { assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { PageListHeader } from '@/components/header';
import { PageList } from '@/components/page-list';
import WorkspaceLayout from '@/components/workspace-layout';
import { useGlobalState } from '@/store/app';
const All = () => {
const pageList = useGlobalState(
useCallback(store => store.dataCenterPageList, [])
);
import {
QueryParamError,
Unreachable,
} from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useLoadWorkspace } from '../../../hooks/use-load-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins';
import { NextPageWithLayout, RemWorkspaceFlavour } from '../../../shared';
const AllPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
return (
<>
<Head>
<title>{t('All pages')} - AFFiNE</title>
</Head>
<PageListHeader icon={<FolderIcon />}>{t('All pages')}</PageListHeader>
<PageList
pageList={pageList.filter(p => !p.trash)}
showFavoriteTag={true}
listType="all"
/>
</>
useLoadWorkspace(currentWorkspace);
useSyncRouterWithCurrentWorkspace(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: currentWorkspace.id,
pageId,
},
});
}
},
[currentWorkspace, router]
);
if (!router.isReady) {
return <PageLoading />;
}
if (typeof router.query.workspaceId !== 'string') {
throw new QueryParamError('workspaceId', router.query.workspaceId);
}
if (currentWorkspace === null) {
return <PageLoading />;
}
if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) {
const PageList = WorkspacePlugins[currentWorkspace.flavour].PageList;
if (currentWorkspace.firstBinarySynced) {
return (
<>
<WorkspaceTitle icon={<FolderIcon />}>
{t('All pages')}
</WorkspaceTitle>
<PageList
onOpenPage={onClickPage}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
</>
);
} else {
return <div>loading</div>;
}
} else if (currentWorkspace.flavour === RemWorkspaceFlavour.LOCAL) {
const PageList = WorkspacePlugins[currentWorkspace.flavour].PageList;
return (
<>
<WorkspaceTitle icon={<FolderIcon />}>{t('All pages')}</WorkspaceTitle>
<PageList
onOpenPage={onClickPage}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
</>
);
}
throw new Unreachable();
};
All.getLayout = function getLayout(page: ReactElement) {
export default AllPage;
AllPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};
export default All;

View File

@@ -1,31 +1,64 @@
import { useTranslation } from '@affine/i18n';
import { FavoriteIcon } from '@blocksuite/icons';
import Head from 'next/head';
import { ReactElement } from 'react';
import { assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { Helmet } from 'react-helmet-async';
import { PageListHeader } from '@/components/header';
import { PageList } from '@/components/page-list';
import WorkspaceLayout from '@/components/workspace-layout';
import { useGlobalState } from '@/store/app';
import PageList from '../../../components/blocksuite/block-suite-page-list/page-list';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useLoadWorkspace } from '../../../hooks/use-load-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { NextPageWithLayout } from '../../../shared';
export const Favorite = () => {
const pageList = useGlobalState(store => store.dataCenterPageList);
const FavouritePage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useLoadWorkspace(currentWorkspace);
useSyncRouterWithCurrentWorkspace(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: currentWorkspace.id,
pageId,
},
});
}
},
[currentWorkspace, router]
);
if (currentWorkspace === null) {
return <PageLoading />;
}
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
return (
<>
<Head>
<Helmet>
<title>{t('Favorites')} - AFFiNE</title>
</Head>
<PageListHeader icon={<FavoriteIcon />}>{t('Favorites')}</PageListHeader>
</Helmet>
<WorkspaceTitle icon={<FavoriteIcon />}>{t('Favorites')}</WorkspaceTitle>
<PageList
pageList={pageList.filter(p => p.favorite && !p.trash)}
showFavoriteTag={true}
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onClickPage}
listType="favorite"
/>
</>
);
};
Favorite.getLayout = function getLayout(page: ReactElement) {
export default FavouritePage;
FavouritePage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};
export default Favorite;

View File

@@ -1,46 +0,0 @@
import { useRouter } from 'next/router';
import { useCallback, useEffect } from 'react';
import { PageLoading } from '@/components/loading';
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import usePageHelper from '@/hooks/use-page-helper';
import { useGlobalState } from '@/store/app';
const WorkspaceIndex = () => {
const router = useRouter();
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const { createPage } = usePageHelper();
const { workspaceLoaded, activeWorkspaceId } = useEnsureWorkspace();
useEffect(() => {
const initPage = async () => {
if (!workspaceLoaded) {
return;
}
const savedPageId =
currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas.find(
meta => !meta.trash
)?.id;
if (savedPageId) {
router.replace(`/workspace/${activeWorkspaceId}/${savedPageId}`);
return;
}
const pageId = await createPage();
router.replace(`/workspace/${activeWorkspaceId}/${pageId}`);
};
initPage();
}, [
currentWorkspace,
createPage,
router,
workspaceLoaded,
activeWorkspaceId,
]);
return <PageLoading />;
};
export default WorkspaceIndex;

View File

@@ -1,177 +1,167 @@
import { styled } from '@affine/component';
import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import { SettingsIcon } from '@blocksuite/icons';
import Head from 'next/head';
import {
CSSProperties,
ReactElement,
ReactNode,
startTransition,
useCallback,
useEffect,
useState,
} from 'react';
import { assertExists } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import { PageListHeader } from '@/components/header';
import WorkspaceLayout from '@/components/workspace-layout';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useLoadWorkspace } from '../../../hooks/use-load-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins';
import {
ExportPage,
GeneralPage,
MembersPage,
PublishPage,
SyncPage,
} from '@/components/workspace-setting';
import {
StyledSettingContainer,
StyledSettingContent,
WorkspaceSettingTagItem,
} from '@/components/workspace-setting/style';
import { useGlobalState } from '@/store/app';
NextPageWithLayout,
RemWorkspaceFlavour,
SettingPanel,
settingPanel,
settingPanelValues,
} from '../../../shared';
const useTabMap = () => {
const settingPanelAtom = atomWithStorage<SettingPanel>(
'workspaceId',
settingPanel.General
);
const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
const isOwner = useGlobalState(store => store.isOwner);
const tabMap: {
id: string;
name: string;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [
{
id: 'General',
name: t('General'),
panelRender: workspace => <GeneralPage workspace={workspace} />,
},
// TODO: add it back for desktop version
{
id: 'Sync',
name: t('Sync'),
panelRender: workspace => <SyncPage workspace={workspace} />,
},
{
id: 'Collaboration',
name: t('Collaboration'),
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
id: 'Publish',
name: t('Publish'),
panelRender: workspace => <PublishPage workspace={workspace} />,
},
// TODO: next version will finish this feature
{
id: 'Export',
name: t('Export'),
panelRender: workspace => <ExportPage workspace={workspace} />,
},
];
const [activeTab, setActiveTab] = useState<string>(tabMap[0].id);
const handleTabChange = (tabId: string) => {
setActiveTab(tabId);
};
const activeTabPanelRender = tabMap.find(
tab => tab.id === activeTab
)?.panelRender;
return {
activeTabPanelRender,
tabMap: isOwner ? tabMap : tabMap.slice(0, 1),
handleTabChange,
activeTab,
};
};
const StyledIndicator = styled.div(({ theme }) => {
return {
height: '2px',
background: theme.colors.primaryColor,
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
};
});
const StyledTabButtonWrapper = styled.div(() => {
return {
display: 'flex',
position: 'relative',
};
});
const WorkspaceSetting = () => {
const { t } = useTranslation();
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const user = useGlobalState(store => store.user);
const { activeTabPanelRender, tabMap, handleTabChange, activeTab } =
useTabMap();
const [indicatorState, setIndicatorState] = useState<
Pick<CSSProperties, 'left' | 'width'>
>({
left: 0,
width: 0,
});
const shouldHideSyncTab =
currentWorkspace?.owner?.id !== user?.id ||
currentWorkspace?.provider === 'local';
useEffect(() => {
const tabButton = document.querySelector(
`[data-setting-tab-button="${activeTab}"]`
);
if (tabButton instanceof HTMLElement) {
startTransition(() => {
setIndicatorState({
width: `${tabButton.offsetWidth}px`,
left: `${tabButton.offsetLeft}px`,
});
useLoadWorkspace(currentWorkspace);
useSyncRouterWithCurrentWorkspace(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {});
const onChangeTab = useCallback(
(tab: SettingPanel) => {
setCurrentTab(tab as SettingPanel);
router.push({
pathname: router.pathname,
query: {
...router.query,
currentTab: tab,
},
});
}
}, [activeTab]);
return (
<>
<Head>
<title>{t('Workspace Settings')} - AFFiNE</title>
</Head>
<PageListHeader icon={<SettingsIcon />}>
{t('Workspace Settings')}
</PageListHeader>
<StyledSettingContainer>
<StyledTabButtonWrapper>
{tabMap.map(({ id, name }) => {
if (shouldHideSyncTab && id === 'Sync') {
return null;
}
return (
<WorkspaceSettingTagItem
key={id}
isActive={activeTab === id}
data-setting-tab-button={id}
onClick={() => {
handleTabChange(id);
}}
>
{name}
</WorkspaceSettingTagItem>
);
})}
<StyledIndicator style={indicatorState} />
</StyledTabButtonWrapper>
<StyledSettingContent>
{currentWorkspace && activeTabPanelRender?.(currentWorkspace)}
</StyledSettingContent>
</StyledSettingContainer>
</>
},
[router, setCurrentTab]
);
useEffect(() => {
if (!router.isReady) {
return;
}
const queryCurrentTab =
typeof router.query.currentTab === 'string'
? router.query.currentTab
: null;
if (
queryCurrentTab !== null &&
settingPanelValues.indexOf(queryCurrentTab as SettingPanel) === -1
) {
setCurrentTab(settingPanel.General);
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (settingPanelValues.indexOf(currentTab as SettingPanel) === -1) {
setCurrentTab(settingPanel.General);
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: settingPanel.General,
},
});
return;
} else if (queryCurrentTab !== currentTab) {
router.replace({
pathname: router.pathname,
query: {
...router.query,
currentTab: currentTab,
},
});
return;
}
}, [currentTab, router, setCurrentTab]);
const helper = useWorkspacesHelper();
const onDeleteWorkspace = useCallback(() => {
assertExists(currentWorkspace);
const workspaceId = currentWorkspace.id;
helper.deleteWorkspace(workspaceId);
}, [currentWorkspace, helper]);
const onTransformWorkspace = useCallback(
(targetWorkspaceId: string) => {
router
.replace({
pathname: `/workspace/[workspaceId]/all`,
query: {
workspaceId: targetWorkspaceId,
},
})
.then(() => {
router.reload();
});
},
[router]
);
if (!router.isReady) {
return <PageLoading />;
} else if (currentWorkspace === null) {
return <PageLoading />;
} else if (settingPanelValues.indexOf(currentTab as SettingPanel) === -1) {
return <PageLoading />;
} else if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) {
const Setting = WorkspacePlugins[currentWorkspace.flavour].SettingsDetail;
return (
<>
<Helmet>
<title>{t('Workspace Settings')} - AFFiNE</title>
</Helmet>
<WorkspaceTitle icon={<SettingsIcon />}>
{t('Workspace Settings')}
</WorkspaceTitle>
<Setting
onTransformWorkspace={onTransformWorkspace}
onDeleteWorkspace={onDeleteWorkspace}
currentWorkspace={currentWorkspace}
currentTab={currentTab as SettingPanel}
onChangeTab={onChangeTab}
/>
</>
);
} else if (currentWorkspace.flavour === RemWorkspaceFlavour.LOCAL) {
const Setting = WorkspacePlugins[currentWorkspace.flavour].SettingsDetail;
return (
<>
<WorkspaceTitle icon={<SettingsIcon />}>
{t('Workspace Settings')}
</WorkspaceTitle>
<Setting
onTransformWorkspace={onTransformWorkspace}
onDeleteWorkspace={onDeleteWorkspace}
currentWorkspace={currentWorkspace}
currentTab={currentTab as SettingPanel}
onChangeTab={onChangeTab}
/>
</>
);
}
throw new Unreachable();
};
WorkspaceSetting.getLayout = function getLayout(page: ReactElement) {
export default SettingPage;
SettingPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};
export default WorkspaceSetting;

View File

@@ -1,37 +1,69 @@
import { useTranslation } from '@affine/i18n';
import { DeleteTemporarilyIcon } from '@blocksuite/icons';
import Head from 'next/head';
import { ReactElement, useCallback } from 'react';
import { assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { Helmet } from 'react-helmet-async';
import { PageListHeader } from '@/components/header';
import { PageList } from '@/components/page-list';
import WorkspaceLayout from '@/components/workspace-layout';
import { useGlobalState } from '@/store/app';
import PageList from '../../../components/blocksuite/block-suite-page-list/page-list';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useLoadWorkspace } from '../../../hooks/use-load-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { NextPageWithLayout } from '../../../shared';
export const Trash = () => {
const pageList = useGlobalState(
useCallback(store => store.dataCenterPageList, [])
);
const TrashPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useLoadWorkspace(currentWorkspace);
useSyncRouterWithCurrentWorkspace(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: currentWorkspace.id,
pageId,
},
});
}
},
[currentWorkspace, router]
);
if (!router.isReady) {
return <PageLoading />;
} else if (currentWorkspace === null) {
return <PageLoading />;
}
// todo(himself65): refactor to plugin
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
return (
<>
<Head>
<Helmet>
<title>{t('Trash')} - AFFiNE</title>
</Head>
<PageListHeader icon={<DeleteTemporarilyIcon />}>
</Helmet>
<WorkspaceTitle icon={<DeleteTemporarilyIcon />}>
{t('Trash')}
</PageListHeader>
</WorkspaceTitle>
<PageList
pageList={pageList.filter(p => p.trash)}
isTrash={true}
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onClickPage}
listType="trash"
/>
</>
);
};
Trash.getLayout = function getLayout(page: ReactElement) {
export default TrashPage;
TrashPage.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};
export default Trash;

View File

@@ -1,24 +0,0 @@
import { useRouter } from 'next/router';
import { useCallback, useEffect } from 'react';
import { PageLoading } from '@/components/loading';
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import { useGlobalState } from '@/store/app';
export const WorkspaceIndex = () => {
const router = useRouter();
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const { workspaceLoaded } = useEnsureWorkspace();
useEffect(() => {
if (workspaceLoaded) {
router.push(`/workspace/${currentWorkspace?.id}`);
}
}, [currentWorkspace, router, workspaceLoaded]);
return <PageLoading />;
};
export default WorkspaceIndex;