mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 10:21:35 +03: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:
parent
78ce30db69
commit
d4e78dd3d0
@ -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 (
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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',
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user