From 76b94e363e237bfb26a1bf13ee53e0fe4d918502 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Wed, 3 May 2023 14:54:07 +0800 Subject: [PATCH] Support quote block (#2415) * feat: support quote block * fix: database ts error --- .../_shared/database-hooks/loadField.ts | 6 ++-- .../document/DocumentTitle/index.tsx | 2 +- .../components/document/Node/NodeChildren.tsx | 4 +-- .../components/document/Node/index.tsx | 6 +++- .../components/document/QuoteBlock/index.tsx | 20 ++++++++++++ .../components/document/TextBlock/index.tsx | 2 +- .../document/TextBlock/useMarkDown.hooks.ts | 31 ++++++++++++++++--- .../document/TodoListBlock/index.tsx | 2 +- .../appflowy_app/constants/document/config.ts | 8 ++++- .../src/appflowy_app/interfaces/document.ts | 7 +++++ .../document/async-actions/blocks/quote.ts | 31 +++++++++++++++++++ .../utils/document/blocks/common.ts | 11 ++++++- .../utils/document/blocks/heading.ts | 12 +++---- .../utils/document/blocks/quote.ts | 11 +++++++ .../utils/document/slate/markdown.ts | 9 ++++++ 15 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/QuoteBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/quote.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/quote.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts index 94f5455c67..93546940b5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts @@ -70,7 +70,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?: title: field.name, fieldType: field.field_type, fieldOptions: { - NumberFormatPB: typeOption.format, + numberFormat: typeOption.format, }, }; } @@ -82,8 +82,8 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?: title: field.name, fieldType: field.field_type, fieldOptions: { - DateFormatPB: typeOption.date_format, - TimeFormatPB: typeOption.time_format, + dateFormat: typeOption.date_format, + timeFormat: typeOption.time_format, includeTime: typeOption.include_time, }, }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx index 4092759455..555bbf6d12 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx @@ -8,7 +8,7 @@ export default function DocumentTitle({ id }: { id: string }) { if (!node) return null; return ( -
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx index 0a02f70b5d..7b9978cda7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx @@ -1,9 +1,9 @@ import React from 'react'; import NodeComponent from '$app/components/document/Node/index'; -function NodeChildren({ childIds }: { childIds?: string[] }) { +function NodeChildren({ childIds, ...props }: { childIds?: string[] } & React.HTMLAttributes) { return childIds && childIds.length > 0 ? ( -
+
{childIds.map((item) => ( ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index 4ecca39f8d..0c4adaa14a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -7,6 +7,7 @@ import { NodeContext } from '../_shared/SubscribeNode.hooks'; import { BlockType } from '$app/interfaces/document'; import HeadingBlock from '$app/components/document/HeadingBlock'; import TodoListBlock from '$app/components/document/TodoListBlock'; +import QuoteBlock from '$app/components/document/QuoteBlock'; function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { const { node, childIds, isSelected, ref } = useNode(id); @@ -22,6 +23,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes; } + case BlockType.QuoteBlock: { + return ; + } default: return null; } @@ -31,7 +35,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes -
+
{renderBlock()}
{isSelected ? ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/QuoteBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/QuoteBlock/index.tsx new file mode 100644 index 0000000000..f9ed574ae1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/QuoteBlock/index.tsx @@ -0,0 +1,20 @@ +import { BlockType, NestedBlock } from '$app/interfaces/document'; +import TextBlock from '$app/components/document/TextBlock'; +import NodeChildren from '$app/components/document/Node/NodeChildren'; + +export default function QuoteBlock({ + node, + childIds, +}: { + node: NestedBlock; + childIds?: string[]; +}) { + return ( +
+
+ + +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index 201a7558a9..fc430b1ce5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -33,7 +33,7 @@ function TextBlock({ />
- + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useMarkDown.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useMarkDown.hooks.ts index 941f300bba..ea8e86231e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useMarkDown.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useMarkDown.hooks.ts @@ -1,14 +1,19 @@ import { useCallback, useContext, useMemo } from 'react'; import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document'; import { keyBoardEventKeyMap } from '$app/constants/document/text_block'; -import { canHandleToHeadingBlock, canHandleToCheckboxBlock } from '$app/utils/document/slate/markdown'; +import { + canHandleToHeadingBlock, + canHandleToCheckboxBlock, + canHandleToQuoteBlock, +} from '$app/utils/document/slate/markdown'; import { useAppDispatch } from '$app/stores/store'; import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; import { turnToHeadingBlockThunk } from '$app_reducers/document/async-actions/blocks/heading'; import { turnToTodoListBlockThunk } from '$app_reducers/document/async-actions/blocks/todo_list'; +import { turnToQuoteBlockThunk } from '$app_reducers/document/async-actions/blocks/quote'; export function useMarkDown(id: string) { - const { toHeadingBlockAction, toCheckboxBlockAction } = useActions(id); + const { toHeadingBlockAction, toCheckboxBlockAction, toQuoteBlockAction } = useActions(id); const toHeadingBlockEvent = useMemo(() => { return { triggerEventKey: keyBoardEventKeyMap.Space, @@ -25,9 +30,17 @@ export function useMarkDown(id: string) { }; }, [toCheckboxBlockAction]); + const toQuoteBlockEvent = useMemo(() => { + return { + triggerEventKey: keyBoardEventKeyMap.Space, + canHandle: canHandleToQuoteBlock, + handler: toQuoteBlockAction, + }; + }, [toQuoteBlockAction]); + const markdownEvents = useMemo( - () => [toHeadingBlockEvent, toCheckboxBlockEvent], - [toHeadingBlockEvent, toCheckboxBlockEvent] + () => [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent], + [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent] ); return { @@ -56,8 +69,18 @@ function useActions(id: string) { [controller, dispatch, id] ); + const toQuoteBlockAction = useCallback( + (...args: TextBlockKeyEventHandlerParams) => { + if (!controller) return; + const [_event, editor] = args; + dispatch(turnToQuoteBlockThunk({ id, controller, editor })); + }, + [controller, dispatch, id] + ); + return { toHeadingBlockAction, toCheckboxBlockAction, + toQuoteBlockAction, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx index 1ae9a33c00..fa7c5e76e0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx @@ -36,7 +36,7 @@ export default function TodoListBlock({
- + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts index 3bfae1ddca..7e81043af9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts @@ -3,7 +3,13 @@ import { BlockType } from '$app/interfaces/document'; /** * Block types that are allowed to have children */ -export const allowedChildrenBlockTypes = [BlockType.TextBlock, BlockType.PageBlock, BlockType.TodoListBlock]; +export const allowedChildrenBlockTypes = [ + BlockType.TextBlock, + BlockType.PageBlock, + BlockType.TodoListBlock, + BlockType.QuoteBlock, + BlockType.CalloutBlock, +]; /** * Block types that split node can extend to the next line diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index 7545482c08..0c529dc1ab 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -8,6 +8,7 @@ export enum BlockType { CodeBlock = 'code', EmbedBlock = 'embed', QuoteBlock = 'quote', + CalloutBlock = 'callout', DividerBlock = 'divider', MediaBlock = 'media', TableBlock = 'table', @@ -22,6 +23,10 @@ export interface TodoListBlockData extends TextBlockData { checked: boolean; } +export interface QuoteBlockData extends TextBlockData { + size: 'default' | 'large'; +} + export interface TextBlockData { delta: TextDelta[]; } @@ -34,6 +39,8 @@ export type BlockData = Type extends BlockType.HeadingBlock ? PageBlockData : Type extends BlockType.TodoListBlock ? TodoListBlockData + : Type extends BlockType.QuoteBlock + ? QuoteBlockData : TextBlockData; export interface NestedBlock { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/quote.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/quote.ts new file mode 100644 index 0000000000..5950825233 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/quote.ts @@ -0,0 +1,31 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { DocumentController } from '$app/stores/effects/document/document_controller'; +import { BlockType } from '$app/interfaces/document'; +import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to'; +import { Editor } from 'slate'; +import { getQuoteDataFromEditor } from '$app/utils/document/blocks/quote'; + +/** + * transform to quote block + * 1. insert quote block after current block + * 2. move children to quote block + * 3. delete current block + */ +export const turnToQuoteBlockThunk = createAsyncThunk( + 'document/turnToQuoteBlock', + async (payload: { id: string; editor: Editor; controller: DocumentController }, thunkAPI) => { + const { id, controller, editor } = payload; + const { dispatch } = thunkAPI; + const data = getQuoteDataFromEditor(editor); + if (!data) return; + + await dispatch( + turnToBlockThunk({ + id, + controller, + type: BlockType.QuoteBlock, + data, + }) + ); + } +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts index 3765525cb6..d521d4f1d4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts @@ -1,8 +1,9 @@ import { BlockData, BlockType, DocumentState, NestedBlock, TextDelta } from '$app/interfaces/document'; -import { Descendant, Element, Text } from 'slate'; +import { Descendant, Editor, Element, Text } from 'slate'; import { BlockPB } from '@/services/backend'; import { Log } from '$app/utils/log'; import { nanoid } from 'nanoid'; +import { getAfterRangeAt } from '$app/utils/document/slate/text'; export function deltaToSlateValue(delta: TextDelta[]) { const slateNode = { @@ -21,6 +22,14 @@ export function deltaToSlateValue(delta: TextDelta[]) { return slateNodes; } +export function getDeltaAfterSelection(editor: Editor): TextDelta[] | undefined { + const selection = editor.selection; + if (!selection) return; + const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection)); + const delta = getDeltaFromSlateNodes(slateNodes); + return delta; +} + export function getDeltaFromSlateNodes(slateNodes: Descendant[]) { const element = slateNodes[0] as Element; const children = element.children as Text[]; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/heading.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/heading.ts index 6677a1bb64..86b7138bee 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/heading.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/heading.ts @@ -1,11 +1,7 @@ import { Editor } from 'slate'; import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text'; -import { BlockType, HeadingBlockData, NestedBlock } from '$app/interfaces/document'; -import { getDeltaFromSlateNodes, newBlock } from '$app/utils/document/blocks/common'; - -export function newHeadingBlock(parentId: string, data: HeadingBlockData): NestedBlock { - return newBlock(BlockType.HeadingBlock, parentId, data); -} +import { HeadingBlockData } from '$app/interfaces/document'; +import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common'; /** * get heading data from editor, only support markdown @@ -17,8 +13,8 @@ export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | und const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection)); const level = hashTags.match(/#/g)?.length; if (!level) return; - const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection)); - const delta = getDeltaFromSlateNodes(slateNodes); + const delta = getDeltaAfterSelection(editor); + if (!delta) return; return { level, delta, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/quote.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/quote.ts new file mode 100644 index 0000000000..82b321d284 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/quote.ts @@ -0,0 +1,11 @@ +import { Editor } from 'slate'; +import { getDeltaAfterSelection } from '$app/utils/document/blocks/common'; + +export function getQuoteDataFromEditor(editor: Editor) { + const delta = getDeltaAfterSelection(editor); + if (!delta) return; + return { + delta, + size: 'default', + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate/markdown.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate/markdown.ts index 8cca17aca0..8118a20eaa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate/markdown.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate/markdown.ts @@ -18,6 +18,15 @@ export function canHandleToCheckboxBlock(event: React.KeyboardEvent, editor: Editor) { + const flag = getMarkdownFlag(event, editor); + if (!flag) return false; + + const isQuoteMarkdown = /^("|“|”)$/.test(flag); + + return isQuoteMarkdown; +} + function getMarkdownFlag(event: React.KeyboardEvent, editor: Editor) { const isSpaceKey = event.key === keyBoardEventKeyMap.Space; const selection = editor.selection;