feat(core): optimize history list item ui (#5440)
 fix TOV-90
This commit is contained in:
@@ -178,7 +178,11 @@ export const useSnapshotPage = (
|
|||||||
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
||||||
const map = new Map<string, DocHistory[]>();
|
const map = new Map<string, DocHistory[]>();
|
||||||
for (const history of histories) {
|
for (const history of histories) {
|
||||||
const day = new Date(history.timestamp).toLocaleDateString();
|
const day = new Date(history.timestamp).toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
const list = map.get(day) ?? [];
|
const list = map.get(day) ?? [];
|
||||||
list.push(history);
|
list.push(history);
|
||||||
map.set(day, list);
|
map.set(day, list);
|
||||||
@@ -208,8 +212,8 @@ export const useRestorePage = (workspace: Workspace, pageId: string) => {
|
|||||||
// should also update the page title, since it may be changed in the history
|
// should also update the page title, since it may be changed in the history
|
||||||
const title = page.meta.title;
|
const title = page.meta.title;
|
||||||
|
|
||||||
if (getPageMeta(pageDocId)?.title !== title) {
|
if (getPageMeta(pageId)?.title !== title) {
|
||||||
setPageTitle(pageDocId, title);
|
setPageTitle(pageId, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
await recover({
|
await recover({
|
||||||
@@ -230,6 +234,7 @@ export const useRestorePage = (workspace: Workspace, pageId: string) => {
|
|||||||
getPageMeta,
|
getPageMeta,
|
||||||
mutateQueryResource,
|
mutateQueryResource,
|
||||||
page,
|
page,
|
||||||
|
pageId,
|
||||||
recover,
|
recover,
|
||||||
setPageTitle,
|
setPageTitle,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Loading, Scrollable } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
BlockSuiteEditor,
|
BlockSuiteEditor,
|
||||||
EditorLoading,
|
EditorLoading,
|
||||||
@@ -8,11 +8,14 @@ import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
|||||||
import type { PageMode } from '@affine/core/atoms';
|
import type { PageMode } from '@affine/core/atoms';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
|
import { ToggleCollapseIcon } from '@blocksuite/icons';
|
||||||
import type { Page, Workspace } from '@blocksuite/store';
|
import type { Page, Workspace } from '@blocksuite/store';
|
||||||
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import {
|
import {
|
||||||
|
Fragment,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
Suspense,
|
Suspense,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -142,7 +145,9 @@ const HistoryEditorPreview = ({
|
|||||||
onModeChange={onModeChange}
|
onModeChange={onModeChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EditorLoading />
|
<div className={styles.loadingContainer}>
|
||||||
|
<Loading size={24} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -167,6 +172,8 @@ const PageHistoryList = ({
|
|||||||
return historyListGroupByDay(historyList);
|
return historyListGroupByDay(historyList);
|
||||||
}, [historyList]);
|
}, [historyList]);
|
||||||
|
|
||||||
|
const [collapsedMap, setCollapsedMap] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -182,25 +189,57 @@ const PageHistoryList = ({
|
|||||||
</div>
|
</div>
|
||||||
<Scrollable.Root className={styles.historyListScrollable}>
|
<Scrollable.Root className={styles.historyListScrollable}>
|
||||||
<Scrollable.Viewport className={styles.historyListScrollableInner}>
|
<Scrollable.Viewport className={styles.historyListScrollableInner}>
|
||||||
{historyListByDay.map(([day, list]) => {
|
{historyListByDay.map(([day, list], i) => {
|
||||||
|
const collapsed = collapsedMap[i];
|
||||||
return (
|
return (
|
||||||
<div key={day} className={styles.historyItemGroup}>
|
<Collapsible.Root
|
||||||
<div className={styles.historyItemGroupTitle}>{day}</div>
|
open={!collapsed}
|
||||||
{list.map(history => (
|
key={day}
|
||||||
|
className={styles.historyItemGroup}
|
||||||
|
>
|
||||||
|
<Collapsible.Trigger
|
||||||
|
role="button"
|
||||||
|
onClick={() =>
|
||||||
|
setCollapsedMap(prev => ({ ...prev, [i]: !collapsed }))
|
||||||
|
}
|
||||||
|
className={styles.historyItemGroupTitle}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.historyItem}
|
data-testid="page-list-group-header-collapsed-button"
|
||||||
key={history.timestamp}
|
className={styles.collapsedIconContainer}
|
||||||
data-testid="version-history-item"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onVersionChange(history.timestamp);
|
|
||||||
}}
|
|
||||||
data-active={activeVersion === history.timestamp}
|
|
||||||
>
|
>
|
||||||
<button>{timestampToLocalTime(history.timestamp)}</button>
|
<ToggleCollapseIcon
|
||||||
|
className={styles.collapsedIcon}
|
||||||
|
data-collapsed={!!collapsed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
{day}
|
||||||
</div>
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
{list.map((history, idx) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={history.timestamp}>
|
||||||
|
<div
|
||||||
|
className={styles.historyItem}
|
||||||
|
data-testid="version-history-item"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onVersionChange(history.timestamp);
|
||||||
|
}}
|
||||||
|
data-active={activeVersion === history.timestamp}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
{timestampToLocalTime(history.timestamp)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{idx > list.length - 1 ? (
|
||||||
|
<div className={styles.historyItemGap} />
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{loadMore ? (
|
{loadMore ? (
|
||||||
|
|||||||
@@ -71,6 +71,44 @@ export const editor = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const rowWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
':before': {
|
||||||
|
content: '""',
|
||||||
|
width: 1,
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: 'var(--affine-border-color)',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 16,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
transform: 'translate(-50%)',
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
'&:is(:last-of-type, :first-of-type):not(:last-of-type:first-of-type)::before':
|
||||||
|
{
|
||||||
|
height: '50%',
|
||||||
|
},
|
||||||
|
'&:last-of-type:first-of-type::before': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'&:first-of-type::before': {
|
||||||
|
top: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loadingContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: 'var(--affine-background-primary-color)',
|
||||||
|
});
|
||||||
|
|
||||||
export const historyList = style({
|
export const historyList = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -85,7 +123,6 @@ export const historyListScrollable = style({
|
|||||||
|
|
||||||
export const historyListScrollableInner = style({
|
export const historyListScrollableInner = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 16,
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,32 +139,58 @@ export const historyListHeader = style({
|
|||||||
export const historyItemGroup = style({
|
export const historyItemGroup = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
rowGap: 6,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const historyItemGroupTitle = style({
|
export const historyItemGroupTitle = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '12px',
|
padding: '0 12px 0 4px',
|
||||||
fontWeight: 'bold',
|
whiteSpace: 'nowrap',
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
gap: 4,
|
||||||
backgroundColor: 'var(--affine-background-primary-color)',
|
backgroundColor: 'var(--affine-background-primary-color)',
|
||||||
position: 'sticky',
|
height: 28,
|
||||||
top: 0,
|
':hover': {
|
||||||
});
|
background: 'var(--affine-hover-color)',
|
||||||
|
|
||||||
export const historyItem = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0 12px',
|
|
||||||
height: 32,
|
|
||||||
cursor: 'pointer',
|
|
||||||
selectors: {
|
|
||||||
'&:hover, &[data-active=true]': {
|
|
||||||
backgroundColor: 'var(--affine-hover-color)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const historyItem = style([
|
||||||
|
rowWrapper,
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0 32px',
|
||||||
|
height: 30,
|
||||||
|
cursor: 'pointer',
|
||||||
|
selectors: {
|
||||||
|
'&:hover, &[data-active=true]': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
// draw circle
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: '1px solid var(--affine-border-color)',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 16,
|
||||||
|
top: '50%',
|
||||||
|
bottom: 0,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
'&[data-active=true]::after': {
|
||||||
|
backgroundColor: 'var(--affine-primary-color)',
|
||||||
|
borderColor: 'var(--affine-black-30)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const historyItemGap = style([rowWrapper, { height: 16 }]);
|
||||||
|
|
||||||
export const historyItemLoadMore = style([
|
export const historyItemLoadMore = style([
|
||||||
historyItem,
|
historyItem,
|
||||||
{
|
{
|
||||||
@@ -186,3 +249,27 @@ export const emptyHistoryPromptDescription = style({
|
|||||||
fontSize: 'var(--affine-font-xs)',
|
fontSize: 'var(--affine-font-xs)',
|
||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const collapsedIcon = style({
|
||||||
|
transition: 'transform 0.2s ease-in-out',
|
||||||
|
selectors: {
|
||||||
|
'&[data-collapsed="false"]': {
|
||||||
|
transform: 'rotate(90deg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collapsedIconContainer = style({
|
||||||
|
fontSize: 24,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '2px',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
color: 'inherit',
|
||||||
|
selectors: {
|
||||||
|
'&[data-collapsed="true"]': {
|
||||||
|
transform: 'rotate(-90deg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user