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:
Peng Xiao 2024-02-29 14:08:21 +00:00
parent 78ce30db69
commit d4e78dd3d0
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
4 changed files with 103 additions and 41 deletions

View File

@ -63,11 +63,7 @@ export const useDocSnapshotList = (workspaceId: string, pageDocId: string) => {
return data.flatMap(page => page.workspace.histories);
}, [data]);
return [
histories,
shouldLoadMore ? loadMore : undefined,
loadingMore,
] as const;
return [histories, shouldLoadMore ? loadMore : false, !!loadingMore] as const;
};
const snapshotFetcher = async (

View File

@ -17,6 +17,7 @@ import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Doc, type PageMode, Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { atom, useAtom, useSetAtom } from 'jotai';
import { range } from 'lodash-es';
import {
Fragment,
type PropsWithChildren,
@ -24,6 +25,7 @@ import {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { encodeStateAsUpdate } from 'yjs';
@ -90,6 +92,7 @@ const ModalContainer = ({
interface HistoryEditorPreviewProps {
ts?: string;
historyList: HistoryList;
snapshotPage?: BlockSuiteDoc;
mode: PageMode;
onModeChange: (mode: PageMode) => void;
@ -98,6 +101,7 @@ interface HistoryEditorPreviewProps {
const HistoryEditorPreview = ({
ts,
historyList,
snapshotPage,
onModeChange,
mode,
@ -110,11 +114,9 @@ const HistoryEditorPreview = ({
onModeChange('edgeless');
}, [onModeChange]);
return (
<div className={styles.previewWrapper}>
<div className={styles.previewContainerStack2} />
<div className={styles.previewContainerStack1} />
<div className={styles.previewContainer}>
const content = useMemo(() => {
return (
<>
<div className={styles.previewHeader}>
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
<PageSwitchItem
@ -152,7 +154,38 @@ const HistoryEditorPreview = ({
<Loading size={24} />
</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>
);
};
@ -240,21 +273,21 @@ const PlanPrompt = () => {
) : null;
};
type HistoryList = ReturnType<typeof useDocSnapshotList>[0];
const PageHistoryList = ({
pageDocId,
workspaceId,
historyList,
onLoadMore,
loadingMore,
activeVersion,
onVersionChange,
}: {
workspaceId: string;
pageDocId: string;
activeVersion?: string;
onVersionChange: (version: string) => void;
historyList: HistoryList;
onLoadMore: (() => void) | false;
loadingMore: boolean;
}) => {
const [historyList, loadMore, loadingMore] = useDocSnapshotList(
workspaceId,
pageDocId
);
const historyListByDay = useMemo(() => {
return historyListGroupByDay(historyList);
}, [historyList]);
@ -330,13 +363,13 @@ const PageHistoryList = ({
</Collapsible.Root>
);
})}
{loadMore ? (
{onLoadMore ? (
<Button
type="plain"
loading={loadingMore}
disabled={loadingMore}
className={styles.historyItemLoadMore}
onClick={loadMore}
onClick={onLoadMore}
>
{t['com.affine.history.confirm-restore-modal.load-more']()}
</Button>
@ -468,11 +501,17 @@ const PageHistoryManager = ({
[handleRestore]
);
const [historyList, loadMore, loadingMore] = useDocSnapshotList(
workspaceId,
pageDocId
);
return (
<div className={styles.root}>
<div className={styles.modalContent} data-empty={!activeVersion}>
<HistoryEditorPreview
ts={activeVersion}
historyList={historyList}
snapshotPage={snapshotPage}
mode={mode}
onModeChange={setMode}
@ -480,8 +519,9 @@ const PageHistoryManager = ({
/>
<PageHistoryList
workspaceId={workspaceId}
pageDocId={pageDocId}
historyList={historyList}
onLoadMore={loadMore}
loadingMore={loadingMore}
activeVersion={activeVersion}
onVersionChange={setActiveVersion}
/>

View File

@ -30,6 +30,7 @@ export const previewWrapper = style({
flexGrow: 1,
height: '100%',
position: 'relative',
zIndex: 0,
overflow: 'hidden',
width: `calc(100% - ${historyListWidth})`,
backgroundColor: cssVar('backgroundSecondaryColor'),
@ -47,24 +48,38 @@ export const previewContainer = style({
boxShadow: cssVar('shadow3'),
height: 'calc(100% - 40px)',
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({
display: 'flex',
alignItems: 'center',

View File

@ -13,14 +13,20 @@ import {
PageTags,
type PageTagsProps,
} from '@affine/core/components/page-list';
import { workbenchRoutes } from '@affine/core/router';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { PageIcon, TagsIcon } from '@blocksuite/icons';
import { Schema, Workspace } from '@blocksuite/store';
import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react';
import { userEvent } from '@storybook/testing-library';
import { initEmptyPage } from '@toeverything/infra';
import { useState } from 'react';
import { withRouter } from 'storybook-addon-react-router-v6';
import {
reactRouterOutlets,
reactRouterParameters,
withRouter,
} from 'storybook-addon-react-router-v6';
export default {
title: 'AFFiNE/PageList',
@ -41,6 +47,11 @@ AffineOperationCell.args = {
onDisablePublicSharing: () => toast('Disable public sharing'),
onRemoveToTrash: () => toast('Remove to trash'),
};
AffineOperationCell.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(workbenchRoutes),
}),
};
AffineOperationCell.play = async ({ canvasElement }) => {
{
const button = canvasElement.querySelector(