mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: router & block preview
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"code-example": "^3.3.6",
|
||||
"codemirror": "6.0.1",
|
||||
"keymap": "link:@codemirror/next/keymap",
|
||||
"nanoid": "^4.0.0",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-window": "^1.8.7",
|
||||
"slate": "^0.81.1",
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { StyledBlockPreview } from '@toeverything/components/common';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '@toeverything/framework/virgo';
|
||||
import { debounce, sleep } from '@toeverything/utils';
|
||||
|
||||
const updateTitle = async (
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
onFinal: (title?: string) => void,
|
||||
retry = 0
|
||||
) => {
|
||||
const [page] = await services.api.editorBlock.search(workspace, {
|
||||
tag: `id:${blockId}`,
|
||||
});
|
||||
if (page?.content) {
|
||||
onFinal(page?.content);
|
||||
} else if (retry < 20) {
|
||||
await sleep(500);
|
||||
updateTitle(workspace, blockId, onFinal, retry + 1);
|
||||
} else {
|
||||
onFinal('Untitled');
|
||||
}
|
||||
};
|
||||
|
||||
const bindBlock = async (
|
||||
handleId: string,
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
onFinal: (title: string) => void,
|
||||
retry = 0
|
||||
): Promise<() => void> | undefined => {
|
||||
const block = await services.api.editorBlock.getBlock(workspace, blockId);
|
||||
if (block.id === blockId) {
|
||||
const debouncedOnFinal = debounce(onFinal, 100, { maxWait: 500 });
|
||||
const onUpdated = () =>
|
||||
updateTitle(workspace, blockId, debouncedOnFinal);
|
||||
block.on('content', handleId, onUpdated);
|
||||
onUpdated();
|
||||
return () => block.off('content', handleId);
|
||||
} else if (retry < 20) {
|
||||
await sleep(500);
|
||||
return bindBlock(handleId, workspace, blockId, onFinal, retry + 1);
|
||||
} else {
|
||||
onFinal('Untitled');
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const useBlockTitle = (block: AsyncBlock, blockId: string) => {
|
||||
const [title, setTitle] = useState('Loading...');
|
||||
|
||||
useEffect(() => {
|
||||
let callback: any = undefined;
|
||||
|
||||
bindBlock(
|
||||
block.id + nanoid(8),
|
||||
block.workspace,
|
||||
blockId,
|
||||
setTitle
|
||||
).then(cb => {
|
||||
callback = cb;
|
||||
});
|
||||
|
||||
return () => callback?.();
|
||||
}, [block.id, block.workspace, blockId]);
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
type BlockPreviewProps = {
|
||||
block: AsyncBlock;
|
||||
blockId: string;
|
||||
editorElement?: () => JSX.Element;
|
||||
};
|
||||
|
||||
const InternalBlockPreview = (props: BlockPreviewProps) => {
|
||||
const container = useRef<HTMLDivElement>();
|
||||
const [preview, setPreview] = useState(true);
|
||||
const title = useBlockTitle(props.block, props.blockId);
|
||||
|
||||
const AffineEditor = props.editorElement as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (container?.current) {
|
||||
const element = container?.current;
|
||||
const resizeObserver = new IntersectionObserver(entries => {
|
||||
const height = entries?.[0]?.intersectionRect.height;
|
||||
setPreview(height < 174);
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
return () => resizeObserver.unobserve(element);
|
||||
}
|
||||
return undefined;
|
||||
}, [container, props]);
|
||||
|
||||
return (
|
||||
<div ref={container}>
|
||||
<StyledBlockPreview title={title}>
|
||||
{preview ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
fontSize: '128px',
|
||||
height: '480px',
|
||||
alignItems: 'center',
|
||||
color: '#5591ff',
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
) : AffineEditor ? (
|
||||
<AffineEditor
|
||||
workspace={props.block.workspace}
|
||||
rootBlockId={props.blockId}
|
||||
/>
|
||||
) : null}
|
||||
</StyledBlockPreview>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockPreview = memo(InternalBlockPreview, (prev, next) => {
|
||||
return (
|
||||
prev.block.workspace === next.block.workspace &&
|
||||
prev.blockId === next.blockId
|
||||
);
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
import { FC, memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { StyledBlockPreview } from '@toeverything/components/common';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { AsyncBlock, useRecastBlockScene } from '@toeverything/framework/virgo';
|
||||
|
||||
import { formatUrl } from './format-url';
|
||||
import { SCENE_CONFIG } from '../../blocks/group/config';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { debounce } from '@toeverything/utils';
|
||||
import { BlockPreview } from './BlockView';
|
||||
const MouseMaskContainer = styled('div')({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
@@ -47,96 +45,6 @@ const _getLinkStyle = (scene: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
type BlockPreviewProps = {
|
||||
block: AsyncBlock;
|
||||
blockId: string;
|
||||
editorElement?: () => JSX.Element;
|
||||
};
|
||||
|
||||
const BlockPreview = (props: BlockPreviewProps) => {
|
||||
const container = useRef<HTMLDivElement>();
|
||||
const [preview, setPreview] = useState(true);
|
||||
const [title, setTitle] = useState('Loading...');
|
||||
useEffect(() => {
|
||||
let callback: any = undefined;
|
||||
services.api.editorBlock
|
||||
.getBlock(props.block.workspace, props.blockId)
|
||||
.then(block => {
|
||||
if (block.id === props.blockId) {
|
||||
const updateTitle = debounce(
|
||||
async () => {
|
||||
const [page] =
|
||||
await services.api.editorBlock.search(
|
||||
props.block.workspace,
|
||||
{ tag: 'id:affine67Uz4DstDk6PKUbz' }
|
||||
);
|
||||
|
||||
console.log(page);
|
||||
setTitle(page?.content || 'Untitled');
|
||||
},
|
||||
100,
|
||||
{ maxWait: 500 }
|
||||
);
|
||||
block.on('content', props.block.id, updateTitle);
|
||||
callback = () => block.off('content', props.block.id);
|
||||
updateTitle();
|
||||
} else {
|
||||
setTitle('Untitled');
|
||||
}
|
||||
});
|
||||
return () => callback?.();
|
||||
}, [props.block.id, props.block.workspace, props.blockId]);
|
||||
|
||||
const AffineEditor = props.editorElement as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (container?.current) {
|
||||
const element = container?.current;
|
||||
const resizeObserver = new IntersectionObserver(entries => {
|
||||
const height = entries?.[0]?.intersectionRect.height;
|
||||
setPreview(height < 174);
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
return () => resizeObserver.unobserve(element);
|
||||
}
|
||||
return undefined;
|
||||
}, [container]);
|
||||
|
||||
return (
|
||||
<div ref={container}>
|
||||
<StyledBlockPreview title={title}>
|
||||
{preview ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
fontSize: '128px',
|
||||
height: '480px',
|
||||
alignItems: 'center',
|
||||
color: '#5591ff',
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
) : AffineEditor ? (
|
||||
<AffineEditor
|
||||
workspace={props.block.workspace}
|
||||
rootBlockId={props.blockId}
|
||||
/>
|
||||
) : null}
|
||||
</StyledBlockPreview>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoBlockPreview = memo(BlockPreview, (prev, next) => {
|
||||
return (
|
||||
prev.block.workspace === next.block.workspace &&
|
||||
prev.blockId === next.blockId
|
||||
);
|
||||
});
|
||||
|
||||
const SourceViewContainer = styled('div')<{
|
||||
isSelected: boolean;
|
||||
scene: string;
|
||||
@@ -186,7 +94,7 @@ export const SourceView: FC<Props> = props => {
|
||||
scene={SCENE_CONFIG.REFLINK}
|
||||
style={{ padding: '0' }}
|
||||
>
|
||||
<MemoBlockPreview
|
||||
<BlockPreview
|
||||
block={block}
|
||||
editorElement={editorElement}
|
||||
blockId={src}
|
||||
|
||||
Reference in New Issue
Block a user