feat(core): download template from snapshot url (#8211)

This commit is contained in:
EYHN
2024-09-12 06:21:52 +00:00
parent aad7b90859
commit 3999b04cf1
12 changed files with 87 additions and 94 deletions
@@ -3,20 +3,18 @@ import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
export const ImportTemplateButton = ({ export const ImportTemplateButton = ({
workspaceId,
docId,
name, name,
snapshotUrl,
}: { }: {
workspaceId: string;
docId: string;
name: string; name: string;
snapshotUrl: string;
}) => { }) => {
const t = useI18n(); const t = useI18n();
const { jumpToImportTemplate } = useNavigateHelper(); const { jumpToImportTemplate } = useNavigateHelper();
return ( return (
<Button <Button
variant="primary" variant="primary"
onClick={() => jumpToImportTemplate(workspaceId, docId, name)} onClick={() => jumpToImportTemplate(name, snapshotUrl)}
> >
{t['com.affine.share-page.header.import-template']()} {t['com.affine.share-page.header.import-template']()}
</Button> </Button>
@@ -9,19 +9,17 @@ import * as styles from './styles.css';
import { PublishPageUserAvatar } from './user-avatar'; import { PublishPageUserAvatar } from './user-avatar';
export type ShareHeaderRightItemProps = { export type ShareHeaderRightItemProps = {
workspaceId: string;
docId: string;
publishMode: DocMode; publishMode: DocMode;
isTemplate?: boolean; isTemplate?: boolean;
templateName?: string; templateName?: string;
snapshotUrl?: string;
}; };
const ShareHeaderRightItem = ({ const ShareHeaderRightItem = ({
publishMode, publishMode,
isTemplate, isTemplate,
templateName, templateName,
workspaceId, snapshotUrl,
docId,
}: ShareHeaderRightItemProps) => { }: ShareHeaderRightItemProps) => {
const loginStatus = useLiveData(useService(AuthService).session.status$); const loginStatus = useLiveData(useService(AuthService).session.status$);
const authenticated = loginStatus === 'authenticated'; const authenticated = loginStatus === 'authenticated';
@@ -29,9 +27,8 @@ const ShareHeaderRightItem = ({
<div className={styles.rightItemContainer}> <div className={styles.rightItemContainer}>
{isTemplate ? ( {isTemplate ? (
<ImportTemplateButton <ImportTemplateButton
docId={docId}
workspaceId={workspaceId}
name={templateName ?? ''} name={templateName ?? ''}
snapshotUrl={snapshotUrl ?? ''}
/> />
) : ( ) : (
<> <>
@@ -183,9 +183,9 @@ export function useNavigateHelper() {
); );
const jumpToImportTemplate = useCallback( const jumpToImportTemplate = useCallback(
(workspaceId: string, docId: string, name: string) => { (name: string, snapshotUrl: string) => {
return navigate( return navigate(
`/template/import?workspaceId=${encodeURIComponent(workspaceId)}&docId=${encodeURIComponent(docId)}&name=${encodeURIComponent(name)}` `/template/import?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
); );
}, },
[navigate] [navigate]
@@ -1,15 +1,16 @@
import { Entity, LiveData } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra';
interface TemplateOptions {
templateName: string;
snapshotUrl: string;
}
export class ImportTemplateDialog extends Entity { export class ImportTemplateDialog extends Entity {
readonly isOpen$ = new LiveData(false); readonly isOpen$ = new LiveData(false);
readonly template$ = new LiveData<{ readonly template$ = new LiveData<TemplateOptions | null>(null);
workspaceId: string;
docId: string;
templateName: string;
} | null>(null);
open(workspaceId: string, docId: string, templateName: string) { open(options: TemplateOptions) {
this.template$.next({ workspaceId, docId, templateName }); this.template$.next(options);
this.isOpen$.next(true); this.isOpen$.next(true);
} }
@@ -23,29 +23,27 @@ export class TemplateDownloader extends Entity {
readonly error$ = new LiveData<any | null>(null); readonly error$ = new LiveData<any | null>(null);
readonly download = effect( readonly download = effect(
switchMap( switchMap(({ snapshotUrl }: { snapshotUrl: string }) => {
({ workspaceId, docId }: { workspaceId: string; docId: string }) => { return fromPromise(() => this.store.download(snapshotUrl)).pipe(
return fromPromise(() => this.store.download(workspaceId, docId)).pipe( mergeMap(({ data }) => {
mergeMap(({ data }) => { this.data$.next(data);
this.data$.next(data); return EMPTY;
return EMPTY; }),
}), backoffRetry({
backoffRetry({ when: isNetworkError,
when: isNetworkError, count: Infinity,
count: Infinity, }),
}), backoffRetry({
backoffRetry({ when: isBackendError,
when: isBackendError, }),
}), catchErrorInto(this.error$),
catchErrorInto(this.error$), onStart(() => {
onStart(() => { this.isDownloading$.next(true);
this.isDownloading$.next(true); this.data$.next(null);
this.data$.next(null); this.error$.next(null);
this.error$.next(null); }),
}), onComplete(() => this.isDownloading$.next(false))
onComplete(() => this.isDownloading$.next(false)) );
); })
}
)
); );
} }
@@ -1,7 +1,7 @@
import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import { ZipTransformer } from '@blocksuite/blocks'; 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 { DocsService, Service } from '@toeverything/infra';
export class ImportTemplateService extends Service { export class ImportTemplateService extends Service {
constructor(private readonly workspacesService: WorkspacesService) { constructor(private readonly workspacesService: WorkspacesService) {
@@ -23,7 +23,10 @@ export class ImportTemplateService extends Service {
type: 'application/zip', type: 'application/zip',
}) })
); );
const docsService = workspace.scope.get(DocsService);
if (importedDoc) { if (importedDoc) {
// only support page mode for now
docsService.list.setPrimaryMode(importedDoc.id, 'page');
disposeWorkspace(); disposeWorkspace();
return importedDoc.id; return importedDoc.id;
} else { } else {
@@ -7,16 +7,10 @@ export class TemplateDownloaderStore extends Store {
super(); super();
} }
async download( async download(snapshotUrl: string) {
/* not support workspaceid for now */ _workspaceId: string, const response = await this.fetchService.fetch(snapshotUrl, {
docId: string priority: 'high',
) { } as any);
const response = await this.fetchService.fetch(
`https://affine.pro/templates/snapshots/${docId}.zip `,
{
priority: 'high',
} as any
);
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
return { data: new Uint8Array(arrayBuffer) }; return { data: new Uint8Array(arrayBuffer) };
@@ -23,14 +23,12 @@ import { ImportTemplateService } from '../services/import';
import * as styles from './dialog.css'; import * as styles from './dialog.css';
const Dialog = ({ const Dialog = ({
workspaceId,
docId,
templateName, templateName,
snapshotUrl,
onClose, onClose,
}: { }: {
workspaceId: string;
docId: string;
templateName: string; templateName: string;
snapshotUrl: string;
onClose?: () => void; onClose?: () => void;
}) => { }) => {
const t = useI18n(); const t = useI18n();
@@ -69,28 +67,26 @@ const Dialog = ({
useEffect(() => { useEffect(() => {
if (!isSessionRevalidating && notLogin) { if (!isSessionRevalidating && notLogin) {
jumpToSignIn( jumpToSignIn(
'/template/import?workspaceId=' + '/template/import?' +
workspaceId +
'&docId=' +
docId +
'&name=' + '&name=' +
templateName templateName +
'&snapshotUrl=' +
snapshotUrl
); );
onClose?.(); onClose?.();
} }
}, [ }, [
docId,
isSessionRevalidating, isSessionRevalidating,
jumpToSignIn, jumpToSignIn,
notLogin, notLogin,
onClose, onClose,
snapshotUrl,
templateName, templateName,
workspaceId,
]); ]);
useEffect(() => { useEffect(() => {
templateDownloader.download({ workspaceId, docId }); templateDownloader.download({ snapshotUrl });
}, [docId, templateDownloader, workspaceId]); }, [snapshotUrl, templateDownloader]);
const handleSelectedWorkspace = useCallback( const handleSelectedWorkspace = useCallback(
(workspaceMetadata: WorkspaceMetadata) => { (workspaceMetadata: WorkspaceMetadata) => {
@@ -238,9 +234,8 @@ export const ImportTemplateDialogProvider = () => {
> >
{template && ( {template && (
<Dialog <Dialog
docId={template.docId}
templateName={template.templateName} templateName={template.templateName}
workspaceId={template.workspaceId} snapshotUrl={template.snapshotUrl}
onClose={() => importTemplateDialogService.dialog.close()} onClose={() => importTemplateDialogService.dialog.close()}
/> />
)} )}
@@ -10,11 +10,10 @@ export const Component = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
useEffect(() => { useEffect(() => {
importTemplateDialogService.dialog.open( importTemplateDialogService.dialog.open({
searchParams.get('workspaceId') ?? '', templateName: searchParams.get('name') ?? '',
searchParams.get('docId') ?? '', snapshotUrl: searchParams.get('snapshotUrl') ?? '',
searchParams.get('name') ?? '' });
);
}, [importTemplateDialogService.dialog, jumpToIndex, searchParams]); }, [importTemplateDialogService.dialog, jumpToIndex, searchParams]);
// no ui for this route, just open the dialog // no ui for this route, just open the dialog
return null; return null;
@@ -3,22 +3,21 @@ import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item'; import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
import type { DocMode } from '@blocksuite/blocks'; import type { DocMode } from '@blocksuite/blocks';
import type { DocCollection } from '@blocksuite/store';
import * as styles from './share-header.css'; import * as styles from './share-header.css';
export function ShareHeader({ export function ShareHeader({
pageId, pageId,
publishMode, publishMode,
docCollection,
isTemplate, isTemplate,
templateName, templateName,
snapshotUrl,
}: { }: {
pageId: string; pageId: string;
publishMode: DocMode; publishMode: DocMode;
docCollection: DocCollection;
isTemplate?: boolean; isTemplate?: boolean;
templateName?: string; templateName?: string;
snapshotUrl?: string;
}) { }) {
return ( return (
<div className={styles.header}> <div className={styles.header}>
@@ -26,10 +25,9 @@ export function ShareHeader({
<BlocksuiteHeaderTitle docId={pageId} /> <BlocksuiteHeaderTitle docId={pageId} />
<div className={styles.spacer} /> <div className={styles.spacer} />
<ShareHeaderRightItem <ShareHeaderRightItem
workspaceId={docCollection.id}
docId={pageId}
publishMode={publishMode} publishMode={publishMode}
isTemplate={isTemplate} isTemplate={isTemplate}
snapshotUrl={snapshotUrl}
templateName={templateName} templateName={templateName}
/> />
<AuthModal /> <AuthModal />
@@ -57,19 +57,21 @@ export const SharePage = ({
const location = useLocation(); const location = useLocation();
const { mode, isTemplate, templateName } = useMemo(() => { const { mode, isTemplate, templateName, templateSnapshotUrl } =
const searchParams = new URLSearchParams(location.search); useMemo(() => {
const queryStringMode = searchParams.get('mode') as DocMode | null; const searchParams = new URLSearchParams(location.search);
const queryStringMode = searchParams.get('mode') as DocMode | null;
return { return {
mode: mode:
queryStringMode && DocModes.includes(queryStringMode) queryStringMode && DocModes.includes(queryStringMode)
? queryStringMode ? queryStringMode
: null, : null,
isTemplate: searchParams.has('isTemplate'), isTemplate: searchParams.has('isTemplate'),
templateName: searchParams.get('templateName') || '', templateName: searchParams.get('templateName') || '',
}; templateSnapshotUrl: searchParams.get('snapshotUrl') || '',
}, [location.search]); };
}, [location.search]);
useEffect(() => { useEffect(() => {
shareReaderService.reader.loadShare({ workspaceId, docId }); shareReaderService.reader.loadShare({ workspaceId, docId });
@@ -94,6 +96,7 @@ export const SharePage = ({
publishMode={mode || data.publishMode} publishMode={mode || data.publishMode}
isTemplate={isTemplate} isTemplate={isTemplate}
templateName={templateName} templateName={templateName}
templateSnapshotUrl={templateSnapshotUrl}
/> />
); );
} else { } else {
@@ -109,6 +112,7 @@ const SharePageInner = ({
publishMode = 'page' as DocMode, publishMode = 'page' as DocMode,
isTemplate, isTemplate,
templateName, templateName,
templateSnapshotUrl,
}: { }: {
workspaceId: string; workspaceId: string;
docId: string; docId: string;
@@ -117,6 +121,7 @@ const SharePageInner = ({
publishMode?: DocMode; publishMode?: DocMode;
isTemplate?: boolean; isTemplate?: boolean;
templateName?: string; templateName?: string;
templateSnapshotUrl?: string;
}) => { }) => {
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
@@ -227,9 +232,9 @@ const SharePageInner = ({
<ShareHeader <ShareHeader
pageId={page.id} pageId={page.id}
publishMode={publishMode} publishMode={publishMode}
docCollection={page.blockSuiteDoc.collection}
isTemplate={isTemplate} isTemplate={isTemplate}
templateName={templateName} templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/> />
<Scrollable.Root> <Scrollable.Root>
<Scrollable.Viewport <Scrollable.Viewport
+6 -1
View File
@@ -101,9 +101,14 @@ export const topLevelRoutes = [
const workspaceId = url.searchParams.get('workspaceId'); const workspaceId = url.searchParams.get('workspaceId');
const docId = url.searchParams.get('docId'); const docId = url.searchParams.get('docId');
const templateName = url.searchParams.get('name'); const templateName = url.searchParams.get('name');
const snapshotUrl = url.searchParams.get('snapshotUrl');
return redirect( return redirect(
`/workspace/${workspaceId}/${docId}?isTemplate=true&templateName=${encodeURIComponent(templateName ?? '')}` `/workspace/${workspaceId}/${docId}?${new URLSearchParams({
isTemplate: 'true',
templateName: templateName ?? '',
snapshotUrl: snapshotUrl ?? '',
}).toString()}`
); );
}, },
}, },