diff --git a/frontend/appflowy_web_app/jest.config.cjs b/frontend/appflowy_web_app/jest.config.cjs index 0d20a532414a8..eea60781d016a 100644 --- a/frontend/appflowy_web_app/jest.config.cjs +++ b/frontend/appflowy_web_app/jest.config.cjs @@ -17,4 +17,5 @@ module.exports = { '(.*)/node_modules/nanoid/.+\\.(j|t)sx?$': 'ts-jest', }, 'transformIgnorePatterns': [`/node_modules/(?!${esModules})`], + testMatch: ['**/*.test.ts'], }; \ No newline at end of file diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 8c8fd676272ee..c21b762d311e3 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -86,6 +86,7 @@ "slate": "^0.101.4", "slate-history": "^0.100.0", "slate-react": "^0.101.3", + "smooth-scroll-into-view-if-needed": "^2.0.2", "ts-results": "^3.3.0", "unsplash-js": "^7.0.19", "utf8": "^3.0.0", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 03e1e7e411b51..5dbb15bd1db52 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -197,6 +197,9 @@ dependencies: slate-react: specifier: ^0.101.3 version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) + smooth-scroll-into-view-if-needed: + specifier: ^2.0.2 + version: 2.0.2 ts-results: specifier: ^3.3.0 version: 3.3.0 @@ -7972,6 +7975,12 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /smooth-scroll-into-view-if-needed@2.0.2: + resolution: {integrity: sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q==} + dependencies: + scroll-into-view-if-needed: 3.1.0 + dev: false + /snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts index 2a3bd0a2e7e11..ce9bebcefbe2f 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts @@ -1,8 +1,9 @@ export enum CoverType { NormalColor = 'color', GradientColor = 'gradient', - BuildInImage = 'none', + BuildInImage = 'built_in', CustomImage = 'custom', LocalImage = 'local', UpsplashImage = 'unsplash', + None = 'none', } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index 9784292884b94..837063fd1ddab 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -1,6 +1,5 @@ import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; -import { applySlateOp } from '@/application/slate-yjs/utils/applySlateOpts'; -import { translateYjsEvent } from 'src/application/slate-yjs/utils/translateYjsEvent'; +import { applyToYjs } from '@/application/slate-yjs/utils/applyToYjs'; import { Editor, Operation, Descendant } from 'slate'; import Y, { YEvent, Transaction } from 'yjs'; import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert'; @@ -57,12 +56,11 @@ export const YjsEditor = { export function withYjs( editor: T, doc: Y.Doc, - { - localOrigin, - }: { + opts?: { localOrigin: CollabOrigin; } ): T & YjsEditor { + const { localOrigin = CollabOrigin.Local } = opts ?? {}; const e = editor as T & YjsEditor; const { apply, onChange } = e; @@ -76,23 +74,34 @@ export function withYjs( } e.children = content.children; + + console.log('initializeDocumentContent', doc.getMap(YjsEditorKey.data_section).toJSON(), e.children); Editor.normalize(editor, { force: true }); }; - e.applyRemoteEvents = (events: Array>, _: Transaction) => { + const applyIntercept = (op: Operation) => { + if (YjsEditor.connected(e)) { + YjsEditor.storeLocalChange(e, op); + } + + apply(op); + }; + + const applyRemoteIntercept = (op: Operation) => { + apply(op); + }; + + e.applyRemoteEvents = (_events: Array>, _: Transaction) => { + // Flush local changes to ensure all local changes are applied before processing remote events YjsEditor.flushLocalChanges(e); + // Replace the apply function to avoid storing remote changes as local changes + e.apply = applyRemoteIntercept; - // TODO: handle remote events - // This is a temporary implementation to apply remote events to slate + // Initialize or update the document content to ensure it is in the correct state before applying remote events initializeDocumentContent(); - Editor.withoutNormalizing(editor, () => { - events.forEach((event) => { - translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => { - // apply remote events to slate, don't call e.apply here because e.apply has been overridden. - apply(op); - }); - }); - }); + + // Restore the apply function to store local changes after applying remote changes + e.apply = applyIntercept; }; const handleYEvents = (events: Array>, transaction: Transaction) => { @@ -133,18 +142,12 @@ export function withYjs( // parse changes and apply to ydoc doc.transact(() => { changes.forEach((change) => { - applySlateOp(doc, { children: change.slateContent }, change.op); + applyToYjs(doc, { children: change.slateContent }, change.op); }); }, localOrigin); }; - e.apply = (op) => { - if (YjsEditor.connected(e)) { - YjsEditor.storeLocalChange(e, op); - } - - apply(op); - }; + e.apply = applyIntercept; e.onChange = () => { if (YjsEditor.connected(e)) { diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/applyRemoteEvents.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/applyRemoteEvents.ts new file mode 100644 index 0000000000000..7348ce0029022 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/applyRemoteEvents.ts @@ -0,0 +1,67 @@ +import { + CollabOrigin, + YBlocks, + YChildrenMap, + YjsEditorKey, + YMeta, + YSharedRoot, + YTextMap, +} from '@/application/collab.type'; +import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert'; +import { generateId, withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor'; +import { createEditor } from 'slate'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +export async function runApplyRemoteEventsTest() { + const pageId = generateId(); + const remoteDoc = withTestingYDoc(pageId); + const remote = withTestingYjsEditor(createEditor(), remoteDoc); + + const localDoc = new Y.Doc(); + + Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc)); + const editor = withTestingYjsEditor(createEditor(), localDoc); + + editor.connect(); + expect(editor.children).toEqual(remote.children); + + // update remote doc + insertBlock(remoteDoc, generateId(), pageId, 0); + remote.children = yDocToSlateContent(remoteDoc)?.children ?? []; + + // apply remote changes to local doc + Y.transact( + localDoc, + () => { + Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc)); + }, + CollabOrigin.Remote + ); + + expect(editor.children).toEqual(remote.children); +} + +function insertBlock(doc: Y.Doc, blockId: string, parentId: string, index: number) { + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + const document = sharedRoot.get(YjsEditorKey.document); + const blocks = document.get(YjsEditorKey.blocks) as YBlocks; + const meta = document.get(YjsEditorKey.meta) as YMeta; + const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap; + const textMap = meta.get(YjsEditorKey.text_map) as YTextMap; + + const block = new Y.Map(); + + block.set(YjsEditorKey.block_id, blockId); + block.set(YjsEditorKey.block_children, blockId); + block.set(YjsEditorKey.block_type, 'paragraph'); + block.set(YjsEditorKey.block_data, '{}'); + block.set(YjsEditorKey.block_external_id, blockId); + blocks.set(blockId, block); + childrenMap.set(blockId, new Y.Array()); + childrenMap.get(parentId).insert(index, [blockId]); + const text = new Y.Text(); + + text.insert(0, 'Hello, World!'); + textMap.set(blockId, text); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/convert.ts new file mode 100644 index 0000000000000..ef0c26b0545fb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/convert.ts @@ -0,0 +1,41 @@ +import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor'; +import { yDocToSlateContent } from '../convert'; +import { createEditor, Editor } from 'slate'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +function normalizedSlateDoc(doc: Y.Doc) { + const editor = createEditor(); + + const yjsEditor = withTestingYjsEditor(editor, doc); + + editor.children = yDocToSlateContent(doc)?.children ?? []; + return yjsEditor.children; +} + +export async function runCollaborationTest() { + const doc = withTestingYDoc('1'); + const editor = createEditor(); + const yjsEditor = withTestingYjsEditor(editor, doc); + + // Keep the 'local' editor state before applying run. + const baseState = Y.encodeStateAsUpdateV2(doc); + + Editor.normalize(editor, { force: true }); + + expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children); + + // Setup remote editor with input base state + const remoteDoc = new Y.Doc(); + + Y.applyUpdateV2(remoteDoc, baseState); + const remote = withTestingYjsEditor(createEditor(), remoteDoc); + + // Apply changes from 'run' + Y.applyUpdateV2(remoteDoc, Y.encodeStateAsUpdateV2(yjsEditor.sharedRoot.doc!)); + + // Verify remote and editor state are equal + expect(normalizedSlateDoc(remoteDoc)).toEqual(remote.children); + expect(yjsEditor.children).toEqual(remote.children); + expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/index.test.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/index.test.ts new file mode 100644 index 0000000000000..bcbd87176f62a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/index.test.ts @@ -0,0 +1,12 @@ +import { runCollaborationTest } from './convert'; +import { runApplyRemoteEventsTest } from './applyRemoteEvents'; + +describe('slate-yjs adapter', () => { + it('should pass the collaboration test', async () => { + await runCollaborationTest(); + }); + + it('should pass the apply remote events test', async () => { + await runApplyRemoteEventsTest(); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/withTestingYjsEditor.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/withTestingYjsEditor.ts new file mode 100644 index 0000000000000..9d6922ad625e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/__tests__/withTestingYjsEditor.ts @@ -0,0 +1,45 @@ +import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; +import { withYjs } from '@/application/slate-yjs'; +import { Editor } from 'slate'; +import * as Y from 'yjs'; +import { v4 as uuidv4 } from 'uuid'; + +export function generateId() { + return uuidv4(); +} + +export function withTestingYjsEditor(editor: Editor, doc: Y.Doc) { + const yjdEditor = withYjs(editor, doc, { + localOrigin: CollabOrigin.LocalSync, + }); + + return yjdEditor; +} + +export function withTestingYDoc(docId: string) { + const doc = new Y.Doc(); + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + const document = new Y.Map(); + const blocks = new Y.Map(); + const meta = new Y.Map(); + const children_map = new Y.Map(); + const text_map = new Y.Map(); + const rootBlock = new Y.Map(); + const blockOrders = new Y.Array(); + const pageId = docId; + + sharedRoot.set(YjsEditorKey.document, document); + document.set(YjsEditorKey.page_id, pageId); + document.set(YjsEditorKey.blocks, blocks); + document.set(YjsEditorKey.meta, meta); + meta.set(YjsEditorKey.children_map, children_map); + meta.set(YjsEditorKey.text_map, text_map); + children_map.set(pageId, blockOrders); + blocks.set(pageId, rootBlock); + rootBlock.set(YjsEditorKey.block_id, pageId); + rootBlock.set(YjsEditorKey.block_children, pageId); + rootBlock.set(YjsEditorKey.block_type, 'page'); + rootBlock.set(YjsEditorKey.block_data, '{}'); + rootBlock.set(YjsEditorKey.block_external_id, ''); + return doc; +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts deleted file mode 100644 index f6e9abaf4032f..0000000000000 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Operation, Node } from 'slate'; -import * as Y from 'yjs'; - -// transform slate op to yjs op and apply it to ydoc -export function applySlateOp(_ydoc: Y.Doc, _slateRoot: Node, _op: Operation) { - // console.log('applySlateOp', op); -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts new file mode 100644 index 0000000000000..98daed4817cd4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts @@ -0,0 +1,8 @@ +import { Operation, Node } from 'slate'; +import * as Y from 'yjs'; + +// transform slate op to yjs op and apply it to ydoc +export function applyToYjs(_ydoc: Y.Doc, _slateRoot: Node, op: Operation) { + if (op.type === 'set_selection') return; + console.log('applySlateOp', op); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts index 65689002ad147..e4ef8efce5de9 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts @@ -10,22 +10,14 @@ import { BlockData, BlockType, } from '@/application/collab.type'; +import { BlockJson } from '@/application/slate-yjs/utils/types'; import { getFontFamily } from '@/utils/font'; import { uniq } from 'lodash-es'; import { Element, Text } from 'slate'; -interface BlockJson { - id: string; - ty: string; - data?: string; - children?: string; - external_id?: string; -} - export function yDocToSlateContent(doc: YDoc): Element | undefined { const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; - console.log(sharedRoot.toJSON()); const document = sharedRoot.get(YjsEditorKey.document); const pageId = document.get(YjsEditorKey.page_id) as string; const blocks = document.get(YjsEditorKey.blocks) as YBlocks; diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts deleted file mode 100644 index 0565e8de9278c..0000000000000 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { YSharedRoot } from '@/application/collab.type'; -import * as Y from 'yjs'; -import { Editor, Operation } from 'slate'; - -export function translateYArrayEvent( - _sharedRoot: YSharedRoot, - _editor: Editor, - _event: Y.YEvent> -): Operation[] { - return []; -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts deleted file mode 100644 index c3f3bfd9035bd..0000000000000 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { YSharedRoot } from '@/application/collab.type'; -import { translateYArrayEvent } from '@/application/slate-yjs/utils/translateYjsEvent/arrayEvent'; -import { translateYMapEvent } from '@/application/slate-yjs/utils/translateYjsEvent/mapEvent'; -import { Editor, Operation } from 'slate'; -import * as Y from 'yjs'; -import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYjsEvent/textEvent'; - -/** - * Translate a yjs event into slate operations. The editor state has to match the - * yText state before the event occurred. - * - * @param sharedType - * @param op - */ -export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent): Operation[] { - if (event instanceof Y.YMapEvent) { - return translateYMapEvent(sharedRoot, editor, event); - } - - if (event instanceof Y.YTextEvent) { - return translateYTextEvent(sharedRoot, editor, event); - } - - if (event instanceof Y.YArrayEvent) { - return translateYArrayEvent(sharedRoot, editor, event); - } - - throw new Error('Unexpected Y event type'); -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts deleted file mode 100644 index cab9831833673..0000000000000 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { YSharedRoot } from '@/application/collab.type'; -import * as Y from 'yjs'; -import { Editor, Operation } from 'slate'; - -export function translateYMapEvent( - _sharedRoot: YSharedRoot, - _editor: Editor, - _event: Y.YEvent> -): Operation[] { - return []; -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts deleted file mode 100644 index 2fc6deca73c9c..0000000000000 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { YSharedRoot } from '@/application/collab.type'; -import * as Y from 'yjs'; -import { Editor, Operation } from 'slate'; - -export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent): Operation[] { - return []; -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts new file mode 100644 index 0000000000000..0dbe81a970559 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts @@ -0,0 +1,29 @@ +import { Node as SlateNode } from 'slate'; + +export interface BlockJson { + id: string; + ty: string; + data?: string; + children?: string; + external_id?: string; +} + +export interface Operation { + type: OperationType; +} + +export enum OperationType { + InsertNode = 'insert_node', + InsertChildren = 'insert_children', +} + +export interface InsertNodeOperation extends Operation { + type: OperationType.InsertNode; + node: SlateNode; +} + +export interface InsertChildrenOperation extends Operation { + type: OperationType.InsertChildren; + blockId: string; + children: string[]; +} diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png new file mode 100644 index 0000000000000..fb720222877b6 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png new file mode 100644 index 0000000000000..9ecf02d253c15 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png new file mode 100644 index 0000000000000..97072b04f49d3 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png new file mode 100644 index 0000000000000..00d26a05000be Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png new file mode 100644 index 0000000000000..3ecc9546c11ad Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png new file mode 100644 index 0000000000000..0abd2700e86bf Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png differ diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx index 53d0315271b27..1d014746229e3 100644 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx @@ -1,25 +1,21 @@ -import { DocCoverType, YDoc } from '@/application/collab.type'; -import { CoverType } from '@/application/folder-yjs/folder.type'; -import { useId } from '@/components/_shared/context-provider/IdProvider'; -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import { useBlockCover } from '@/components/document/document_header/useBlockCover'; import { showColorsForImage } from '@/components/document/document_header/utils'; import { renderColor } from '@/utils/color'; import React, { useCallback } from 'react'; -import DefaultImage from './default_cover.jpg'; - -function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: string) => void }) { - const viewId = useId().objectId; - const { extra } = usePageInfo(viewId); - - const pageCover = extra.cover; - const { cover } = useBlockCover(doc); +function DocumentCover({ + coverValue, + coverType, + onTextColor, +}: { + coverValue?: string; + coverType?: string; + onTextColor: (color: string) => void; +}) { const renderCoverColor = useCallback((color: string) => { return (
@@ -45,26 +41,14 @@ function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: s [onTextColor] ); - if (!pageCover && !cover?.cover_selection) return null; + if (!coverType || !coverValue) { + return null; + } + return ( -
- {pageCover ? ( - <> - {[CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type) - ? renderCoverColor(pageCover.value) - : null} - {CoverType.BuildInImage === pageCover.type ? renderCoverImage(DefaultImage) : null} - {[CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type) - ? renderCoverImage(pageCover.value) - : null} - - ) : cover?.cover_selection ? ( - <> - {cover.cover_selection_type === DocCoverType.Asset ? renderCoverImage(DefaultImage) : null} - {cover.cover_selection_type === DocCoverType.Color ? renderCoverColor(cover.cover_selection) : null} - {cover.cover_selection_type === DocCoverType.Image ? renderCoverImage(cover.cover_selection) : null} - - ) : null} +
+ {coverType === 'color' && renderCoverColor(coverValue)} + {(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)}
); } diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx index 3e7fd5ee28d14..04201f5ce511e 100644 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx @@ -1,7 +1,16 @@ -import { YDoc, YjsFolderKey } from '@/application/collab.type'; +import { DocCoverType, YDoc, YjsFolderKey } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; +import { CoverType } from '@/application/folder-yjs/folder.type'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; import DocumentCover from '@/components/document/document_header/DocumentCover'; +import { useBlockCover } from '@/components/document/document_header/useBlockCover'; import React, { memo, useMemo, useRef, useState } from 'react'; +import BuiltInImage1 from '@/assets/cover/m_cover_image_1.png'; +import BuiltInImage2 from '@/assets/cover/m_cover_image_2.png'; +import BuiltInImage3 from '@/assets/cover/m_cover_image_3.png'; +import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png'; +import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png'; +import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png'; export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { const ref = useRef(null); @@ -16,19 +25,59 @@ export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { } }, [icon]); + const { extra } = usePageInfo(viewId); + + const pageCover = extra.cover; + const { cover } = useBlockCover(doc); + + const coverType = useMemo(() => { + if ( + (pageCover && [CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)) || + cover?.cover_selection_type === DocCoverType.Color + ) { + return 'color'; + } + + if (CoverType.BuildInImage === pageCover?.type || cover?.cover_selection_type === DocCoverType.Asset) { + return 'built_in'; + } + + if ( + (pageCover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)) || + cover?.cover_selection_type === DocCoverType.Image + ) { + return 'custom'; + } + }, [cover?.cover_selection_type, pageCover]); + + const coverValue = useMemo(() => { + if (coverType === 'built_in') { + return { + 1: BuiltInImage1, + 2: BuiltInImage2, + 3: BuiltInImage3, + 4: BuiltInImage4, + 5: BuiltInImage5, + 6: BuiltInImage6, + }[pageCover?.value as string]; + } + + return pageCover?.value || cover?.cover_selection; + }, [coverType, cover?.cover_selection, pageCover]); + return (
- +
{iconObject?.value}
diff --git a/frontend/appflowy_web_app/src/components/document/document_header/default_cover.jpg b/frontend/appflowy_web_app/src/components/document/document_header/default_cover.jpg deleted file mode 100644 index aeaa6a0f29b2d..0000000000000 Binary files a/frontend/appflowy_web_app/src/components/document/document_header/default_cover.jpg and /dev/null differ diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx index cddab4cfb79b7..e26c066a713f8 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx @@ -3,6 +3,7 @@ import { EditorElementProps, HeadingNode, OutlineNode } from '@/components/edito import React, { forwardRef, memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSlate } from 'slate-react'; +import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed'; export const Outline = memo( forwardRef>(({ node, children, className, ...attributes }, ref) => { @@ -22,7 +23,10 @@ export const Outline = memo( const element = document.getElementById(id); if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + void smoothScrollIntoViewIfNeeded(element, { + behavior: 'smooth', + block: 'center', + }); } }, []); @@ -32,7 +36,14 @@ export const Outline = memo( const { text, level } = heading.data as { text: string; level: number }; return ( -
jumpToHeading(heading)} className={`my-1 ml-4 `} key={`${level}-${index}`}> +
{ + e.stopPropagation(); + jumpToHeading(heading); + }} + className={`my-1 ml-4 `} + key={`${level}-${index}`} + >
{text}
{children}
diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx index e7a2d288f2c88..51ed844e825ba 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx @@ -1,6 +1,6 @@ import { ToggleListNode } from '@/components/editor/editor.type'; import React from 'react'; -import { ReactComponent as RightSvg } from '$icons/16x/more.svg'; +import { ReactComponent as ExpandSvg } from '$icons/16x/drop_menu_show.svg'; function ToggleIcon({ block, className }: { block: ToggleListNode; className: string }) { const { collapsed } = block.data; @@ -14,7 +14,7 @@ function ToggleIcon({ block, className }: { block: ToggleListNode; className: st }} className={`${className} cursor-pointer pr-1 text-xl hover:text-fill-default`} > - {collapsed ? : } + {collapsed ? : } ); } diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx index cf9f697ff6644..9bad9f5eba37e 100644 --- a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx @@ -17,7 +17,9 @@ function Item({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: bo }} > {icon} - + {name || t('menuAppHeader.defaultNewPageName')}
diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css index d52ac6e96dbb9..de8fcf9824ace 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -48,4 +48,11 @@ --shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.3); --scrollbar-track: #252F41; --scrollbar-thumb: #3c4557; + --gradient1: linear-gradient(233deg, #34BDAF 0%, #B682D5 100%); + --gradient2: linear-gradient(180deg, #4CC2CC 0%, #E17570 100%); + --gradient3: linear-gradient(180deg, #AF70E1 0%, #ED7196 100%); + --gradient4: linear-gradient(180deg, #A348D6 0%, #45A7DF 100%); + --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); + --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); + --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css index bae74c9b3ef71..cd4ffee0f6e93 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -51,4 +51,11 @@ --shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); --scrollbar-thumb: #bdbdbd; --scrollbar-track: #e5e5e5; + --gradient1: linear-gradient(233deg, #34BDAF 0%, #B682D5 100%); + --gradient2: linear-gradient(180deg, #4CC2CC 0%, #E17570 100%); + --gradient3: linear-gradient(180deg, #AF70E1 0%, #ED7196 100%); + --gradient4: linear-gradient(180deg, #A348D6 0%, #45A7DF 100%); + --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); + --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); + --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/color.ts b/frontend/appflowy_web_app/src/utils/color.ts index 025c8c45ed324..9de9da1dcac3e 100644 --- a/frontend/appflowy_web_app/src/utils/color.ts +++ b/frontend/appflowy_web_app/src/utils/color.ts @@ -10,6 +10,16 @@ export enum ColorEnum { Blue = 'appflowy_them_color_tint9', } +export enum GradientEnum { + gradient1 = 'appflowy_them_color_gradient1', + gradient2 = 'appflowy_them_color_gradient2', + gradient3 = 'appflowy_them_color_gradient3', + gradient4 = 'appflowy_them_color_gradient4', + gradient5 = 'appflowy_them_color_gradient5', + gradient6 = 'appflowy_them_color_gradient6', + gradient7 = 'appflowy_them_color_gradient7', +} + export const colorMap = { [ColorEnum.Purple]: 'var(--tint-purple)', [ColorEnum.Pink]: 'var(--tint-pink)', @@ -22,6 +32,16 @@ export const colorMap = { [ColorEnum.Blue]: 'var(--tint-blue)', }; +export const gradientMap = { + [GradientEnum.gradient1]: 'var(--gradient1)', + [GradientEnum.gradient2]: 'var(--gradient2)', + [GradientEnum.gradient3]: 'var(--gradient3)', + [GradientEnum.gradient4]: 'var(--gradient4)', + [GradientEnum.gradient5]: 'var(--gradient5)', + [GradientEnum.gradient6]: 'var(--gradient6)', + [GradientEnum.gradient7]: 'var(--gradient7)', +}; + // Convert ARGB to RGBA // Flutter uses ARGB, but CSS uses RGBA function argbToRgba(color: string): string { @@ -46,5 +66,9 @@ export function renderColor(color: string) { return colorMap[color as ColorEnum]; } + if (gradientMap[color as GradientEnum]) { + return gradientMap[color as GradientEnum]; + } + return argbToRgba(color); } diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index 8431b8bd2f933..3f64b0bf66749 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -72,7 +72,7 @@ export default defineConfig({ }, envPrefix: ['AF', 'TAURI_'], esbuild: { - pure: !isDev ? ['console.log', 'console.debug', 'console.info'] : [], + pure: !isDev ? ['console.log', 'console.debug', 'console.info', 'console.trace'] : [], }, build: !!process.env.TAURI_PLATFORM ? {