mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 08:09:52 +08:00
feat: add animation for history preview (#5966)
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/5ce45d13-1117-4853-a066-e8ab1446eb4f.mp4">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/5ce45d13-1117-4853-a066-e8ab1446eb4f.mp4">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/5ce45d13-1117-4853-a066-e8ab1446eb4f.mp4">Kapture 2024-02-29 at 18.54.15.mp4</video>
This commit is contained in:
@@ -63,11 +63,7 @@ export const useDocSnapshotList = (workspaceId: string, pageDocId: string) => {
|
|||||||
return data.flatMap(page => page.workspace.histories);
|
return data.flatMap(page => page.workspace.histories);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return [
|
return [histories, shouldLoadMore ? loadMore : false, !!loadingMore] as const;
|
||||||
histories,
|
|
||||||
shouldLoadMore ? loadMore : undefined,
|
|
||||||
loadingMore,
|
|
||||||
] as const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const snapshotFetcher = async (
|
const snapshotFetcher = async (
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { DialogContentProps } from '@radix-ui/react-dialog';
|
|||||||
import { Doc, type PageMode, Workspace } from '@toeverything/infra';
|
import { Doc, type PageMode, Workspace } from '@toeverything/infra';
|
||||||
import { useService } from '@toeverything/infra/di';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||||
|
import { range } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { encodeStateAsUpdate } from 'yjs';
|
import { encodeStateAsUpdate } from 'yjs';
|
||||||
@@ -90,6 +92,7 @@ const ModalContainer = ({
|
|||||||
|
|
||||||
interface HistoryEditorPreviewProps {
|
interface HistoryEditorPreviewProps {
|
||||||
ts?: string;
|
ts?: string;
|
||||||
|
historyList: HistoryList;
|
||||||
snapshotPage?: BlockSuiteDoc;
|
snapshotPage?: BlockSuiteDoc;
|
||||||
mode: PageMode;
|
mode: PageMode;
|
||||||
onModeChange: (mode: PageMode) => void;
|
onModeChange: (mode: PageMode) => void;
|
||||||
@@ -98,6 +101,7 @@ interface HistoryEditorPreviewProps {
|
|||||||
|
|
||||||
const HistoryEditorPreview = ({
|
const HistoryEditorPreview = ({
|
||||||
ts,
|
ts,
|
||||||
|
historyList,
|
||||||
snapshotPage,
|
snapshotPage,
|
||||||
onModeChange,
|
onModeChange,
|
||||||
mode,
|
mode,
|
||||||
@@ -110,11 +114,9 @@ const HistoryEditorPreview = ({
|
|||||||
onModeChange('edgeless');
|
onModeChange('edgeless');
|
||||||
}, [onModeChange]);
|
}, [onModeChange]);
|
||||||
|
|
||||||
return (
|
const content = useMemo(() => {
|
||||||
<div className={styles.previewWrapper}>
|
return (
|
||||||
<div className={styles.previewContainerStack2} />
|
<>
|
||||||
<div className={styles.previewContainerStack1} />
|
|
||||||
<div className={styles.previewContainer}>
|
|
||||||
<div className={styles.previewHeader}>
|
<div className={styles.previewHeader}>
|
||||||
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
|
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
|
||||||
<PageSwitchItem
|
<PageSwitchItem
|
||||||
@@ -152,7 +154,38 @@ const HistoryEditorPreview = ({
|
|||||||
<Loading size={24} />
|
<Loading size={24} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
mode,
|
||||||
|
onSwitchToEdgelessMode,
|
||||||
|
onSwitchToPageMode,
|
||||||
|
snapshotPage,
|
||||||
|
title,
|
||||||
|
ts,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.previewWrapper} ref={previewRef}>
|
||||||
|
{range(0, historyList.length).map(i => {
|
||||||
|
const historyIndex = historyList.findIndex(h => h.timestamp === ts);
|
||||||
|
const distance = i - historyIndex;
|
||||||
|
const flag =
|
||||||
|
distance === 0
|
||||||
|
? 'current'
|
||||||
|
: distance > 2
|
||||||
|
? '> 2'
|
||||||
|
: distance < 0
|
||||||
|
? '< 0'
|
||||||
|
: distance;
|
||||||
|
return (
|
||||||
|
<div data-distance={flag} key={i} className={styles.previewContainer}>
|
||||||
|
{historyIndex === i ? content : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -240,21 +273,21 @@ const PlanPrompt = () => {
|
|||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HistoryList = ReturnType<typeof useDocSnapshotList>[0];
|
||||||
|
|
||||||
const PageHistoryList = ({
|
const PageHistoryList = ({
|
||||||
pageDocId,
|
historyList,
|
||||||
workspaceId,
|
onLoadMore,
|
||||||
|
loadingMore,
|
||||||
activeVersion,
|
activeVersion,
|
||||||
onVersionChange,
|
onVersionChange,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
|
||||||
pageDocId: string;
|
|
||||||
activeVersion?: string;
|
activeVersion?: string;
|
||||||
onVersionChange: (version: string) => void;
|
onVersionChange: (version: string) => void;
|
||||||
|
historyList: HistoryList;
|
||||||
|
onLoadMore: (() => void) | false;
|
||||||
|
loadingMore: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [historyList, loadMore, loadingMore] = useDocSnapshotList(
|
|
||||||
workspaceId,
|
|
||||||
pageDocId
|
|
||||||
);
|
|
||||||
const historyListByDay = useMemo(() => {
|
const historyListByDay = useMemo(() => {
|
||||||
return historyListGroupByDay(historyList);
|
return historyListGroupByDay(historyList);
|
||||||
}, [historyList]);
|
}, [historyList]);
|
||||||
@@ -330,13 +363,13 @@ const PageHistoryList = ({
|
|||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{loadMore ? (
|
{onLoadMore ? (
|
||||||
<Button
|
<Button
|
||||||
type="plain"
|
type="plain"
|
||||||
loading={loadingMore}
|
loading={loadingMore}
|
||||||
disabled={loadingMore}
|
disabled={loadingMore}
|
||||||
className={styles.historyItemLoadMore}
|
className={styles.historyItemLoadMore}
|
||||||
onClick={loadMore}
|
onClick={onLoadMore}
|
||||||
>
|
>
|
||||||
{t['com.affine.history.confirm-restore-modal.load-more']()}
|
{t['com.affine.history.confirm-restore-modal.load-more']()}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -468,11 +501,17 @@ const PageHistoryManager = ({
|
|||||||
[handleRestore]
|
[handleRestore]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [historyList, loadMore, loadingMore] = useDocSnapshotList(
|
||||||
|
workspaceId,
|
||||||
|
pageDocId
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<div className={styles.modalContent} data-empty={!activeVersion}>
|
<div className={styles.modalContent} data-empty={!activeVersion}>
|
||||||
<HistoryEditorPreview
|
<HistoryEditorPreview
|
||||||
ts={activeVersion}
|
ts={activeVersion}
|
||||||
|
historyList={historyList}
|
||||||
snapshotPage={snapshotPage}
|
snapshotPage={snapshotPage}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
@@ -480,8 +519,9 @@ const PageHistoryManager = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageHistoryList
|
<PageHistoryList
|
||||||
workspaceId={workspaceId}
|
historyList={historyList}
|
||||||
pageDocId={pageDocId}
|
onLoadMore={loadMore}
|
||||||
|
loadingMore={loadingMore}
|
||||||
activeVersion={activeVersion}
|
activeVersion={activeVersion}
|
||||||
onVersionChange={setActiveVersion}
|
onVersionChange={setActiveVersion}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const previewWrapper = style({
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
zIndex: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
width: `calc(100% - ${historyListWidth})`,
|
width: `calc(100% - ${historyListWidth})`,
|
||||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||||
@@ -47,24 +48,38 @@ export const previewContainer = style({
|
|||||||
boxShadow: cssVar('shadow3'),
|
boxShadow: cssVar('shadow3'),
|
||||||
height: 'calc(100% - 40px)',
|
height: 'calc(100% - 40px)',
|
||||||
width: `calc(100% - 80px)`,
|
width: `calc(100% - 80px)`,
|
||||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||||
|
transformOrigin: 'top center',
|
||||||
|
transition: 'all 0.5s ease-in-out',
|
||||||
|
selectors: {
|
||||||
|
'&[data-distance="> 2"]': {
|
||||||
|
transform: 'scale(0.60)',
|
||||||
|
opacity: 0,
|
||||||
|
zIndex: -3,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&[data-distance="2"]': {
|
||||||
|
transform: 'scale(0.90) translateY(-16px)',
|
||||||
|
zIndex: -2,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&[data-distance="1"]': {
|
||||||
|
transform: 'scale(0.95) translateY(-8px)',
|
||||||
|
zIndex: -1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&[data-distance="current"]': {
|
||||||
|
opacity: 1,
|
||||||
|
zIndex: 0,
|
||||||
|
},
|
||||||
|
'&[data-distance="< 0"]': {
|
||||||
|
transform: 'scale(1.60) translateY(18px)',
|
||||||
|
opacity: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const previewContainerStack1 = style([
|
|
||||||
previewContainer,
|
|
||||||
{
|
|
||||||
left: 48,
|
|
||||||
height: 'calc(100% - 32px)',
|
|
||||||
width: `calc(100% - 96px)`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
export const previewContainerStack2 = style([
|
|
||||||
previewContainer,
|
|
||||||
{
|
|
||||||
left: 56,
|
|
||||||
height: 'calc(100% - 24px)',
|
|
||||||
width: `calc(100% - 112px)`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
export const previewHeader = style({
|
export const previewHeader = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -13,14 +13,20 @@ import {
|
|||||||
PageTags,
|
PageTags,
|
||||||
type PageTagsProps,
|
type PageTagsProps,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
|
import { workbenchRoutes } from '@affine/core/router';
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
import { PageIcon, TagsIcon } from '@blocksuite/icons';
|
import { PageIcon, TagsIcon } from '@blocksuite/icons';
|
||||||
import { Schema, Workspace } from '@blocksuite/store';
|
import { Schema, Workspace } from '@blocksuite/store';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import { userEvent } from '@storybook/testing-library';
|
import { userEvent } from '@storybook/testing-library';
|
||||||
import { initEmptyPage } from '@toeverything/infra';
|
import { initEmptyPage } from '@toeverything/infra';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { withRouter } from 'storybook-addon-react-router-v6';
|
import {
|
||||||
|
reactRouterOutlets,
|
||||||
|
reactRouterParameters,
|
||||||
|
withRouter,
|
||||||
|
} from 'storybook-addon-react-router-v6';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'AFFiNE/PageList',
|
title: 'AFFiNE/PageList',
|
||||||
@@ -41,6 +47,11 @@ AffineOperationCell.args = {
|
|||||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||||
onRemoveToTrash: () => toast('Remove to trash'),
|
onRemoveToTrash: () => toast('Remove to trash'),
|
||||||
};
|
};
|
||||||
|
AffineOperationCell.parameters = {
|
||||||
|
reactRouter: reactRouterParameters({
|
||||||
|
routing: reactRouterOutlets(workbenchRoutes),
|
||||||
|
}),
|
||||||
|
};
|
||||||
AffineOperationCell.play = async ({ canvasElement }) => {
|
AffineOperationCell.play = async ({ canvasElement }) => {
|
||||||
{
|
{
|
||||||
const button = canvasElement.querySelector(
|
const button = canvasElement.querySelector(
|
||||||
|
|||||||
Reference in New Issue
Block a user