feat(core): preview template & snapshot import (#8193)

This commit is contained in:
EYHN
2024-09-11 07:11:33 +00:00
parent 52d9569f47
commit 8c191e6baa
9 changed files with 123 additions and 29 deletions

View File

@@ -0,0 +1,24 @@
import { Button } from '@affine/component';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useI18n } from '@affine/i18n';
export const ImportTemplateButton = ({
workspaceId,
docId,
name,
}: {
workspaceId: string;
docId: string;
name: string;
}) => {
const t = useI18n();
const { jumpToImportTemplate } = useNavigateHelper();
return (
<Button
variant="primary"
onClick={() => jumpToImportTemplate(workspaceId, docId, name)}
>
{t['com.affine.share-page.header.import-template']()}
</Button>
);
};

View File

@@ -2,6 +2,7 @@ import { AuthService } from '@affine/core/modules/cloud';
import type { DocMode } from '@blocksuite/blocks'; import type { DocMode } from '@blocksuite/blocks';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { ImportTemplateButton } from './import-template';
import { PresentButton } from './present'; import { PresentButton } from './present';
import { SignIn } from './sign-in'; import { SignIn } from './sign-in';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -9,28 +10,45 @@ import { PublishPageUserAvatar } from './user-avatar';
export type ShareHeaderRightItemProps = { export type ShareHeaderRightItemProps = {
workspaceId: string; workspaceId: string;
pageId: string; docId: string;
publishMode: DocMode; publishMode: DocMode;
isTemplate?: boolean;
templateName?: string;
}; };
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => { const ShareHeaderRightItem = ({
publishMode,
isTemplate,
templateName,
workspaceId,
docId,
}: ShareHeaderRightItemProps) => {
const loginStatus = useLiveData(useService(AuthService).session.status$); const loginStatus = useLiveData(useService(AuthService).session.status$);
const { publishMode } = props;
const authenticated = loginStatus === 'authenticated'; const authenticated = loginStatus === 'authenticated';
return ( return (
<div className={styles.rightItemContainer}> <div className={styles.rightItemContainer}>
{authenticated ? null : <SignIn />} {isTemplate ? (
{publishMode === 'edgeless' ? <PresentButton /> : null} <ImportTemplateButton
{authenticated ? ( docId={docId}
workspaceId={workspaceId}
name={templateName ?? ''}
/>
) : (
<> <>
<div {authenticated ? null : <SignIn />}
className={styles.headerDivider} {publishMode === 'edgeless' ? <PresentButton /> : null}
data-authenticated={true} {authenticated ? (
data-is-edgeless={publishMode === 'edgeless'} <>
/> <div
<PublishPageUserAvatar /> className={styles.headerDivider}
data-authenticated={true}
data-is-edgeless={publishMode === 'edgeless'}
/>
<PublishPageUserAvatar />
</>
) : null}
</> </>
) : null} )}
</div> </div>
); );
}; };

View File

@@ -182,6 +182,15 @@ export function useNavigateHelper() {
[navigate] [navigate]
); );
const jumpToImportTemplate = useCallback(
(workspaceId: string, docId: string, name: string) => {
return navigate(
`/template/import?workspaceId=${encodeURIComponent(workspaceId)}&docId=${encodeURIComponent(docId)}&name=${encodeURIComponent(name)}`
);
},
[navigate]
);
return useMemo( return useMemo(
() => ({ () => ({
jumpToPage, jumpToPage,
@@ -197,6 +206,7 @@ export function useNavigateHelper() {
jumpToTags, jumpToTags,
jumpToTag, jumpToTag,
openInApp, openInApp,
jumpToImportTemplate,
}), }),
[ [
jumpToPage, jumpToPage,
@@ -212,6 +222,7 @@ export function useNavigateHelper() {
jumpToTags, jumpToTags,
jumpToTag, jumpToTag,
openInApp, openInApp,
jumpToImportTemplate,
] ]
); );
} }

View File

@@ -1,4 +1,5 @@
import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import { ZipTransformer } from '@blocksuite/blocks';
import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra'; import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
@@ -16,13 +17,18 @@ export class ImportTemplateService extends Service {
metadata: workspaceMetadata, metadata: workspaceMetadata,
}); });
await workspace.engine.waitForRootDocReady(); await workspace.engine.waitForRootDocReady();
const newDoc = workspace.docCollection.createDoc({}); const [importedDoc] = await ZipTransformer.importDocs(
await workspace.engine.doc.storage.behavior.doc.set( workspace.docCollection,
newDoc.spaceDoc.guid, new Blob([docBinary], {
docBinary type: 'application/zip',
})
); );
disposeWorkspace(); if (importedDoc) {
return newDoc.id; disposeWorkspace();
return importedDoc.id;
} else {
throw new Error('Failed to import doc');
}
} }
async importToNewWorkspace( async importToNewWorkspace(

View File

@@ -7,9 +7,12 @@ export class TemplateDownloaderStore extends Store {
super(); super();
} }
async download(workspaceId: string, docId: string) { async download(
/* not support workspaceid for now */ _workspaceId: string,
docId: string
) {
const response = await this.fetchService.fetch( const response = await this.fetchService.fetch(
`/api/workspaces/${workspaceId}/docs/${docId}`, `https://affine.pro/templates/snapshots/${docId}.zip `,
{ {
priority: 'high', priority: 'high',
} as any } as any

View File

@@ -11,10 +11,14 @@ export function ShareHeader({
pageId, pageId,
publishMode, publishMode,
docCollection, docCollection,
isTemplate,
templateName,
}: { }: {
pageId: string; pageId: string;
publishMode: DocMode; publishMode: DocMode;
docCollection: DocCollection; docCollection: DocCollection;
isTemplate?: boolean;
templateName?: string;
}) { }) {
return ( return (
<div className={styles.header}> <div className={styles.header}>
@@ -23,8 +27,10 @@ export function ShareHeader({
<div className={styles.spacer} /> <div className={styles.spacer} />
<ShareHeaderRightItem <ShareHeaderRightItem
workspaceId={docCollection.id} workspaceId={docCollection.id}
pageId={pageId} docId={pageId}
publishMode={publishMode} publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
/> />
<AuthModal /> <AuthModal />
</div> </div>

View File

@@ -32,7 +32,7 @@ import {
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
@@ -57,14 +57,18 @@ export const SharePage = ({
const location = useLocation(); const location = useLocation();
const [mode, setMode] = useState<DocMode | null>(null); const { mode, isTemplate, templateName } = useMemo(() => {
useEffect(() => {
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const queryStringMode = searchParams.get('mode') as DocMode | null; const queryStringMode = searchParams.get('mode') as DocMode | null;
if (queryStringMode && DocModes.includes(queryStringMode)) {
setMode(queryStringMode); return {
} mode:
queryStringMode && DocModes.includes(queryStringMode)
? queryStringMode
: null,
isTemplate: searchParams.has('isTemplate'),
templateName: searchParams.get('templateName') || '',
};
}, [location.search]); }, [location.search]);
useEffect(() => { useEffect(() => {
@@ -88,6 +92,8 @@ export const SharePage = ({
workspaceBinary={data.workspaceBinary} workspaceBinary={data.workspaceBinary}
docBinary={data.docBinary} docBinary={data.docBinary}
publishMode={mode || data.publishMode} publishMode={mode || data.publishMode}
isTemplate={isTemplate}
templateName={templateName}
/> />
); );
} else { } else {
@@ -101,12 +107,16 @@ const SharePageInner = ({
workspaceBinary, workspaceBinary,
docBinary, docBinary,
publishMode = 'page' as DocMode, publishMode = 'page' as DocMode,
isTemplate,
templateName,
}: { }: {
workspaceId: string; workspaceId: string;
docId: string; docId: string;
workspaceBinary: Uint8Array; workspaceBinary: Uint8Array;
docBinary: Uint8Array; docBinary: Uint8Array;
publishMode?: DocMode; publishMode?: DocMode;
isTemplate?: boolean;
templateName?: string;
}) => { }) => {
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
@@ -218,6 +228,8 @@ const SharePageInner = ({
pageId={page.id} pageId={page.id}
publishMode={publishMode} publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection} docCollection={page.blockSuiteDoc.collection}
isTemplate={isTemplate}
templateName={templateName}
/> />
<Scrollable.Root> <Scrollable.Root>
<Scrollable.Viewport <Scrollable.Viewport

View File

@@ -94,6 +94,19 @@ export const topLevelRoutes = [
path: '/template/import', path: '/template/import',
lazy: () => import('./pages/import-template'), lazy: () => import('./pages/import-template'),
}, },
{
path: '/template/preview',
loader: ({ request }) => {
const url = new URL(request.url);
const workspaceId = url.searchParams.get('workspaceId');
const docId = url.searchParams.get('docId');
const templateName = url.searchParams.get('name');
return redirect(
`/workspace/${workspaceId}/${docId}?isTemplate=true&templateName=${encodeURIComponent(templateName ?? '')}`
);
},
},
{ {
path: '/auth/:authType', path: '/auth/:authType',
lazy: () => import(/* webpackChunkName: "auth" */ './pages/auth/auth'), lazy: () => import(/* webpackChunkName: "auth" */ './pages/auth/auth'),

View File

@@ -1457,6 +1457,7 @@
"com.affine.share-page.footer.get-started": "Get started for free", "com.affine.share-page.footer.get-started": "Get started for free",
"com.affine.share-page.header.login": "Login or Sign Up", "com.affine.share-page.header.login": "Login or Sign Up",
"com.affine.share-page.header.present": "Present", "com.affine.share-page.header.present": "Present",
"com.affine.share-page.header.import-template": "Use This Template",
"com.affine.shortcutsTitle.edgeless": "Edgeless", "com.affine.shortcutsTitle.edgeless": "Edgeless",
"com.affine.shortcutsTitle.general": "General", "com.affine.shortcutsTitle.general": "General",
"com.affine.shortcutsTitle.markdownSyntax": "Markdown syntax", "com.affine.shortcutsTitle.markdownSyntax": "Markdown syntax",