mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Initialize appflowy block data and render block list (#1940)
This commit is contained in:
parent
21199c04ac
commit
1d28bed281
@ -16,28 +16,38 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"google-protobuf": "^3.21.2",
|
||||
"i18next": "^22.4.10",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"jest": "^29.4.3",
|
||||
"nanoid": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react18-input-otp": "^1.1.2",
|
||||
"redux": "^4.2.1",
|
||||
"rxjs": "^7.8.0",
|
||||
"slate": "^0.91.4",
|
||||
"slate-react": "^0.91.9",
|
||||
"ts-results": "^3.3.0",
|
||||
"utf8": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@types/google-protobuf": "^3.15.6",
|
||||
"@types/is-hotkey": "^0.1.7",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
|
4740
frontend/appflowy_tauri/pnpm-lock.yaml
Normal file
4740
frontend/appflowy_tauri/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,51 @@
|
||||
import { useSlate } from 'slate-react';
|
||||
import { toggleFormat, isFormatActive } from '$app/utils/editor/format';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useMemo } from 'react';
|
||||
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
|
||||
import { command, iconSize } from '$app/constants/toolbar';
|
||||
|
||||
const FormatButton = ({ format, icon }: { format: string; icon: string }) => {
|
||||
const editor = useSlate();
|
||||
|
||||
const renderComponent = useMemo(() => {
|
||||
switch (icon) {
|
||||
case 'bold':
|
||||
return <FormatBold sx={iconSize} />;
|
||||
case 'underlined':
|
||||
return <FormatUnderlined sx={iconSize} />;
|
||||
case 'italic':
|
||||
return <FormatItalic sx={iconSize} />;
|
||||
case 'code':
|
||||
return <CodeOutlined sx={iconSize} />;
|
||||
case 'strikethrough':
|
||||
return <StrikethroughSOutlined sx={iconSize} />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
slotProps={{ tooltip: { style: { background: '#E0F8FF', borderRadius: 8 } } }}
|
||||
title={
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-base font-medium text-black'>{command[format].title}</span>
|
||||
<span className='text-sm text-slate-400'>{command[format].key}</span>
|
||||
</div>
|
||||
}
|
||||
placement='top-start'
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{ color: isFormatActive(editor, format) ? '#00BCF0' : 'white' }}
|
||||
onClick={() => toggleFormat(editor, format)}
|
||||
>
|
||||
{renderComponent}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormatButton;
|
@ -0,0 +1,5 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
export const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
|
||||
const root = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
|
||||
return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFocused, useSlate } from 'slate-react';
|
||||
import FormatButton from './FormatButton';
|
||||
import { Portal } from './components';
|
||||
import { calcToolbarPosition } from '$app/utils/editor/toolbar';
|
||||
|
||||
const HoveringToolbar = ({ blockId }: { blockId: string }) => {
|
||||
const editor = useSlate();
|
||||
const inFocus = useFocused();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const blockDom = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
|
||||
const blockRect = blockDom?.getBoundingClientRect();
|
||||
|
||||
const position = calcToolbarPosition(editor, el, blockRect);
|
||||
|
||||
if (!position) {
|
||||
el.style.opacity = '0';
|
||||
} else {
|
||||
el.style.opacity = '1';
|
||||
el.style.top = position.top;
|
||||
el.style.left = position.left;
|
||||
}
|
||||
});
|
||||
|
||||
if (!inFocus) return null;
|
||||
|
||||
return (
|
||||
<Portal blockId={blockId}>
|
||||
<div
|
||||
ref={ref}
|
||||
className='z-1 absolute mt-[-6px] inline-flex h-[32px] items-stretch overflow-hidden rounded-[8px] bg-[#333] p-2 leading-tight shadow-lg transition-opacity duration-700'
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => (
|
||||
<FormatButton key={format} format={format} icon={format} />
|
||||
))}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HoveringToolbar;
|
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Block, BlockType } from '$app/interfaces';
|
||||
import PageBlock from '../PageBlock';
|
||||
import TextBlock from '../TextBlock';
|
||||
import HeadingBlock from '../HeadingBlock';
|
||||
import ListBlock from '../ListBlock';
|
||||
import CodeBlock from '../CodeBlock';
|
||||
|
||||
export default function BlockComponent({ block }: { block: Block }) {
|
||||
const renderComponent = () => {
|
||||
switch (block.type) {
|
||||
case BlockType.PageBlock:
|
||||
return <PageBlock block={block} />;
|
||||
case BlockType.TextBlock:
|
||||
return <TextBlock block={block} />;
|
||||
case BlockType.HeadingBlock:
|
||||
return <HeadingBlock block={block} />;
|
||||
case BlockType.ListBlock:
|
||||
return <ListBlock block={block} />;
|
||||
case BlockType.CodeBlock:
|
||||
return <CodeBlock block={block} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative' data-block-id={block.id}>
|
||||
{renderComponent()}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { BlockContext } from "$app/utils/block_context";
|
||||
import { buildTree } from "$app/utils/tree";
|
||||
import { Block } from "$app/interfaces";
|
||||
|
||||
export function useBlockList() {
|
||||
const blockContext = useContext(BlockContext);
|
||||
|
||||
const [blockList, setBlockList] = useState<Block[]>([]);
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!blockContext) return;
|
||||
const { blocksMap, id } = blockContext;
|
||||
if (!id || !blocksMap) return;
|
||||
const root = buildTree(id, blocksMap);
|
||||
if (!root) return;
|
||||
console.log(root);
|
||||
setTitle(root.data.title);
|
||||
setBlockList(root.children || []);
|
||||
}, [blockContext]);
|
||||
|
||||
return {
|
||||
title,
|
||||
blockList
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { useBlockList } from './BlockList.hooks';
|
||||
import BlockComponent from './BlockComponent';
|
||||
|
||||
export default function BlockList() {
|
||||
const { blockList, title } = useBlockList();
|
||||
|
||||
return (
|
||||
<div className='min-x-[0%] p-lg w-[900px] max-w-[100%]'>
|
||||
<div className='my-[50px] flex px-14 text-4xl font-bold'>{title}</div>
|
||||
<div className='px-14'>
|
||||
{blockList?.map((block) => (
|
||||
<BlockComponent key={block.id} block={block} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
|
||||
export default function CodeBlock({ block }: { block: Block }) {
|
||||
return <div>{block.data.text}</div>;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
const fontSize: Record<string, string> = {
|
||||
1: 'mt-8 text-3xl',
|
||||
2: 'mt-6 text-2xl',
|
||||
3: 'mt-4 text-xl',
|
||||
};
|
||||
export default function HeadingBlock({ block }: { block: Block }) {
|
||||
return (
|
||||
<div className={`${fontSize[block.data.level]} font-semibold `}>
|
||||
<TextBlock
|
||||
block={{
|
||||
...block,
|
||||
children: [],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
export default function ListBlock({ block }: { block: Block }) {
|
||||
const renderChildren = () => {
|
||||
return block.children?.map((item) => (
|
||||
<li key={item.id}>
|
||||
<BlockComponent block={item} />
|
||||
</li>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${block.data.type === 'ul' ? 'bulleted_list' : 'number_list'} flex`}>
|
||||
<li className='w-[24px]' />
|
||||
<div>
|
||||
<TextBlock
|
||||
block={{
|
||||
...block,
|
||||
children: [],
|
||||
}}
|
||||
/>
|
||||
{renderChildren()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
|
||||
export default function PageBlock({ block }: { block: Block }) {
|
||||
return <div className='cursor-pointer underline'>{block.data.title}</div>;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { RenderLeafProps } from 'slate-react';
|
||||
|
||||
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
|
||||
let newChildren = children;
|
||||
if ('bold' in leaf && leaf.bold) {
|
||||
newChildren = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if ('code' in leaf && leaf.code) {
|
||||
newChildren = <code className='rounded-sm bg-[#F2FCFF] p-1'>{newChildren}</code>;
|
||||
}
|
||||
|
||||
if ('italic' in leaf && leaf.italic) {
|
||||
newChildren = <em>{newChildren}</em>;
|
||||
}
|
||||
|
||||
if ('underlined' in leaf && leaf.underlined) {
|
||||
newChildren = <u>{newChildren}</u>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...attributes} className={'strikethrough' in leaf && leaf.strikethrough ? `line-through` : ''}>
|
||||
{newChildren}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Leaf;
|
@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
|
||||
import { createEditor } from 'slate';
|
||||
import { Slate, Editable, withReact } from 'slate-react';
|
||||
import Leaf from './Leaf';
|
||||
import HoveringToolbar from '$app/components/HoveringToolbar';
|
||||
import { triggerHotkey } from '$app/utils/editor/hotkey';
|
||||
|
||||
export default function TextBlock({ block }: { block: Block }) {
|
||||
const [editor] = useState(() => withReact(createEditor()));
|
||||
|
||||
return (
|
||||
<div className='mb-2'>
|
||||
<Slate
|
||||
editor={editor}
|
||||
onChange={(e) => console.log('===', e, editor.operations)}
|
||||
value={[
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
type: 'paragraph',
|
||||
children: [{ text: block.data.text }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HoveringToolbar blockId={block.id} />
|
||||
<Editable
|
||||
onKeyDownCapture={(event) => {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
triggerHotkey(event, editor);
|
||||
}}
|
||||
renderLeaf={(props) => <Leaf {...props} />}
|
||||
placeholder='Enter some text...'
|
||||
/>
|
||||
</Slate>
|
||||
<div className='pl-[1.5em]'>
|
||||
{block.children?.map((item: Block) => (
|
||||
<BlockComponent key={item.id} block={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
|
||||
export const iconSize = { width: 18, height: 18 };
|
||||
|
||||
export const command: Record<string, { title: string; key: string }> = {
|
||||
bold: {
|
||||
title: 'Bold',
|
||||
key: '⌘ + B',
|
||||
},
|
||||
underlined: {
|
||||
title: 'Underlined',
|
||||
key: '⌘ + U',
|
||||
},
|
||||
italic: {
|
||||
title: 'Italic',
|
||||
key: '⌘ + I',
|
||||
},
|
||||
code: {
|
||||
title: 'Mark as code',
|
||||
key: '⌘ + E',
|
||||
},
|
||||
strikethrough: {
|
||||
title: 'Strike through',
|
||||
key: '⌘ + Shift + S or ⌘ + Shift + X',
|
||||
},
|
||||
};
|
51
frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
Normal file
51
frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum BlockType {
|
||||
PageBlock = 0,
|
||||
HeadingBlock = 1,
|
||||
ListBlock = 2,
|
||||
TextBlock = 3,
|
||||
CodeBlock = 4,
|
||||
EmbedBlock = 5,
|
||||
QuoteBlock = 6,
|
||||
DividerBlock = 7,
|
||||
MediaBlock = 8,
|
||||
TableBlock = 9,
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type BlockData<T> = T extends BlockType.TextBlock ? TextBlockData :
|
||||
T extends BlockType.PageBlock ? PageBlockData :
|
||||
T extends BlockType.HeadingBlock ? HeadingBlockData:
|
||||
T extends BlockType.ListBlock ? ListBlockData : any;
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: BlockData<BlockType>;
|
||||
parent: string | null;
|
||||
prev: string | null;
|
||||
next: string | null;
|
||||
firstChild: string | null;
|
||||
lastChild: string | null;
|
||||
children?: Block[];
|
||||
}
|
||||
|
||||
|
||||
interface TextBlockData {
|
||||
text: string;
|
||||
attr: string;
|
||||
}
|
||||
|
||||
interface PageBlockData {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ListBlockData {
|
||||
type: 'ul' | 'ol';
|
||||
}
|
||||
|
||||
interface HeadingBlockData {
|
||||
level: number;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { Block, BlockType } from '../interfaces';
|
||||
|
||||
export const BlockContext = createContext<{
|
||||
id?: string;
|
||||
blocksMap?: Record<string, Block>;
|
||||
} | null>(null);
|
@ -0,0 +1,25 @@
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
Text,
|
||||
Node
|
||||
} from 'slate';
|
||||
|
||||
export function toggleFormat(editor: Editor, format: string) {
|
||||
const isActive = isFormatActive(editor, format)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ [format]: isActive ? null : true },
|
||||
{ match: Text.isText, split: true }
|
||||
)
|
||||
}
|
||||
|
||||
export const isFormatActive = (editor: Editor, format: string) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
match: (n: Node) => n[format] === true,
|
||||
mode: 'all',
|
||||
})
|
||||
return !!match
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { toggleFormat } from './format';
|
||||
import { Editor } from 'slate';
|
||||
|
||||
const HOTKEYS: Record<string, string> = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underline',
|
||||
'mod+e': 'code',
|
||||
'mod+shift+X': 'strikethrough',
|
||||
'mod+shift+S': 'strikethrough',
|
||||
};
|
||||
|
||||
export function triggerHotkey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault()
|
||||
const format = HOTKEYS[hotkey]
|
||||
toggleFormat(editor, format)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { Editor, Range } from 'slate';
|
||||
export function calcToolbarPosition(editor: Editor, el: HTMLDivElement, blockRect: DOMRect) {
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const domSelection = window.getSelection();
|
||||
let domRange;
|
||||
if (domSelection?.rangeCount === 0) {
|
||||
domRange = document.createRange();
|
||||
domRange.setStart(el, domSelection?.anchorOffset);
|
||||
domRange.setEnd(el, domSelection?.anchorOffset);
|
||||
} else {
|
||||
domRange = domSelection?.getRangeAt(0);
|
||||
}
|
||||
|
||||
const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
|
||||
|
||||
const top = `${-el.offsetHeight - 5}px`;
|
||||
const left = `${rect.left - blockRect.left - el.offsetWidth / 2 + rect.width / 2}px`;
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
}
|
||||
|
||||
}
|
36
frontend/appflowy_tauri/src/appflowy_app/utils/tree.ts
Normal file
36
frontend/appflowy_tauri/src/appflowy_app/utils/tree.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Block } from "../interfaces";
|
||||
|
||||
export function buildTree(id: string, blocksMap: Record<string, Block>) {
|
||||
const head = blocksMap[id];
|
||||
let node: Block | null = head;
|
||||
while(node) {
|
||||
|
||||
if (node.parent) {
|
||||
const parent = blocksMap[node.parent];
|
||||
!parent.children && (parent.children = []);
|
||||
parent.children.push(node);
|
||||
}
|
||||
|
||||
if (node.firstChild) {
|
||||
node = blocksMap[node.firstChild];
|
||||
} else if (node.next) {
|
||||
node = blocksMap[node.next];
|
||||
} else {
|
||||
while(node && node?.parent) {
|
||||
const parent: Block | null = blocksMap[node.parent];
|
||||
if (parent?.next) {
|
||||
node = blocksMap[parent.next];
|
||||
break;
|
||||
} else {
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
if (node.id === head.id) {
|
||||
node = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return head;
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DocumentEventGetDocument,
|
||||
DocumentVersionPB,
|
||||
OpenDocumentPayloadPB,
|
||||
} from '../../services/backend/events/flowy-document';
|
||||
import { Block, BlockType } from '../interfaces';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export const useDocument = () => {
|
||||
const params = useParams();
|
||||
const [blocksMap, setBlocksMap] = useState<Record<string, Block>>();
|
||||
const loadDocument = async (id: string): Promise<any> => {
|
||||
const getDocumentResult = await DocumentEventGetDocument(
|
||||
OpenDocumentPayloadPB.fromObject({
|
||||
@ -20,5 +25,149 @@ export const useDocument = () => {
|
||||
throw new Error('get document error');
|
||||
}
|
||||
};
|
||||
return { loadDocument };
|
||||
|
||||
const loadBlockData = async (blockId: string): Promise<Record<string, Block>> => {
|
||||
return {
|
||||
[blockId]: {
|
||||
id: blockId,
|
||||
type: BlockType.PageBlock,
|
||||
data: { title: 'Document Title' },
|
||||
parent: null,
|
||||
next: null,
|
||||
prev: null,
|
||||
firstChild: "A",
|
||||
lastChild: "E"
|
||||
},
|
||||
"A": {
|
||||
id: "A",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, text: 'A Heading-1' },
|
||||
parent: blockId,
|
||||
prev: null,
|
||||
next: "B",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"B": {
|
||||
id: "B",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Hello', attr: '' },
|
||||
parent: blockId,
|
||||
prev: "A",
|
||||
next: "C",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"C": {
|
||||
id: "C",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'block c' },
|
||||
prev: null,
|
||||
parent: blockId,
|
||||
next: "D",
|
||||
firstChild: "F",
|
||||
lastChild: null,
|
||||
},
|
||||
"D": {
|
||||
id: "D",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'number_list', text: 'D List' },
|
||||
prev: "C",
|
||||
parent: blockId,
|
||||
next: null,
|
||||
firstChild: "G",
|
||||
lastChild: "H",
|
||||
},
|
||||
"E": {
|
||||
id: "E",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'World', attr: '' },
|
||||
prev: "D",
|
||||
parent: blockId,
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"F": {
|
||||
id: "F",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Heading', attr: '' },
|
||||
prev: null,
|
||||
parent: "C",
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"G": {
|
||||
id: "G",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Item 1', attr: '' },
|
||||
prev: null,
|
||||
parent: "D",
|
||||
next: "H",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"H": {
|
||||
id: "H",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Item 2', attr: '' },
|
||||
prev: "G",
|
||||
parent: "D",
|
||||
next: "I",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"I": {
|
||||
id: "I",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, text: 'B Heading-1' },
|
||||
parent: blockId,
|
||||
prev: "H",
|
||||
next: 'L',
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"L": {
|
||||
id: "L",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: '456' },
|
||||
parent: blockId,
|
||||
prev: "I",
|
||||
next: 'J',
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"J": {
|
||||
id: "J",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, text: 'C Heading-1' },
|
||||
parent: blockId,
|
||||
prev: "L",
|
||||
next: "K",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"K": {
|
||||
id: "K",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: '123' },
|
||||
parent: blockId,
|
||||
prev: "J",
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!params?.id) return;
|
||||
const data = await loadBlockData(params.id);
|
||||
console.log(data);
|
||||
setBlocksMap(data);
|
||||
})();
|
||||
}, [params]);
|
||||
return { blocksMap, blockId: params.id };
|
||||
};
|
||||
|
@ -1,17 +1,28 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useDocument } from './DocumentPage.hooks';
|
||||
import BlockList from '../components/block/BlockList';
|
||||
import { BlockContext } from '../utils/block_context';
|
||||
import { createTheme, ThemeProvider } from '@mui/material';
|
||||
|
||||
const theme = createTheme({
|
||||
typography: {
|
||||
fontFamily: ['Poppins'].join(','),
|
||||
},
|
||||
});
|
||||
export const DocumentPage = () => {
|
||||
const params = useParams();
|
||||
const { loadDocument } = useDocument();
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!params?.id) return;
|
||||
const content: any = await loadDocument(params.id);
|
||||
console.log(content);
|
||||
})();
|
||||
}, [params]);
|
||||
const { blocksMap, blockId } = useDocument();
|
||||
|
||||
return <div className={'p-8'}>Document Page ID: {params.id}</div>;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div id='appflowy-block-doc' className='flex flex-col items-center'>
|
||||
<BlockContext.Provider
|
||||
value={{
|
||||
id: blockId,
|
||||
blocksMap,
|
||||
}}
|
||||
>
|
||||
<BlockList />
|
||||
</BlockContext.Provider>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
|
@ -16,6 +16,10 @@ body {
|
||||
font-family: Poppins;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-[#E0F8FF]
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply rounded-xl border border-gray-500 px-4 py-3;
|
||||
}
|
||||
|
@ -14,8 +14,15 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node", "jest"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"$app/*": ["src/appflowy_app/*"]
|
||||
},
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "../app_flowy/assets/translations"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
@ -24,4 +24,10 @@ export default defineConfig({
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@/', replacement: `${__dirname}/src/` },
|
||||
{ find: '$app/', replacement: `${__dirname}/src/appflowy_app/` }
|
||||
],
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user