mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 15:26:59 +08:00
feat(core): preview template & snapshot import (#8193)
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user