mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 20:18:42 +08:00
feat(core): download template from snapshot url (#8211)
This commit is contained in:
+3
-5
@@ -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
|
||||||
|
|||||||
@@ -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()}`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user