瀏覽代碼

Support quote block (#2415)

* feat: support quote block

* fix: database ts error
Kilu.He 2 年之前
父節點
當前提交
76b94e363e

+ 3 - 3
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,
         },
       };

+ 1 - 1
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 (
     <NodeContext.Provider value={node}>
-      <div data-block-id={node.id} className='doc-title relative mb-2 px-2 pt-[50px] text-4xl font-bold'>
+      <div data-block-id={node.id} className='doc-title relative mb-2 px-1 pt-[50px] text-4xl font-bold'>
         <TextBlock placeholder='Untitled' childIds={[]} node={node} />
       </div>
     </NodeContext.Provider>

+ 2 - 2
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<HTMLDivElement>) {
   return childIds && childIds.length > 0 ? (
-    <div className='pl-[1.5em]'>
+    <div {...props}>
       {childIds.map((item) => (
         <NodeComponent key={item} id={item} />
       ))}

+ 5 - 1
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<HTMLDivElement>) {
   const { node, childIds, isSelected, ref } = useNode(id);
@@ -22,6 +23,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
       case BlockType.TodoListBlock: {
         return <TodoListBlock node={node} childIds={childIds} />;
       }
+      case BlockType.QuoteBlock: {
+        return <QuoteBlock node={node} childIds={childIds} />;
+      }
       default:
         return null;
     }
@@ -31,7 +35,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
 
   return (
     <NodeContext.Provider value={node}>
-      <div {...props} ref={ref} data-block-id={node.id} className={`relative px-2  ${props.className}`}>
+      <div {...props} ref={ref} data-block-id={node.id} className={`relative px-1  ${props.className}`}>
         {renderBlock()}
         <div className='block-overlay' />
         {isSelected ? (

+ 20 - 0
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<BlockType.QuoteBlock>;
+  childIds?: string[];
+}) {
+  return (
+    <div className={'py-[2px]'}>
+      <div className={'border-l-4 border-solid border-main-accent px-3 '}>
+        <TextBlock node={node} />
+        <NodeChildren childIds={childIds} />
+      </div>
+    </div>
+  );
+}

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx

@@ -33,7 +33,7 @@ function TextBlock({
           />
         </Slate>
       </div>
-      <NodeChildren childIds={childIds} />
+      <NodeChildren className='pl-[1.5em]' childIds={childIds} />
     </>
   );
 }

+ 27 - 4
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,
   };
 }

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx

@@ -36,7 +36,7 @@ export default function TodoListBlock({
           <TextBlock node={node} />
         </div>
       </div>
-      <NodeChildren childIds={childIds} />
+      <NodeChildren className='pl-[1.5em]' childIds={childIds} />
     </>
   );
 }

+ 7 - 1
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

+ 7 - 0
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> = Type extends BlockType.HeadingBlock
   ? PageBlockData
   : Type extends BlockType.TodoListBlock
   ? TodoListBlockData
+  : Type extends BlockType.QuoteBlock
+  ? QuoteBlockData
   : TextBlockData;
 
 export interface NestedBlock<Type = any> {

+ 31 - 0
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,
+      })
+    );
+  }
+);

+ 10 - 1
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[];

+ 4 - 8
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>(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,

+ 11 - 0
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',
+  };
+}

+ 9 - 0
frontend/appflowy_tauri/src/appflowy_app/utils/document/slate/markdown.ts

@@ -18,6 +18,15 @@ export function canHandleToCheckboxBlock(event: React.KeyboardEvent<HTMLDivEleme
   return isCheckboxMarkdown;
 }
 
+export function canHandleToQuoteBlock(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
+  const flag = getMarkdownFlag(event, editor);
+  if (!flag) return false;
+
+  const isQuoteMarkdown = /^("|“|”)$/.test(flag);
+
+  return isQuoteMarkdown;
+}
+
 function getMarkdownFlag(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
   const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
   const selection = editor.selection;